Skip to content
页面导航
精简

一、MyBatis / MyBatis-Plus  是什么?在框架中如何使用?

简单来说,MyBatisMyBatis-Plus 是 Java 后端开发中用来跟数据库打交道的持久层框架(ORM 框架)。它们负责把 Java 代码里的对象,自动转换并存储到数据库中,或者把数据库里的数据查询出来变成 Java 对象。

我们可以用一个通俗的比喻来理解它们的关系:

  • MyBatis 就像是一辆“手动挡的跑车”:性能极高,操控性极强。你想怎么开(写极其复杂的 SQL)都可以,但你需要频繁地踩离合、换挡(手动写大量的 CRUD 基础 SQL 和 XML 配置文件)。
  • MyBatis-Plus(简称 MP)则是这辆跑车的“升级版自动挡/智能驾驶系统”:它只做增强,不做改变。引入它之后,它把所有常用的基础功能(增删改查、分页、条件构造)全部自动化了。你不需要写一行 SQL,就能完成 90% 的常规数据库操作,同时你想写复杂 SQL 时,它依然支持你退回手动挡。

🛠️ 在 Spring Boot 框架中如何使用?(以 MyBatis-Plus 为例)

因为 MyBatis-Plus 包含了 MyBatis 的所有功能,并且极大简化了开发,现在绝大多数新项目都会直接使用 MyBatis-Plus。下面是标准的使用四步法:

第一步:引入依赖 (Maven)

在项目的 pom.xml 中引入 MyBatis-Plus 的 Spring Boot Starter。

xml
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version> <!-- 结合你的 Spring Boot 版本选择 -->
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

第二步:配置数据库连接 (application.yml)

src/main/resources/application.yml 中配置数据库的连接信息:

yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC
    username: root
    password: your_password

# 如果有自定义的复杂SQL,需要配置 XML 的扫描路径
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启控制台打印 SQL 日志,开发时非常有用

第三步:编写实体类 (Entity)

定义一个 Java 类和数据库中的表进行映射。

java
@Data
@TableName("user") // 映射数据库中的 user 表
public class User {
    @TableId(type = IdType.AUTO) // 指定主键,且为自增
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

第四步:编写 Mapper 接口(核心神器)

这就是 MyBatis-Plus 最强大的地方。你只需要定义一个接口,**继承 BaseMapper<T>**,不需要写任何实现代码,也不用写 XML。

java
@Mapper // 告诉 Spring 这是一个 Mapper 接口
public interface UserMapper extends BaseMapper<User> {
    // 继承了 BaseMapper 后,自带有 insert, delete, update, selectById 等几十个方法
    
    // 如果有极其复杂的、MP搞不定的多表联查,再自己定义方法并去写 XML
    List<User> selectComplexUsers(@Param("age") Integer age);
}

🎯 业务中如何调用?

在你的 Service 层或者 Controller 层,直接注入 UserMapper 就可以开始用了:

java
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public void demo() {
        // 1. 插入数据
        User user = new User();
        user.setName("Tom");
        userMapper.insert(user);

        // 2. 基础查询
        User u = userMapper.selectById(1L);

        // 3. 条件构造器查询 (查询年龄大于 18 岁的用户)
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.gt("age", 18);
        List<User> list = userMapper.selectList(wrapper);
    }
}

💡 扩展:面试高频知识点

在面试中,只要你提到了 MyBatis 或 MyBatis-Plus,面试官几乎必问以下几个关于“底层原理”和“高级特性”的问题:

1. MyBatis 的插件机制(拦截器)原理是什么?

面试常问: “MyBatis-Plus 的分页插件很好用,你知道它是怎么实现分页的吗?MyBatis 底层是怎么支持插件的?”

  • 核心原理: MyBatis 允许你在 SQL 执行的生命周期中进行拦截。它通过 JDK 动态代理,在四大核心对象(ExecutorStatementHandlerParameterHandlerResultSetHandler)生成代理对象。
  • 分页插件原理: 当你调用分页查询时,MyBatis-Plus 的分页拦截器(PaginationInnerInterceptor)会拦截到你的拦截对象 StatementHandler。在原本的 SQL 语句(如 SELECT * FROM user)执行前,动态地在末尾拼接上数据库对应的分页关键字(如 MySQL 的 LIMIT ?, ?),并自动帮你先执行一笔 SELECT COUNT(*) 来获取总条数。

2. MyBatis 的一二级缓存机制

面试常问: “请聊聊 MyBatis 的缓存,它们在分布式环境下有什么问题?”

  • 一级缓存(SqlSession 级别): 默认开启。在同一个 SqlSession(一次数据库会话)中,如果执行相同的 SQL 查询,MyBatis 会直接从内存缓存中取数据,不再走数据库。注意: 一旦执行了增删改(commit),一级缓存就会被清空。
  • 二级缓存(Mapper 级别 / Namespace 级别): 默认关闭。多个 SqlSession 可以共享二级缓存。
  • 分布式大坑: 在如今的微服务/分布式架构下,强烈建议不要使用 MyBatis 的二级缓存。因为二级缓存是缓存在单机本地内存中的。如果服务 A 修改了数据库并清空了服务 A 的二级缓存,服务 B 毫不知情,由于服务 B 的本地二级缓存还在,它会继续读取脏数据。分布式环境下,缓存应该交给 Redis 等分布式缓存来做。

3. 什么是持久层的“逻辑删除”?MyBatis-Plus 是如何实现的?

面试常问: “线上生产环境的数据通常不能真正 DELETE,你们是怎么做逻辑删除的?”

  • 概念: 逻辑删除是指不真正从数据库删掉记录,而是通过一个字段(如 deleted,1 表示已删,0 表示未删)来标记。
  • MP 实现:
  1. application.yml 里配置全局的逻辑删除字段和值。
  2. 在实体类的字段上加上 @TableLogic 注解。
  3. 效果:当你调用 userMapper.deleteById(1) 时,MP 底层实际执行的是 UPDATE user SET deleted = 1 WHERE id = 1;而当你调用 select 查询时,MP 会自动在 SQL 末尾加上 WHERE deleted = 0

二、MyBatis-Plus 通常放在哪个目录文件下?

在标准的 Spring Boot 项目结构中,MyBatis / MyBatis-Plus 相关的组件由于职责不同,会分散在几个不同的目录(包)下。它们遵循 MVC(模型-视图-控制器) 架构思想和三层架构设计。

下面是它们通常存放的目录结构及详细说明:

📂 核心目录结构演示

text
src/main/
├── java/com/yourcompany/project/
│   ├── entity/ (或 domain/ pojo/)     👉 1. 存放实体类
│   ├── mapper/ (或 dao/)              👉 2. 存放 Mapper 接口
│   └── service/                       👉 3. 存放 Service 业务层
│       ├── impl/
│       └── ...
└── resources/
    ├── mapper/                        👉 4. 存放 XML 映射文件
    └── application.yml                👉 5. 核心配置文件

🔍 各目录职责详解

1. entity (或 domain, pojo)

  • 存放内容: 对应数据库表结构的 Java Bean 对象。
  • MyBatis-Plus 特色: 在这些类上,你会使用 MyBatis-Plus 的注解。例如,用 @TableName("user") 关联数据库表,用 @TableId 指定主键,用 @TableField 映射普通字段。

2. mapper (或 dao)

  • 存放内容: 操作数据库的 Java 接口(Interface)
  • MyBatis 做法: 只定义方法名,具体的 SQL 写在对应的 XML 文件里。
  • MyBatis-Plus 做法: 让这个接口继承 BaseMapper<Entity>。继承后,你不需要写任何代码,就已经自带了增删改查(CRUD)的几十种常用方法。
  • 注意: 记得在 Spring Boot 启动类上加上 @MapperScan("com.yourcompany.project.mapper"),或者在每个接口上加 @Mapper 注解。

3. service

  • 存放内容: 业务逻辑层。包含 service 接口和 impl 实现类。
  • MyBatis-Plus 特色: 为了进一步简化开发,MyBatis-Plus 提供了通用 Service。你的 Service 接口可以继承 IService<Entity>,实现类可以继承 ServiceImpl<Mapper, Entity>,从而直接获得更高级的批量操作和链式调用能力。

4. resources/mapper/

  • 存放内容: XML 映射文件(如 UserMapper.xml)。
  • 职责: 当你需要编写复杂的 SQL(如多表联查、复杂的动态 SQL)而 MyBatis-Plus 的内置方法无法满足时,就需要在这个目录下写对应的 XML 文件。
  • 注意: XML 文件的目录结构不一定要和 Java 接口完全一致,但要在 application.yml 中通过 mybatis-plus.mapper-locations 配置好告诉框架去哪里加载它们。

💡 扩展:面试高频知识点

在面试中,聊到 MyBatis / MyBatis-Plus 的目录结构和基础使用时,面试官通常会顺藤摸瓜考察你以下几个深层次的问题:

1. MyBatis 的 # {} 和 $ {} 的区别是什么?(必问)

  • #{}(预编译处理): MyBatis 在处理 #{} 时,会将其替换为 ?,并使用 PreparedStatement 来设置参数。这种方式可以防止 SQL 注入,而且效率更高,通常用于传递参数值。
  • ${}(字符串替换): MyBatis 在处理 ${} 时,是直接把参数值拼接到 SQL 语句中。它无法防止 SQL 注入,通常用于动态传入表名、列名或 ORDER BY 后的排序字段。

2. MyBatis-Plus 是如何实现只写接口就能实现 CRUD 的?(原理题)

  • 核心机制: 动态代理注入器(SqlInjector)
  • 当 Spring Boot 启动时,MyBatis-Plus 会去扫描继承了 BaseMapper 的接口。
  • 通过 ISqlInjector(SQL 注入器),它会把 BaseMapper 里预定义的方法(如 insert, selectById 等)翻译成对应的 SQL 语句,并注册到 MyBatis 的 Configuration 容器中。
  • 在运行时,Spring 利用 JDK 动态代理为你的 Mapper 接口生成实现类,当你调用方法时,它就能找到启动时注入的 SQL 并执行。

3. 在大厂或规范的项目中,为什么通常不建议直接把数据库 Entity 返回给前端?

  • 解耦与安全: 数据库 entity 的结构应该和底层表完全一致。如果直接返回给前端,一是可能会暴露敏感字段(如密码、盐值、逻辑删除标记 is_deleted);二是前端需求变化快,一旦前端需要的字段和数据库结构不一致,直接修改 entity 会破坏其单一职责。
  • 解决方案: 引入 DTO(Data Transfer Object,数据传输对象) 用于接收前端参数,和 VO(View Object,显示层对象) 用于返回给前端展示,它们通常也会有独立的目录(如 dto, vo)。</Mapper,>

三、MP如何实现逻辑删除

在实际的企业级开发中,“物理删除”(直接执行 DELETE FROM...)是非常危险的操作。一旦数据被误删,恢复成本极高。因此,大多数互联网公司都强制要求使用逻辑删除——即通过一个状态字段来标识数据是否有效,删除操作本质上是 UPDATE

MyBatis-Plus(简称 MP)对逻辑删除提供了极其优雅的内建支持。下面为你详细拆解它的实现原理具体配置步骤以及特殊场景下的注意事项


🛠️ MyBatis-Plus 逻辑删除的具体实现步骤

在 MyBatis-Plus 中实现逻辑删除非常简单,主要分为全局配置代码标记两步。

第一步:在 application.yml 中进行全局配置

你需要告诉 MyBatis-Plus,哪个值代表“已删除”,哪个值代表“未删除”。

yaml
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体属性名(也可以不配置,而在实体类上单独指定)
      logic-not-delete-value: 0   # 逻辑未删除值(默认就是 0)
      logic-delete-value: 1       # 逻辑已删除值(默认就是 1)

第二步:在实体类(Entity)上添加注解

在对应的实体类属性上加上 @TableLogic 注解。

java
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String name;
    private Integer age;
    
    @TableLogic
    // value = "0" 表示未删除,delval = "1" 表示已删除。
    // 如果你在 yml 里配置了全局变量,这里直接写 @TableLogic 即可,无需指定参数
    private Integer deleted; 
}

🔄 核心效果对比:引入前 vs 引入后

一旦配置了 @TableLogic,MyBatis-Plus 的内置方法(CRUD)就会自动改变它们的底层 SQL 行为:

1. 删除操作(Delete)

  • 你调用的代码: userMapper.deleteById(1L);
  • 没有逻辑删除时(物理删除):
sql
DELETE FROM user WHERE id = 1;
  • 开启逻辑删除后(本质是更新):
sql
    UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0;
    ```

### 2. 查询操作(Select)
*   **你调用的代码:** `List<User> list = userMapper.selectList(null);`
*   **没有逻辑删除时:**
    
```sql
    SELECT id, name, age, deleted FROM user;
  • 开启逻辑删除后(自动拼接过滤条件):
sql
    SELECT id, name, age, deleted FROM user WHERE deleted = 0;

⚠️ 重要提示: 这一套自动转换,只对 MyBatis-Plus 自带的通用 CRUD 方法有效(比如 selectById, selectList, updateById, deleteById)。如果你在 XML 文件里自己写了手写 SQL,MyBatis-Plus 是不会自动帮你拼接 WHERE deleted = 0 的,你必须自己手动写上去。


💡 扩展:面试高频知识点

逻辑删除看似简单,但在高并发、大数据量的真实面试场景中,面试官往往会抛出以下连环炮问题:

1. 逻辑删除会带来什么“唯一索引冲突”问题?如何解决?

面试暗坑: 如果表里有个字段建立了唯一索引(比如 username 或者是 email),用户删除了账号(逻辑删除,deleted 变成 1),然后他用同一个邮箱重新注册,这时候会发生什么?

  • 问题所在: 数据库里会存在一条 email='abc@qq.com', deleted=1 的记录。当新注册的用户再次插入 email='abc@qq.com', deleted=0 时,由于 email 字段是唯一索引,数据库会直接报错 Duplicate key error(违反唯一约束)。
  • 大厂常见解决方案:
    1. 复合唯一索引: 不要单独给 email 加唯一索引,而是建立 emaildeleted复合唯一索引。但这种方案有局限性:如果用户反复删除再注册,deleted 全是 1,依然会冲突。
    2. deleted 字段改为时间戳(推荐): 改变底层设计,未删除时 deleted = 0;删除时,将 deleted 的值更新为当前时间戳(如 1718943201)。建立 email + deleted 的复合唯一索引。因为每次删除的时间戳都不一样,这样就永远不会发生冲突。

2. 已经被逻辑删除的数据,如果我想在后台管理系统里彻底把它物理删除,或者想查出包含已删除的数据,该怎么做?

面试常问: “框架自动帮我加了 WHERE deleted=0,那如果数据管理员想在后台彻底清除这些垃圾数据,或者看历史账单,怎么突破 MP 的限制?”

  • 如何查询已删除的数据:
    • 方案 A: 在 XML 中手写自定义 SQL。因为 MP 不会拦截自定义 XML,你可以写 SELECT * FROM user WHERE id = #{id}(不加 deleted 条件),就能查出来。
    • 方案 B(MP高阶): 使用 userMapper.selectList(new QueryWrapper<User>().eq("deleted", 1)) 显式指定条件。
  • 如何真正物理删除:
    • 方案 A: 同样在 XML 中手写 DELETE FROM user WHERE id = #{id}
    • 方案 B(MP内建): 使用 MyBatis-Plus 的 始终物理删除 自定义注入器。或者直接使用原生的 JdbcTemplate / MyBatis 原生接口绕过 MP 机制。

3. 数据量极大时,逻辑删除对索引和查询性能有什么影响?

面试常问: “随着时间推移,表中被逻辑删除的数据(deleted=1)越来越多,甚至占了总数据的 80%,会对查询性能产生什么影响?该怎么优化?”

  • 性能影响:
    1. 索引块虚胖: 虽然你查的是 deleted=0 的数据,但那些 deleted=1 的无效数据依然占用着 B+ 树索引的物理空间,导致索引树变高,每次查询的磁盘 I/O 次数变多。
    2. 全表扫描变慢: 如果不小心触发了全表扫描,数据库需要把大量已删除的废数据也从磁盘加载到内存中,严重浪费 I/O 和内存。
  • 架构优化方案(冷热数据分离):
    • 不能任由逻辑删除的数据无限留在原表。需要配合定时任务(如 XXL-JOB),定期(比如每个月历史低峰期)将原表中 deleted=1 且删除时间超过 3 个月的数据,归档迁移到专门的历史归档表(冷数据库)中,然后再从原表中物理删除。确保原表(热数据库)始终保持轻量和高性能。

四、MP主要的方法和注解有哪些?

在实际开发和面试中,MyBatis-Plus(简称 MP)的核心内容可以精准地总结为:“四大核心注解”“两大通用接口”“三大条件构造器”


一、 四大核心注解(在 Entity 实体类上使用)

这些注解建立了 Java 对象与数据库表、字段之间的映射规则。

1. @TableName("表名")

  • 作用: 指定实体类对应的数据库表名。
  • 面试加分点: 如果表名都有统一的前缀(如 t_user, t_order),可以在 yml 中配置 mybatis-plus.global-config.db-config.table-prefix: t_,这样类名 User 就会自动映射到 t_user,无需在每个类上写 @TableName

2. @TableId

  • 作用: 声明主键。
  • 核心属性: type(主键策略),面试最常问:
  • IdType.AUTO:数据库自增。
  • IdType.ASSIGN_ID雪花算法(默认,19位分布式唯一ID)。
  • IdType.INPUT:由开发者手动 set 赋值。

3. @TableField

  • 作用: 声明普通字段。
  • 三大高频应用场景:
  • @TableField(exist = false):标记该属性不是数据库表的列(仅业务临时用),让 MP 忽略它。
  • @TableField(value = "db_col_name"):当 Java 属性名(如 userName)与数据库列名(如 user_name)不一致时强行映射(注:默认驼峰法会自动转换,无需写此属性)。
  • @TableField(fill = FieldFill.INSERT_UPDATE)自动填充(如创建时间、更新时间,无需手动 set)。

4. @TableLogic

  • 作用: 标记该字段为逻辑删除标识字段(如 deleted,0有效,1删除)。配置后,delete 变为 UPDATEselect 自动拼接 WHERE deleted = 0

二、 两大通用接口 (在 Mapper 和 Service 层使用)

1. BaseMapper<T>(数据访问层 Mapper 使用)

只要你的接口继承了 BaseMapper<T>,你就立刻拥有了以下这些开箱即用的方法(无需写任何 SQL):

💾 增加 (Insert)

  • int insert(T entity); // 插入一条记录

❌ 删除 (Delete)

  • int deleteById(Serializable id); // 根据 ID 删除(若配置了逻辑删除,则变为更新)
  • int deleteByMap(Map<String, Object> columnMap); // 根据条件删除
  • int delete(Wrapper<T> queryWrapper); // 根据条件构造器删除

📝 修改 (Update)

  • int updateById(T entity); // 根据 ID 修改(注意: 只修改 entity 中不为 null 的字段)
  • int update(T entity, Wrapper<T> updateWrapper); // 根据条件修改

🔍 查询 (Select)

  • T selectById(Serializable id); // 根据 ID 查询
  • List<T> selectBatchIds(Collection<? extends Serializable> idList); // 根据 ID 批量查询(如:IN (1, 2, 3)
  • T selectOne(Wrapper<T> queryWrapper); // 根据条件查询一条记录(如果查出多条会报错)
  • Long selectCount(Wrapper<T> queryWrapper); // 查询符合条件的记录总数
  • List<T> selectList(Wrapper<T> queryWrapper); // 查询符合条件的所有记录
  • IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper); // 分页查询

2. IService<T>ServiceImpl<M, T>(业务逻辑层 Service 使用)

Service 接口继承 IService<T>,实现类继承 ServiceImpl<Mapper, Entity>,直接拥有更高级的批量和链式 CRUD 技能:

  • 批量操作(MP独有优势): saveBatch(entityList)(批量插入)、saveOrUpdateBatch(list)(批量存在则更新,不存在则插入)。
  • 便捷查询: getById(id)list(wrapper)count(wrapper)
  • 链式调用(极爽):
java
// 一行代码完成查询
List<User> list = this.lambdaQuery().gt(User::getAge, 20).like(User::getName, "张").list();

三、 三大条件构造器(动态 SQL 的灵魂)

当你需要编写 WHERE 后面的复杂条件时,MP 提供了 Wrapper 体系。

text
            Wrapper (基类)

         QueryWrapper (传统拼字符串)

     LambdaQueryWrapper (面向对象方法引用,推荐!🌟)

1. LambdaQueryWrapper<T>(最常用,防错防硬编码)

  • 使用示例:
java
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.gt(User::getAge, 18)         // WHERE age > 18
       .like(User::getEmail, "qq")   // AND email LIKE '%qq%'
       .orderByDesc(User::getId);    // ORDER BY id DESC

2. LambdaUpdateWrapper<T>(专门用于复杂修改)

  • 使用场景: 当你想把某个字段强行修改为 null,或者根据复杂条件修改时。
java
    LambdaUpdateWrapper<User> luw = new LambdaUpdateWrapper<>();
    luw.eq(User::getId, 1L).set(User::getEmail, null); // 强行设为 null
    userMapper.update(null, luw);

3. Wrapper 体系中常用的条件方法:

  • eq() (=) 、 ne() (<>) 、 gt() (>) 、 ge() (>=) 、 lt() (<) 、 le() (<=)
  • like() (%值%) 、 likeLeft() (%值) 、 likeRight() (值%)
  • in("column", 1, 2, 3)isNull()isNotNull()
  • and(w -> w.eq(...))(嵌套且)、 or()(或者)

💡 扩展:面试高频知识点

当面试官让你盘点完这些注解和方法后,往往会通过以下三个底层和进阶问题,拉开你与其他候选人的差距:

1. BaseMapper 里的 updateByIdServiceImpl 里的 saveOrUpdate 底层有什么区别?

面试常问: “我看你既调过 updateById,又调过 saveOrUpdate,它们判断数据是否存在、怎么去更新的底层逻辑是一样的吗?”

  • updateById(entity)
    • 底层行为: 直接生成一条 UPDATE ... WHERE id = ? 的 SQL 语句发给数据库。
    • 效率: 高。只跟数据库交互 1 次。
  • saveOrUpdate(entity)
    • 底层行为: 它会先去看你传的实体类里有没有主键 ID。如果没有 ID,直接执行 insert;如果有 ID,它会先执行一次查询 SELECT COUNT(*) FROM table WHERE id = ?。如果查出来结果大于 0,再执行 updateById
    • 效率: 相对较低。在更新时,它需要与数据库交互 2 次(先查后改)。

2. 什么是 MyBatis-Plus 的“悲观锁”与“乐观锁”插件?日常怎么用注解实现?

面试常问: “在高并发修改库存的场景下,如何防止并发带来的‘超卖’问题?MyBatis-Plus 提供了什么支持?”

  • MP 的乐观锁实现: MP 提供了 OptimisticLockerInnerInterceptor(乐观锁插件)。
    1. 在数据库表中增加一个版本号字段 version(默认值 1)。
    2. 在实体类的 Java 属性上加上 @Version 注解。
    3. 运行原理: 当你更新数据时,MP 会自动把 SQL 变成:UPDATE table SET name = '新值', version = version + 1 WHERE id = 1 AND version = 当前版本号。如果这条 SQL 执行返回的受影响行数是 0,说明在你查询之后、修改之前,有别人把数据偷偷改了,此时框架或业务会抛出异常或提示失败,从而保证并发安全。

3. 如果项目需要支持“多租户隔离”(比如不同公司的员工登录,只能看到自己公司的数据),MyBatis-Plus 怎么做?

面试高级题: “你们系统是多租户的,难道每个业务 SQL 后面都要手动拼一句 WHERE tenant_id = 1 吗?有没有无感知的优雅做法?”

  • 正确答案: 绝对不需要手动拼。利用 MyBatis-Plus 的 多租户插件(TenantLineInnerInterceptor
  • 实现原理:
    1. 你只需要实现 MP 提供的 TenantLineHandler 接口,告诉框架你的租户字段名叫什么(如 tenant_id),以及当前登录用户的租户 ID 是多少(通常从拦截器的 ThreadLocal 中获取)。
    2. 在 Spring 中把这个插件配置好。
    3. 效果: 之后你写的所有内置 CRUD 以及你手写的自定义 XML SQL,MP 的底层 AST(抽象语法树)解析器都会在运行时,自动、强行在你的 WHERE 条件后面拼接上 AND tenant_id = 当前租户ID。对上层开发者完全透明,极其优雅。

4. 为什么大厂通常强制要求使用 LambdaQueryWrapper 而不是普通的 QueryWrapper

  • 普通 QueryWrapper 的痛点: 普通写法是 wrapper.eq("user_name", "张三"),这里的 "user_name" 是数据库里的字符串列名(硬编码)。如果以后数据库表结构改了,把 user_name 改成了 username,编译器不会报错,只有在运行到这一行代码时才会崩掉,极难维护。
  • LambdaQueryWrapper 的优势: 写法是 wrapper.eq(User::getUserName, "张三")。它利用了 Java 8 的方法引用。这样一来,如果实体类中的属性名变了,代码在编译期就会直接报错,强迫开发者修改,保证了代码的健壮性。

5. 说说雪花算法(IdType.ASSIGN_ID)的原理?为什么分布式系统不用数据库自增 ID?

面试常问: “我看你项目里主键策略用了 MP 自带的雪花算法,为什么不用 MySQL 自增?”

  • 为什么不用自增: 在单机数据库时,自增 ID(IdType.AUTO)很爽。但在分库分表或分布式微服务架构下,多台数据库各自自增,会导致不同数据库之间的 ID 冲突
  • 雪花算法(Snowflake)原理: MP 默认的 ASSIGN_ID 底层就是雪花算法。它会生成一个 64 位的 Long 型数字,其核心结构为:
  1. 1位符号位:固定为 0,表示正数。
  2. 41位时间戳:精确到毫秒,可以使用 69 年。
  3. 10位工作机器 ID:代表最多支持 1024 个节点/服务,保证不同机器生成的 ID 不冲突。
  4. 12位序列号:同一机器同一毫秒内从 0 开始自增,每毫秒最多生成 4096 个不重复 ID。
  • 优点: 全局唯一、纯数字、整体呈趋势递增(对数据库索引 B+ 树的插入性能非常友好)。

6. updateById 方法,如果我传了一个字段值是空字符串""或者 null,它会更新到数据库吗?怎么让它强制更新?

面试暗坑: “用户在前端把邮箱清空了,点击保存,后端用 updateById(user) 更新,结果发现数据库里的邮箱还是原来的值,并没有变空。这是为什么?怎么解决?”

  • 原因(MP的保护机制): MyBatis-Plus 的策略是 NOT_NULL(非空更新)。当调用 updateById 时,MP 会对实体类字段进行判断,如果字段值为 null,生成的 SQL 语句里就不会拼接该字段。这样做是为了防止你只想改名字,却不小心把用户的密码、邮箱等其他没传的字段给覆盖成 null 了。
  • 解决方案:
  1. 注解控制(临时改变策略): 在实体类的该属性上加上 @TableField(updateStrategy = FieldStrategy.ALWAYS),告诉 MP 无论这个字段是什么(哪怕是 null),都必须生成到 SQL 中去更新。
  2. 使用条件构造器(推荐): 不用 updateById,改用 UpdateWrapper 强制赋值:
java
LambdaUpdateWrapper<User> luw = new LambdaUpdateWrapper<>();
luw.eq(User::getId, 1L).set(User::getEmail, null); // 显式强制设为 null
userMapper.update(null, luw);

五、MyBatis Generator 是什么?

MyBatis Generator (简称 MBG) 是 MyBatis 官方提供的一个极其经典的逆向工程(Reverse Engineering)工具

简单来说,它的核心功能就是:根据数据库里已经建好的表,自动生成 Java 后端开发所需的基础代码。


🛠️ MyBatis Generator 在实际开发中是怎么使用的?

在实际项目开发中,MBG 通常不会单独下载一个软件去运行,而是作为项目构建工具(如 Maven 或 Gradle)的一个插件(Plugin)来集成的。

以下是企业开发中最标准的使用三步法:

第一步:在 pom.xml 中引入 MBG 插件

你需要告诉 Maven,在打包或构建项目时,引入 MyBatis Generator 的插件支持:

xml
<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.4.2</version>
    <configuration>
        <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
        <verbose>true</verbose>
        <overwrite>true</overwrite>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>
</plugin>

第二步:编写核心配置文件 generatorConfig.xml

这是 MBG 的“指挥官”。你需要在 src/main/resources/ 下创建这个文件,在里面配置数据库连接、代码生成路径,以及哪些表需要生成代码

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MySqlContext" targetRuntime="MyBatis3">
        
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/your_database"
                        userId="root"
                        password="your_password">
        </jdbcConnection>

        <javaModelGenerator targetPackage="com.kaiwu.kdp.model.entity" targetProject="src/main/java"/>

        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.kaiwu.kdp.mbg.mapper" targetProject="src/main/java"/>

        <table tableName="system_user" domainObjectName="SystemUser"/>
        <table tableName="t_order" domainObjectName="Order"/>
        
    </context>
</generatorConfiguration>

第三步:运行插件生成代码

配置完成后,在 IDEA 的右侧快捷栏中打开 Maven 面板,找到 Plugins -> mybatis-generator -> 双击 mybatis-generator:generate

随着控制台一阵打印,你会发现项目中瞬间多出了 3 类文件:

  1. SystemUser.java(Entity:里面带有一堆属性和 get/set 方法)。
  2. SystemUserMapper.java(接口:里面有基础的增删改查方法声明)。
  3. SystemUserMapper.xml(XML文件:里面写好了长达几百行的原生标准 SQL 语句)。

🆚 MyBatis Generator 与 MyBatis-Plus 自动生成的区别

很多初学者会产生疑问:前面学了 MyBatis-Plus (MP),现在又看到 MyBatis Generator (MBG),它们到底是什么关系?看懂下面的对比,你的思路就会彻底清晰:

维度MyBatis Generator (传统 MBG)MyBatis-Plus Generator (MP 生成器)
底层理念代码生成依赖:把几百行的原生 SQL 语句直接死生硬地写进你的 XML 文件里框架注入依赖:XML 里空空如也,完全通过 Java 接口继承 BaseMapper 在内存中动态注入
条件查询方式自动生成一个额外的 XxxExample 类。在 Java 里通过 example.createCriteria().andNameEqualTo("张三") 来拼条件。使用框架内建的 QueryWrapperLambdaQueryWrapper 面向对象拼条件。
后期表结构变更如果数据库表加了字段,重新生成时极其容易把开发人员手写的自定义 SQL 给覆盖掉重新生成只会覆盖 Entity 实体类,因为业务方法都在框架底层或独立的 Service 里,对原有代码影响极小。

💡 扩展:面试高频知识点

在面试中,如果面试官看到你的项目里用到了传统的 MBG(比如存在 XxxExample 类),通常会考察你关于工具选型、版本控制以及团队规范的深层问题:

1. 为什么你这个项目使用的是传统的 MyBatis Generator 逆向工程,而不是现在流行的 MyBatis-Plus?它们在选型上怎么考虑?

面试常问: “你觉得 MBG 生成的 Example 机制,相比于 MyBatis-Plus 的 Wrapper 机制,有什么优缺点?”

  • MBG 的优点(纯粹、无侵入、性能透明): 1. MBG 只是一个单纯的“代码外包工”。它打完工、把 XML 和 Java 接口生成出来后就彻底撤退了。你的项目运行时依赖的是最纯粹的 MyBatis 原生框架,没有任何第三方魔改,执行性能最高,也最容易排查慢 SQL(因为 SQL 全都在 XML 里写着)。
  1. 对于有些历史包袱重、不允许引入非官方开源框架的大厂项目,MBG 是唯一的合规选择。
  • MBG 的缺点(臃肿、难维护): 1. 生成的文件体积极大。一张表就会生成几百行甚至上千行的 XML 映射代码,导致项目整体显得很臃肿。
  1. 如果表结构频繁变动,维护成本很高。

2. 在团队协作中,MBG 自动生成的代码需要提交到 Git 仓库吗?如果别人改了数据库,怎么配合?

面试暗坑: “自动生成的代码每天都在变,在团队协作中,应该怎么管理这些 MBG 生成的文件,避免 Git 冲突?”

  • 大厂标准实践(二选一):
  • 方案 A(常规做法 - 独立包管理): 团队规定,自动生成的代码放在一个独立的包下(如你项目里的 com.kaiwu.kdp.mbg 目录)。任何开发人员绝对禁止在这个目录下手动修改或添加任何自定义方法。 如果要写自定义多表联查,必须在外面另外新建一个包(如 com.kaiwu.kdp.custom.mapper),继承原接口去写。这样数据库变动时,直接重新生成覆盖整个 mbg 包,绝对不会发生 Git 代码冲突。
  • 方案 B(高级做法 - 编译期动态生成): 彻底不把自动生成的 Java 文件提交到 Git。在 pom.xml 中把 MBG 插件绑定到 Maven 的 compile(编译)生命周期。Git 仓库里只有 generatorConfig.xml 配置文件。当拉取代码、本地打包或 Jenkins 部署时,构建工具会自动根据配置去连开发环境数据库,动态生成这些代码并放入 target 目录参与编译。

3. MBG 的 insertinsertSelective 到底是怎么通过底层 SQL 实现“选择性”的?

面试常问: “你看了 MBG 生成的 XML 吗?它是用什么标签来实现 Selective 这种非空判断的?”

  • 底层原理: 它是通过 MyBatis 的 <trim> 标签 或者 <set> / <if> 标签 嵌套实现的。
  • insertSelective 为例,MBG 生成的 XML 结构长这样:
xml
<insert id="insertSelective" parameterType="com.kaiwu.kdp.model.entity.SystemUser">
  insert into system_user
  <trim prefix="(" suffix=")" suffixOverrides=",">
    <if test="username != null"> username, </if>
    <if test="password != null"> password, </if>
  </trim>
  <trim prefix="values (" suffix=")" suffixOverrides=",">
    <if test="username != null"> #{username,jdbcType=VARCHAR}, </if>
    <if test="password != null"> #{password,jdbcType=VARCHAR}, </if>
  </trim>
</insert>
  • 拦截机制: 当 Java 传过来的 passwordnull 时,对应的 <if> 条件不成立,MyBatis 就会利用 suffixOverrides="," 动态地把前后多余的逗号切掉。这样,最终发送给 MySQL 的语句里就完全没有 password 这个字段,从而强迫数据库去使用该列的默认值。