做Java后端,天天跟数据库打交道,MyBatis和MyBatis-Plus(下面直接叫MP)是绕不开的两个名字。很多人一开始会以为MP就是MyBatis的升级版,其实这是个挺常见的误解。MyBatis是底层SQL映射框架,负责把Java方法和SQL绑定起来;而MP是在MyBatis之上做了一层“开箱即用”的封装,把常用的CRUD、分页、逻辑删除、字段自动填充这些都内置好了。两者不是竞争关系,而是“基座”和“增强工具”的关系。这篇文章我会结合自己实际项目里的经验,把它们的核心区别、选型思路、SpringBoot集成方式、常见坑和面试高频题一次讲清楚。不管是刚入门的新手,还是准备面试或者正在做技术选型的朋友,都能从里面找到点有用的东西。
1. 为什么纠结这两个:核心区别与选型思路
1.1 从JDBC到MyBatis,我们到底在用什么
先回到最底层。Java操作数据库,原生的方式就是JDBC,写起来极其痛苦:要手动加载驱动、获取Connection、写PreparedStatement、处理ResultSet、关闭资源。后来出现了MyBatis,它干了三件大事:
- 把SQL语句和Java代码解耦,SQL写到XML或注解里,不再散落在代码中。
- 自动完成参数映射和结果集映射,JavaBean和数据库字段之间的转换交给框架处理。
- 内置了连接池、事务管理、缓存等能力,开发效率比JDBC高出一个量级。
所以MyBatis的本质是一个“半自动”的ORM框架。半自动的意思是,SQL还是要自己写,但参数处理和结果映射不用管。这个设计给它带来了极高的灵活性,也带来了一个痛点:如果真的只是简单的增删改查,也要写一堆重复XML。Mapper接口里加五个方法,XML里就要配五个对应id的SQL,很机械。
这时候MyBatis-Plus出现了。它不改变MyBatis的底层机制,而是在MyBatis之上封装了一套通用能力。最核心的就是BaseMapper接口,里面已经实现了insert、deleteById、selectById、selectList等方法。你只要让自己的Mapper接口继承BaseMapper,然后什么都不用写,就有了最基础的CRUD能力。这就是它叫“Plus”的原因:不是替代MyBatis,而是帮我把MyBatis中那些繁琐的重复劳动砍掉。
1.2 MyBatis-Plus到底“Plus”在哪
用一句话概括:MyBatis解决的是“SQL映射”的问题,MP解决的是“SQL都懒得写”的问题。
MP的增强点主要集中在以下几个方面:
- 通用Mapper:继承BaseMapper,单表CRUD不用写SQL和XML。
- 条件构造器:通过QueryWrapper或LambdaQueryWrapper以链式方法拼接查询条件,不用记XML里的动态SQL标签。
- 分页插件:基于MyBatis的拦截器机制实现物理分页,传入Page对象即可。
- 逻辑删除:@TableLogic注解,删除时自动改成update语句。
- 自动填充:@TableField(fill = FieldFill.INSERT)等注解,让createTime、updateTime自动赋值。
- 乐观锁:@Version注解,配合插件实现版本号判断。
- 代码生成器:一键生成Entity、Mapper、Service、Controller全套代码。
这些功能不是MyBatis没有,而是MP把它们做成了“约定大于配置”的默认能力。比如逻辑删除,原生MyBatis里你要自己写UPDATE语句,MP只要在实体字段上加一个注解,然后全局配置一下delete值,查询的时候它会自动拼接deleted=0的条件。这件事在代码里完全透明,能省下不少事。
1.3 什么时候用MyBatis,什么时候直接上MP
结合我自己经手的项目,选型逻辑其实很简单:
- 如果项目是单表操作为主、表结构相对规范,可以无脑选MP,开发效率提升非常明显。
- 如果项目里大量复杂报表查询、多表关联、动态SQL非常考验数据库特性,那一定要保留MyBatis原生的XML写法,MP只用来做单表CRUD。
- 如果项目里既有简单CRUD又有复杂查询,那两者完全可以共存,这也是目前很多团队的实际状态。
- 如果团队里有严格的SQL审查规范、DBA要求所有SQL必须人工编写以控制执行计划,那可以直接用MyBatis,MP的自动SQL有时候让人不放心。
所以我不太建议把MP当成万能药。它确实快,但“自动化”意味着“不可见”,在一些对SQL有极致要求的场景里,控制力下降反而会成为风险点。
2. 核心功能对比:从CRUD到复杂查询,差别比你想的大
2.1 CRUD:MP的BaseMapper省掉的不只是XML
原生MyBatis写一个简单的按ID查询,需要做两步:Mapper接口里声明一个方法,XML里写一段select语句。表一多,这些模板代码会大量堆积。MP的BaseMapper直接帮我做了这个事:
public interface UserMapper extends BaseMapper<User> { // 没有声明任何方法,但已经有了 insert/deleteById/selectById/selectList 等 }调用的时候:
User user = userMapper.selectById(1); userMapper.insert(user); userMapper.deleteById(2);这里的selectById、insert是BaseMapper里自带的,MP在启动时通过MyBatis的Mapper注册机制,为这些通用方法自动生成了对应SQL,不需要我去XML里写。这个机制的核心在于泛型参数User,MP通过反射拿到实体的表名、字段名、主键名,然后动态拼出SQL。
有一点值得注意:BaseMapper里所有方法都是单表操作,永远不要想着用它来写关联查询。关联查询还是老老实实在XML里写,或者用自定义Mapper方法。把简单和复杂分开,代码结构才会清晰。
2.2 条件构造器QueryWrapper与LambdaQueryWrapper实战
这是很多初学者最容易懵的地方。QueryWrapper是什么?其实就是MP提供的一个拼接查询条件的“积木盒”。
举个实际场景:查询名字叫“张三”且年龄大于18的用户列表,只要性别是男。
用LambdaQueryWrapper写:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(User::getName, "张三") .gt(User::getAge, 18) .eq(User::getGender, "男"); List<User> users = userMapper.selectList(wrapper);这里eq代表等于,gt代表大于,方法名本身就是语义。用Lambda表达式User::getName,好处是字段名不会写错,因为它是类型安全的,编译期就能发现错误。而如果使用老的QueryWrapper:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("name", "张三") .gt("age", 18) .eq("gender", "男");这样做SQL字段名是字符串,一旦表结构修改,这里就容易出现“运行时才发现列名不对”的问题。所以我个人在项目里是强制要求用LambdaQueryWrapper的。
条件构造器里还有一个很好用的or方法。比如查询名字是“张三”或者年龄大于20的用户:
wrapper.eq(User::getName, "张三") .or() .gt(User::getAge, 20);一般我用的时候都会加括号控制优先级,否则or可能会和后面的条件发生意外组合。比如再往后跟一个status=1,实际SQL会变成name='张三' OR age>20 AND status=1,逻辑就错了。遇到这种情况,可以用lambda内部嵌套:
wrapper.and(w -> w.eq(User::getName, "张三").or().gt(User::getAge, 20)) .eq(User::getStatus, 1);这个细节在参考网上一些踩坑帖的时候经常能看到,也是我实际项目里踩过的坑,必须拿出来说。
2.3 分页插件:PageHelper和MP分页配置对比
分页是另一个高频需求。原生MyBatis时代很多人用PageHelper,通过ThreadLocal方式在SQL执行前自动拼接limit语句。用起来确实方便,但有个痛点:PageHelper生效的前提是调用PageHelper.startPage()之后必须紧跟一条查询,如果中间穿插了其它MyBatis操作,分页可能被污染。
MP的分页插件则更“刻意”。它采用传入Page对象的方式,在Mapper方法参数里带上Page,MyBatis在解析时就能拿到分页参数,执行完SQL后,Page对象里会带上total、current、size这些信息。示例:
Page<User> page = new Page<>(1, 10); Page<User> result = userMapper.selectPage(page, wrapper); List<User> users = result.getRecords(); long total = result.getTotal();使用MP分页时,需要先注册一个分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个插件依赖MyBatis的Interceptor机制,可以在SQL执行前拦截Executor,把limit参数拼到SQL里去。这里要注意DbType必须配置正确,否则不同数据库的方言拼接逻辑不一样,很容易出问题。
2.4 逻辑删除、自动填充、乐观锁这些“隐形”功能
这三个功能属于用了就回不去的类型,但也最容易在配置上出幺蛾子。
逻辑删除,指的是删除数据时不真正执行DELETE,而是将记录标记为已删除。MP的做法是在实体字段上加@TableLogic注解,然后配置全局删除值和未删除值。比如:
@TableLogic private Integer deleted;配置文件里这样写:
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这样我调用deleteById时,MP会执行一条UPDATE语句,把deleted置为1。后续所有查询,MP都会自动追加deleted=0条件。但要注意:如果我在XML里自定义了SQL,且没有手动加deleted=0条件,那么逻辑删除对这条SQL是无效的。所以逻辑删除适合纯MP操作,自定义SQL里要自己注意。
自动填充,用来处理create_time、update_time这类字段。实体里加上:
@TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime;再实现一个MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); this.strictInsertFill(metaObject, "updateTime", Date.class, new Date()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); } }这样在insert和update时,公共字段不用每个实体都手动set,统一入口维护,代码干净不少。
乐观锁,用来解决并发修改的问题。实体里加@Version字段,注册OptimisticLockerInnerInterceptor插件,更新时MP会生成类似“UPDATE xxx SET name=?, version=version+1 WHERE id=? AND version=?”的SQL。如果更新影响行数为0,说明数据已被别人改过,需要业务层处理冲突。
这三个功能背后都是MP对SQL的“改写”,理解了这一点,排查问题的时候就容易想到:是不是MP把SQL改了,而我没注意到。
3. 实战集成:SpringBoot里把两者玩明白
3.1 依赖引入与配置:别再把两个混在一起
很多新手会同时引入mybatis-spring-boot-starter和mybatis-plus-boot-starter,这其实容易引起类冲突。如果你决定用MP,只需要引入一个:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.x</version> </dependency>MP的starter已经包含了MyBatis的核心依赖,不需要再单独加。还有一个细节:如果项目里用了SpringBoot 3.x,要注意选mybatis-plus-spring-boot3-starter,不同版本的包路径不一样,直接照抄老版本依赖很容易启动报错。
配置文件里常用的是:
mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case设置为true后,数据库下划线字段会自动映射到Java驼峰属性。这个配置在原生MyBatis和MP里都生效,但MP的全局配置里还多了一些db-config字段,所以要注意不要和MyBatis的configuration混为一谈。
3.2 代码生成器:表结构自动生成实体、Mapper、Service
MP的代码生成器是一大亮点。官网文档虽然写得复杂,但其实核心步骤很固定。以3.5.1+版本为例:
FastAutoGenerator.create("jdbc:mysql://localhost:3306/test", "root", "password") .globalConfig(builder -> builder.author("jason").outputDir("src/main/java")) .packageConfig(builder -> builder.parent("com.example").entity("entity").mapper("mapper").service("service").controller("controller")) .strategyConfig(builder -> builder.addInclude("user", "order").addTablePrefix("t_", "sys_")) .execute();这里addInclude指定要生成哪几张表,addTablePrefix指定表前缀,比如sys_user会生成User实体,自动去掉sys_前缀。生成的Entity上会自动加上@TableName、@TableId等注解,Mapper接口继承BaseMapper。用这个工具可以大大减少建实体和Mapper的机械工作。
有一点必须提醒:代码生成器生成的是“初始版本”,后续表结构变更时如果重新生成,会覆盖手工改动。我一般会关掉覆盖选项,或者直接把生成的代码当成模板,重要业务表还是手工调整字段。
3.3 打印SQL的正确姿势:配置项与Log插件
排查问题时最需要看到真实执行的SQL和参数。最简单的方式是开启MyBatis的stdout日志:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会直接打印SQL语句,参数通过问号占位,下面preparing和parameters两行分别展示SQL和参数列表。缺点是日志比较啰嗦,生产环境不建议开。
更优雅的方式是用IDEA插件MyBatis Log Free,它能拦截控制台里的Preparing和Parameters日志,直接帮你还原成一条可执行的完整SQL,方便复制到数据库客户端里调试。这个插件是我平时排查MP生成SQL时的必备工具。
如果你用的是logback或log4j2,那可以直接配置mapper接口包名为TRACE级别日志:
logging: level: com.example.mapper: debug这样也只会打印mapper相关的SQL日志,比全局stdout更可控。
3.4 当表不存在自动建表:这个热点到底靠不靠谱
最近“springboot + mybatis 当表不存在自动建表”这个话题热度不低,很多人想实现启动时检查表是否存在,不存在就自动建表。这里我要泼盆冷水:MyBatis和MP本身都不提供自动建表能力,这个功能要么依赖数据库工具,要么自己写初始化逻辑。
我的做法是在系统启动时,用Spring的ApplicationRunner或CommandLineRunner,调用JdbcTemplate执行一段DDL检查:
@Component public class TableInitRunner implements ApplicationRunner { @Resource private DataSource dataSource; @Override public void run(ApplicationArguments args) { try (Connection conn = dataSource.getConnection()) { DatabaseMetaData meta = conn.getMetaData(); try (ResultSet rs = meta.getTables(null, null, "user", new String[]{"TABLE"})) { if (!rs.next()) { // 执行 CREATE TABLE 语句 } } } } }这种方式适合小型项目或演示环境。生产环境我强烈建议用正式的数据表管理工具,比如Flyway或Liquibase,把表结构变更纳入版本管理。自动建表适合关键业务表缺失时的兜底恢复,但不应该成为常态依赖。
需要注意的是,有些MP相关的工具类或第三方库会宣称支持自动建表,但本质还是根据实体类生成建表SQL。这类方案看起来方便,实际字段类型、索引、分表分区都很难精确控制,不适合复杂业务。简单场景用用可以,核心表千万别图省事。
4. 避坑指南:多模块、Lombok、若依和缓存那些事
4.1 多模块工程下@MapperScan扫描路径怎么配
现在的项目基本都是Maven多模块结构,比如api模块、service模块、mapper模块。MP在多模块下最常遇到的问题就是Mapper接口扫描不到。
一般会在启动类或配置类上配置@MapperScan,指定mapper接口所在包:
@SpringBootApplication @MapperScan("com.example.project.**.mapper") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这里有个坑:如果mapper接口分散在多个模块的多个包下,@MapperScan写成固定一个包路径会漏掉其它模块。解决方法是写多个扫描路径,或者用一个公共的包名统一管理,比如所有模块的mapper都放在com.example.common.mapper底下,扫描就简单了。
还有一种场景是父模块扫描到了子模块里的Class,但子模块的Mapper XML文件没有放到mapper-locations指定的路径下,导致“Invalid bound statement (not found)”错误。这时候要检查target目录下是否有对应的XML文件,如果没打进去,多半是resources插件没把src/main/java下的xml文件打包,需要在pom里额外配置 。
4.2 Lombok和MP实体类的相爱相杀
MP的实体类经常配合Lombok用,比如@Getter @Setter @ToString等。Lombok帮我们生成getter/setter,MP的LambdaQueryWrapper依赖的是实体字段对应的getter方法,所以两者配合起来很流畅。
但有些坑需要注意:
- 实体类上用了@Builder后,lombok会生成一个全参构造器,如果没有别的构造器,MyBatis通过反射实例化对象时会失败,因为找不到默认构造器。解决办法是手动加@NoArgsConstructor和@AllArgsConstructor。
- @Accessors(chain = true)开启链式setter,MP的部分场景可能会有兼容问题,比如在某些序列化场景下出现字段无法赋值。不建议全局开启。
- 使用@TableField(exist = false)标记非数据库字段时,Lombok的@Getter @Setter正常使用没问题,但如果用了@Builder,也留意上面说的默认构造器问题。
我之前遇到过一个诡异问题:实体类引用了Lombok,JSON序列化时少字段,后面排查发现是类上同时用了@TableField(exist = false)和@JsonProperty,但Lombok生成的getter和Jackson的命名映射对不上。这类问题往往不是MP本身的bug,而是多个库之间的注解冲突。
4.3 我在若依框架里用MP的踩坑记录
若依(RuoYi)是国内很多团队使用的后台管理脚手架,早期版本默认用的是MyBatis,后面也有人把它改成MP来用。若依自带了一套BaseController,里面封装了很多分页、导出等通用逻辑,它依赖于原生的PageHelper和自定义SQL。
如果在若依里引入MP,最好注意这几点:
- 若依的BaseEntity里有createBy、createTime、updateTime等公共字段,MP自动填充会和若依的手动set冲突。若依默认是在Service里自己set值,所以MP的自动填充可以不配,否则两者重叠反而会乱。
- 若依的通用查询条件是基于Map的,它会在调用Mapper前判断条件是否为空,然后手动拼SQL。MP的LambdaQueryWrapper在这种架构里用得不多,不要强行把若依原有的Mapper改成BaseMapper,否则业务代码兼容成本太高。
- 若依的一些Controller会继承BaseController,里面用到PageDomain和TableDataInfo,MP的Page对象不能直接转换过去,最好做一个适配方法。
我的建议是:改造若依这种完整脚手架之前,先摸清楚它在哪一层做了数据访问封装。能局部引入MP就局部引入,不要一上来就把Mapper全部替换。很多团队把若依和MP一起用,最后都是“MyBatis写常规SQL + MP做简单表操作”的混合模式,这样兼容性最好。
4.4 MyBatis缓存:一级二级缓存到底开不开
MyBatis的一级缓存默认是开启的,作用范围是SqlSession。在Spring管理下,每次Mapper操作通常都对应一个新的SqlSession,所以一级缓存的生命周期很短,基本没什么感知。二级缓存默认是关闭的,需要全局配置和Mapper XML里的 标签配合才能开启。
在实际项目里,我对缓存的态度是“谨慎”。MP虽然集成了MyBatis,内置的BaseMapper方法也会走MyBatis的缓存流程,但缓存不是白开的:
- 缓存的数据更新时机依赖缓存更新策略,如果不同Mapper写了相同的SQL,很容易出现“数据改了,缓存没清”的问题。
- 分布式环境下,节点之间没有缓存同步机制,需要引入Redis等外部缓存,MyBatis的二级缓存意义就会减弱。
- 如果表数据实时性要求高,比如余额、库存这种,缓存开起来就是给自己挖坑。
所以“MyBatis缓存”这个面试常客,讲解时我会说:一级缓存默认存在但作用有限,二级缓存能不开就不开。业务缓存尽量交给Redis或Caffeine这类外部组件,控制力更好,出问题也更容易排查。
5. 动态SQL与全局配置:面试官常问的底层细节
5.1 if标签语法和where拼接:动态SQL的常见坑
MyBatis最强的地方就是动态SQL, 标签几乎人人会用,但坑也最多。典型场景:多条件查询,参数为空时不拼接条件。
<select id="selectByCondition" resultType="com.example.entity.User"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name = #{name} </if> <if test="age != null"> AND age >= #{age} </if> </where> </select>标签会智能去掉第一个多余的AND或OR,这一点非常实用。如果不用 ,自己写WHERE 1=1来支持动态拼接,也能跑,但SQL不是那么优雅,而且有的DBA不喜欢。
用 时必须注意:
- 字符串判断空,用name != null and name != '',数字类型只要判断null。
- XML里比较特殊字符,比如大于号>要写成>,小于号写成<,否则XML解析会直接报错。
- 如果条件里用了list或array参数,配合 遍历时,collection属性的值是list、array或者自定义参数名,千万别写错。
和MP的LambdaQueryWrapper比,XML动态SQL的优点是灵活,适合多表关联或数据库特有语法;缺点是写起来冗长,不如Wrapper直观。两种方式我都会用:简单单表查询用Wrapper,复杂报表或关联查询用XML。
5.2 mybatis-config.xml中的核心标签与配置顺序
很多人只会在SpringBoot里写配置,很少关心MyBatis的全局配置文件mybatis-config.xml。面试官喜欢问里面有哪些标签,其实只要看着DTD顺序,记住类型就好:
- :读取外部属性文件,用来在配置里占位。
- :全局配置项,比如mapUnderscoreToCamelCase、cacheEnabled、lazyLoadingEnabled。
- :给实体类起别名,这样在XML里写resultType时可以不用全限定类名。
- :类型处理器,负责Java类型和JDBC类型之间的转换。
- :自定义结果对象工厂。
- :拦截器配置,MyBatis插件都挂在这里。
- :环境配置,包括事务管理器和数据源。
- :多数据库厂商支持,让同一套XML根据数据库类型选择不同SQL。
- :扫描Mapper文件或接口。
日常开发中,SpringBoot通过mybatis-plus.configuration.*或mybatis.configuration.*配置项就能覆盖settings里的内容,不一定直接编辑xml。但理解这些标签的含义,有助于排查“为什么我这个配置没生效”这样的问题。
5.3 源码层面:MyBatis的执行流程与MP的增强原理
先串一遍MyBatis的执行流程,从Mapper接口调用到底层SQL执行大概是这样:
- 注入的Mapper其实是一个动态代理对象,调用某个方法时,代理类通过MapperMethod找到对应MapperStatement。
- MapperStatement里保存了SQL的id、SQL来源(XML或注解)、参数映射、结果映射、语句类型等信息。
- 执行器Executor负责执行SQL,它有BaseExecutor和CachingExecutor等实现,二级缓存就是通过装饰器模式套在Executor外面实现的。
- 执行过程中会经过StatementHandler、ParameterHandler、ResultSetHandler一系列处理器,分别负责创建JDBC Statement、设置参数、处理结果集。
MP的源码重点在于它是如何增强MyBatis的。核心是MybatisPlusInterceptor,它实现了MyBatis的Interceptor接口,通过拦截Executor和StatementHandler,在SQL执行前后进行改写。比如分页插件,就是在Executor执行query方法前,先解析参数里的Page对象,然后拼接limit;乐观锁插件,则是在执行update时,把实体里的version字段加到where条件和set子句中。
还有一个地方值得注意:MP在Mapper注册阶段,会通过继承BaseMapper的接口自动注册一批内部方法。这些方法都有对应预先生成的SQL串,比如selectById对应的SQL就是“SELECT ... FROM user WHERE id=?”。如果我们把BaseMapper里的某个方法在XML里重写,XML里的SQL会覆盖默认实现。所以有时候项目里看到自定义XML里的selectById生效,就是这个原因。
理解源码不一定能立刻带来开发效率提升,但排查奇怪问题时会非常有帮助。比如某个查询莫名多出了LIMIT,或者delete语句变成了update,第一时间就会想到是不是MP插件和逻辑删除在起作用。
6. 高频面试题速答:MyBatis和MyBatis-Plus的区别
6.1 面试题合集:这些问题你答得上来吗
面试中关于数据库访问层的题目翻来覆去就那几个方向。整理一份高频清单,供临时抱佛脚:
- MyBatis和MBIbatis-Plus的区别是什么? 答:MyBatis是SQL映射框架,需要手写SQL和XML;MP在它之上封装了通用Mapper、条件构造器、分页插件等,单表CRUD不用写SQL。
- MP为什么能省掉SQL? 答:通过泛型反射拿到实体类上的@TableName、@TableId等注解,动态生成通用SQL;利用MyBatis的Mapper注册机制把这些SQL注册到MapperStatement。
- 实现一个分页插件,思路是什么? 答:实现Interceptor接口,拦截Executor的query方法,在方法执行前修改BoundSql,追加limit参数,再通过反射或重新创建BoundSql执行。
- #{}和${}有什么区别? 答:#{}是预编译参数占位,用PreparedStatement的?占位;${}是字符串拼接,一般用于动态表名、列名,有SQL注入风险,必须谨慎使用。
- MyBatis的一级、二级缓存知道吗? 答:一级缓存是SqlSession级别,默认开启;二级缓存是Mapper级别,跨SqlSession,需要配置 开启,实际项目中要结合分布式缓存场景慎重使用。
- 有没有遇到过Mapper方法无法注入的问题? 答:常见的因为扫描路径不对,或XML没打包进target目录,检查@MapperScan、mapper-locations、pom资源过滤配置。
- 聊聊你用过的MyBatis插件? MP的分页插件、乐观锁插件都有接触,本质上是用Interceptor机制,在Executor层做SQL改写。
碰到这些问题时,不要只背书,可以结合项目里的例子,比如“我在分页插件上踩过数据库方言的坑”这类经验。面试官往往更喜欢听有血有肉的回答。
6.2 我自己的判断:项目里怎么选、怎么答
如果项目允许,我现在的选择是:以MP为默认开发底座,复杂查询用XML原生SQL,简单查询用LambdaQueryWrapper。这样既能享受MP的高效率,又能保留MyBatis的灵活性。
具体落地时我会有几条规矩:
- 禁止在XML里写单表简单查询,统一用Wrapper。
- 多表关联、子查询、批量插入、复杂更新一律走XML。
- XML里所有查询字段都要列出具体列名,禁止SELECT *。
- 逻辑删除字段在自定义SQL里要自己加判断,不能依赖MP拦截。
- Mapper接口只做数据访问,不写业务逻辑,条件构造器的使用范围限定在Service层。
这套规矩在多个项目里验证过,效率和可维护性都不错。如果你还在纠结选MyBatis还是MP,不妨先拿一个中小型模块实验三周,用真实业务验证一下再决定,比自己空想靠谱得多。
结尾:最后分享一点实战体会
我踩过最大的坑是“过度依赖MP的自动化”。有一段时间,看到MP能自动生成SQL,就把所有表操作都丢给它,结果在复杂统计场景里跑出全表扫描,慢查询一堆。后来我才意识到,MP省的是重复劳动,不是思考。简单操作交给它,复杂操作自己掌控SQL,这是最舒服的状态。
另外,如果你的团队正在做技术升级,建议先统一规范再升级框架。比如先约定好多模块扫描路径、实体类字段命名规则、全局配置项,再切到MP,否则上线后一定会遇到一堆“莫名其妙”的问题。最后再分享一个调试技巧:遇到MP生成的SQL和自己预期不一致时,把日志里的preparing和parameters复制到数据库工具里手动执行,排错速度会快很多。