MyBatisPlus

3 阅读4分钟

MyBatisPlus的核心特性

在传统 MyBatis 中,即便是简单的增删改查(CRUD),你也需要编写 XML 映射文件或大量的 SQL 语句。而在 MyBatis-Plus 中:

  • 无侵入/损耗小:它只是 MyBatis 的插件,不会影响你原有的业务逻辑。
  • 通用 CRUD:内置了通用的 Mapper 和 Service。只需继承 BaseMapper<T>,即可直接调用 insertupdateselectById 等方法,无需编写任何 SQL
  • 条件构造器 (Wrapper) :通过链式调用(如 .eq("name", "Jack").gt("age", 20))自动生成动态 SQL,极大地减少了代码量。
  • 自动分页:内置分页插件,只需简单配置,即可实现物理分页(支持 MySQL, Oracle, PostgreSQL 等主流数据库)。

工作原理

MyBatis能够实现继承BaseMapper<T>就可以完成自动化的核心秘密在于:启动时的SQL注入机制

当你启动一个 Spring Boot 项目时,MyBatis-Plus 会进行一系列“幕后工作”:

  1. 类扫描:MP 会扫描所有继承了 BaseMapper<T> 的接口。

  2. 泛型解析:它会通过反射获取 BaseMapper<V> 中的泛型 V,从而拿到你的数据库实体类(Entity)。

  3. 内建方法映射:MP 内部定义了一套 AbstractMethod 列表(如 Insert, DeleteById, SelectUpdate 等)。

  4. SQL 生成与注入

    • 它会根据实体类上的注解(如 @TableId, @TableField)解析出表名和字段名。
    • 将这些信息填充进预设的 SQL 模板中。
    • 最后,它把生成好的 SQL 语句手动注册到 MyBatis 核心的 MappedStatement 缓存中。

MyBatis-Plus 的核心 SQL 注入是在 Spring 容器启动过程中完成的,而不是在调用时。所以就算有几千张表,也只是会启动较慢。 当你调用 userMapper.selectById(1) 时,其实是 MyBatis 的 JDK 动态代理 在运行。它会去缓存里找 MP 帮你注入好的那段 SQL,然后执行。

JDK动态代理

动态代理就是在程序运行期间,根据需要动态地创建一个“代理对象”,代替“真实对象”去执行任务,并在此过程中偷偷增加一些额外的功能。

为什么需要“动态”代理

在大型软件开发中,我们经常需要给成百上千个方法添加统一的功能,比如:

  • 日志记录(每个接口调用前打印参数)。

  • 事务控制(MyBatis 开启和提交事务)。

  • 权限校验(判断当前用户是否有权访问)。

如果没有动态代理: 你必须在每一个方法里手动写 log.info(...)。如果有 1000 个方法,你就得写 1000 次。 有了动态代理: 你只需要写一个“代理规则”,程序运行的时候,它会自动给这 1000 个方法套上一个“壳”。

Java动态代理的实现方式

A. JDK动态代理(MyBatis核心)

  • 要求:真实对象必须实现了一个接口(比如你的 UserMapper 接口)。

  • 原理:利用反射机制在内存中创建一个实现了相同接口的新类。

  • MyBatis-Plus 的应用:你定义的 UserMapper 只是个接口,并没有实现类。当你调用 selectById 时,其实是 JDK 动态代理生成了一个代理类,拦截了你的调用,并去执行了 MP 注入的 SQL。

B. CGLIB 代理

  • 要求:不需要接口,通过继承真实对象来生成代理类。

  • 原理:底层通过修改字节码(ASM)生成子类。

  • 应用:Spring 在给没有接口的普通的 @Service 类加事务(@Transactional)时,通常使用 CGLIB。

代码实例

假设你有一个接口和一个实现类,你想在不改动代码的情况下统计执行时间

// 1. 拦截器定义规则
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("方法执行前:记录开始时间"); // 增强逻辑
    Object result = method.invoke(target, args);  // 调用真实技能
    System.out.println("方法执行后:记录结束时间"); // 增强逻辑
    return result;
};

// 2. 动态生成代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    handler
);

// 3. 调用时,其实走的是代理
proxy.saveUser();

ServiceImpl

在 MyBatis-Plus 的设计中,ServiceImpl 不是一个接口,而是一个具体的实现类。它极大地简化了业务层的代码开发。

class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>

  • M (Mapper) : 告诉 Service 你的底层数据库操作用哪个 Mapper(即 ArticleMapper)。
  • T (Entity) : 告诉 Service 你的操作对象是哪个实体(即 Article)。

为什么要有ServiceImpl?

维度Mapper 层 (BaseMapper)Service 层 (IService/ServiceImpl)
粒度原子操作:对应单条 SQL(增删改查)。业务操作:可能包含多步逻辑、校验、事务。
功能数量方法较少(约 17 个)。功能极多(包含批量操作、链式调用)。
典型方法insert, selectByIdsaveOrUpdateBatch (批量存或更新), page (分页)。
现实意义负责**“怎么存”**。负责**“存什么、什么时候存、存完干什么”**。

分页

  1. MyBatisPlus开启分页配置:
@Configuration
public class MyBatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

当调用service.page时,这个拦截器会拦截住原本的sql,然后根据传入的分页参数,把sql改写成:

  • SELECT COUNT(*) FROM article(先查总数)。

  • SELECT * FROM article LIMIT 0, 10(再查具体数据)。

最后把改写后的物理分页 SQL 发给数据库。

  1. 请求参数
@Data
public class PageRequestVo {

    private long current = 1;
    private long size = 10;
    private String sortField;
    private boolean isAsc = false;
    private String keyword;
}
  1. 实体类
public IPage<Article> list(PageRequestVo pageRequestVo) {

    Page<Article> page = new Page<>(pageRequestVo.getCurrent(), pageRequestVo.getSize());

    if (StringUtils.hasText(pageRequestVo.getSortField())){
        OrderItem orderItem = pageRequestVo.isAsc() ?
                OrderItem.asc(pageRequestVo.getSortField()) : OrderItem.desc(pageRequestVo.getSortField());
        page.addOrder(orderItem);
    }

    LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
    wrapper.like(StringUtils.hasText(pageRequestVo.getKeyword()), Article::getArticleTitle, pageRequestVo.getKeyword());
    return this.page(page, wrapper);
}