MyBatis-Plus核心内容

发布于:2025-08-17 ⋅ 阅读:(13) ⋅ 点赞:(0)

MyBatis-Plus

MyBatis-Plus 是一个基于 MyBatis的增强工具,旨在简化开发过程,减少重复代码。它在MyBatis的基础上增加了CRUD操作封装,条件构造器、代码生成器等功能。

一、核心特性与优势

1. 核心特性
  • 无侵入:只做增强不做改变,完全兼容MyBatis
  • 自动CRUD:内置通用Mapper,单表操作无需编写sql
  • 条件构造器:通过Lambda表达式构建复杂查询条件
  • 代码生成器:自动生成Entity、Mapper、Service等代码
  • 分页插件:内置分页功能,支持多种数据库。
  • 乐观锁:内置乐观锁插件,防止脏数据更新。
  • 逻辑删除:支持逻辑删除,避免物理删除数据。
  • 多租户
2. 与原生 MyBatis 对比
功能 原生 MyBatis MyBatis-Plus
单表 CRUD 手动编写 XML 或注解 内置通用 Mapper,零 SQL
条件查询 手动拼接 SQL 或 XML Lambda 条件构造器
分页 手动实现分页插件 内置分页插件,一行代码搞定
代码生成 需自定义模板 内置代码生成器
性能分析 需集成第三方插件 内置性能分析插件

二、高级功能

1. 乐观锁插件
// 1. 配置乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁插件
    return interceptor;
}

// 2. 实体类字段上添加 @Version 注解
@Data
public class Product {
    private Long id;
    private String name;
    
    @Version
    private Integer version;
}

// 3. 使用
Product product = productService.getById(1L);
product.setPrice(100);
productService.updateById(product); // 自动带上 version 条件
2. 逻辑删除
// 1. 配置逻辑删除插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new LogicSqlInjector()); // 逻辑删除插件
    return interceptor;
}

// 2. 配置 application.yml
mybatis-plus:
  global-config:
    db-config:
      logic-not-delete-value: 0  # 未删除值(默认)
      logic-delete-value: 1      # 已删除值(默认)

// 3. 实体类字段上添加 @TableLogic 注解
@Data
public class User {
    private Long id;
    private String name;
    
    @TableLogic
    private Integer deleted;
}

// 4. 使用
userService.removeById(1L); // 实际执行 UPDATE user SET deleted=1 WHERE id=1 AND deleted=0
3. 性能分析插件
// 配置性能分析插件(开发环境使用)
@Bean
@Profile({"dev","test"}) // 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() {
    PerformanceInterceptor interceptor = new PerformanceInterceptor();
    interceptor.setMaxTime(100); // 最大执行时间,单位毫秒
    interceptor.setFormat(true); // SQL 是否格式化
    return interceptor;
}

三、整体架构与核心组件

1. 架构分层
┌───────────────────────────────────────────────────────────┐
│                          用户层                            │
│  (Service/Controller 通过 BaseMapper/IService 调用方法)   │
├───────────────────────────────────────────────────────────┤
│                         接口层                             │
│  (BaseMapper<T>, IService<T>, ServiceImpl<M extends      │
│   BaseMapper<T>, T>)                                       │
├───────────────────────────────────────────────────────────┤
│                         核心层                             │
│  (SqlRunner, SqlHelper, MybatisMapperMethod,              │
│   MybatisMapperProxy)                                      │
├───────────────────────────────────────────────────────────┤
│                        插件层                              │
│  (PaginationInnerInterceptor, OptimisticLockerInnerInterceptor,│
│   LogicSqlInjector)                                        │
├───────────────────────────────────────────────────────────┤
│                        配置层                              │
│  (MybatisPlusAutoConfiguration, MybatisPlusProperties)    │
├───────────────────────────────────────────────────────────┤
│                     MyBatis 原生层                          │
│  (SqlSession, Executor, StatementHandler, ResultSetHandler)│
└───────────────────────────────────────────────────────────┘
2. 核心组件
  • BaseMapper:提供基础 CRUD 方法的接口。
  • ServiceImpl:Service 层的默认实现,封装常用业务逻辑。
  • SqlHelper:SQL 执行工具类,与 MyBatis 交互。
  • MybatisMapperMethod:增强版 Mapper 方法调用处理器。
  • MybatisMapperProxy:Mapper 接口的代理实现。
  • InnerInterceptor:插件机制,用于分页、乐观锁等功能。
3.执行流程总结
用户调用 Service 方法
MP 代理对象拦截
构建 QueryWrapper/LambdaQueryWrapper
生成 SQL 条件
调用 MyBatis SqlSession
Executor 执行查询
插件拦截 分页,乐观锁等
StatementHandler 预处理 SQL
ParameterHandler 设置参数
执行 JDBC 查询
ResultSetHandler 映射结果集
返回结果给用户

四、自动配置原理

1. 自动配置入口
// MybatisPlusAutoConfiguration.java
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
    
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 创建 SqlSessionFactory
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        
        // 配置 MyBatis-Plus 专属设置
        Configuration configuration = new MybatisConfiguration();
        factory.setConfiguration(configuration);
        
        // 注册自定义类型处理器
        if (!ObjectUtils.isEmpty(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        
        // 添加插件(分页、乐观锁等)
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors.toArray(new Interceptor[0]));
        }
        
        return factory.getObject();
    }
    
    // 其他 Bean 定义...
}
2. 插件注册流程
// MybatisPlusAutoConfiguration.java
@Bean
@ConditionalOnMissingBean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    return new MybatisPlusInterceptor();
}

// 在需要使用插件的地方注入并配置
@Autowired
private MybatisPlusInterceptor interceptor;

// 添加分页插件
@PostConstruct
public void addPageInterceptor() {
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
}

五、通用 CRUD 实现

1. BaseMapper 接口
public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);
    int deleteById(Serializable id);
    int deleteByMap(@Param("cm") Map<String, Object> columnMap);
    int delete(@Param("ew") Wrapper<T> queryWrapper);
    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
    int updateById(@Param("et") T entity);
    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
    T selectById(Serializable id);
    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
    T selectOne(@Param("ew") Wrapper<T> queryWrapper);
    Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
    Page<T> selectPage(Page<T> page, @Param("ew") Wrapper<T> queryWrapper);
    // 其他方法...
}
2. SQL 注入器(SqlInjector)

MP 通过 SqlInjector 将通用方法对应的 SQL 注入到 MyBatis 中:

// DefaultSqlInjector.java
public class DefaultSqlInjector extends AbstractSqlInjector {
    
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        List<AbstractMethod> methodList = new ArrayList<>();
        
        // 添加基础 CRUD 方法
        methodList.add(new Insert());
        methodList.add(new Delete());
        methodList.add(new DeleteByMap());
        methodList.add(new DeleteById());
        methodList.add(new DeleteBatchIds());
        methodList.add(new Update());
        methodList.add(new UpdateById());
        methodList.add(new SelectById());
        methodList.add(new SelectBatchIds());
        methodList.add(new SelectByMap());
        methodList.add(new SelectOne());
        methodList.add(new SelectCount());
        methodList.add(new SelectList());
        methodList.add(new SelectPage());
        
        return methodList;
    }
}
3. 方法执行流程
// MybatisMapperMethod.java (关键方法)
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    
    // 根据方法类型执行不同逻辑
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            } else {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional() &&
                        (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName() 
                + " attempted to return null from a method with a primitive return type (" 
                + method.getReturnType() + ").");
    }
    return result;
}

六、条件构造器(Wrapper)源码

1. 核心接口与类
// Wrapper 接口定义
public interface Wrapper<T> extends Serializable {
    String getSqlSegment();
    Object getEntity();
    Map<String, Object> getParamNameValuePairs();
    // 其他方法...
}

// AbstractWrapper 实现基本逻辑
public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>>
        implements Wrapper<T> {
    
    protected T entity;
    protected LinkedHashMap<String, Object> paramNameValuePairs = new LinkedHashMap<>();
    protected StringBuilder sqlWhere = new StringBuilder();
    
    // 条件方法实现
    public Children eq(R column, Object val) {
        return addCriterion("=", column, val);
    }
    
    public Children ne(R column, Object val) {
        return addCriterion("<>", column, val);
    }
    
    public Children gt(R column, Object val) {
        return addCriterion(">", column, val);
    }
    
    // 其他条件方法...
}

// QueryWrapper 具体实现
public class QueryWrapper<T> extends AbstractWrapper<T, String, QueryWrapper<T>> {
    // 构造方法和特定方法
}

// LambdaQueryWrapper 使用 Lambda 表达式
public class LambdaQueryWrapper<T> extends AbstractWrapper<T, SFunction<T, ?>, LambdaQueryWrapper<T>> {
    // Lambda 条件方法
    public <V> LambdaQueryWrapper<T> eq(SFunction<T, V> column, V val) {
        return super.eq(getColumn(column), val);
    }
    
    // 其他 Lambda 条件方法...
}
2. Lambda 表达式解析

MP 通过 com.baomidou.mybatisplus.core.toolkit.support.SFunctionLambdaUtils 解析 Lambda 表达式:

// LambdaUtils.java (关键方法)
public static <T> String getColumn(SFunction<T, ?> fn) {
    // 解析 Lambda 表达式获取字段名
    LambdaMeta meta = LambdaUtils.extract(fn);
    String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
    TableInfo tableInfo = TableInfoHelper.getTableInfo(meta.getInstantiatedType());
    
    // 根据字段名获取表列名
    if (tableInfo != null) {
        TableFieldInfo fieldInfo = tableInfo.getFieldList().stream()
                .filter(f -> f.getProperty().equals(fieldName))
                .findFirst().orElse(null);
        if (fieldInfo != null) {
            return fieldInfo.getColumn();
        }
    }
    
    // 如果找不到,使用默认映射规则
    return StringUtils.camelToUnderline(fieldName);
}

七、分页插件实现

1. 核心拦截器
// PaginationInnerInterceptor.java
public class PaginationInnerInterceptor implements InnerInterceptor {
    
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, 
                           ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 获取分页参数
        IPage<?> page = findPage(parameter).orElse(null);
        if (page == null || page.getSize() < 0) {
            return;
        }
        
        // 获取数据库类型
        DbType dbType = this.dbType;
        if (dbType == null) {
            JdbcConnection connection = executor.getTransaction().getConnection();
            dbType = JdbcUtils.getDbType(connection.getMetaData().getURL());
        }
        
        // 根据不同数据库类型生成不同的分页 SQL
        Dialect dialect = DialectFactory.getDialect(dbType);
        String buildSql = concatOrderBy(boundSql.getSql(), page);
        String pageSql = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
        
        // 修改原始 SQL
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        metaObject.setValue("sql", pageSql);
    }
    
    // 其他方法...
}
2. 方言实现
// MySQLDialect.java
public class MySQLDialect implements IDialect {
    
    @Override
    public String buildPaginationSql(String originalSql, long offset, long limit) {
        StringBuilder sql = new StringBuilder(originalSql);
        sql.append(" LIMIT ").append(offset).append(",").append(limit);
        return sql.toString();
    }
}

// OracleDialect.java
public class OracleDialect implements IDialect {
    
    @Override
    public String buildPaginationSql(String originalSql, long offset, long limit) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT * FROM (SELECT TMP.*, ROWNUM RN FROM (");
        sql.append(originalSql);
        sql.append(") TMP WHERE ROWNUM <= ").append(offset + limit);
        sql.append(") WHERE RN > ").append(offset);
        return sql.toString();
    }
}

八、面试题

1. 乐观锁和逻辑删除的实现原理是什么?
  • 乐观锁

    • 原理:通过版本号(@Version 注解)实现,更新时检查版本号是否一致。
    • 流程:查询时获取版本号 → 更新时带上版本号条件 → 成功后版本号 + 1。
  • 逻辑删除

    • 原理:通过 @TableLogic 注解标记字段,将 DELETE 转换为 UPDATE。
    • 配置:设置 logic-not-delete-value=0logic-delete-value=1
2. 如何自定义 MyBatis-Plus 的插件?
  1. 实现 InnerInterceptor 接口。
  2. 重写 beforeQuerybeforeUpdate 等方法,在 SQL 执行前后进行拦截。
  3. 将插件注册到 MybatisPlusInterceptor 中。
3. MyBatis 的核心组件有哪些?各自的职责是什么?
  • SqlSessionFactory:创建 SqlSession 的工厂,通过 SqlSessionFactoryBuilder 构建。
  • SqlSession:数据库操作的核心接口,提供执行 SQL 的方法。
  • Executor:SQL 执行器,负责处理缓存、事务和 SQL 执行。
  • StatementHandler:处理 SQL 语句的预编译和参数设置。
  • ParameterHandler:处理 SQL 参数。
  • ResultSetHandler:处理查询结果集,映射到 Java 对象。
4. MyBatis 中 #{} 和 ${} 的区别是什么?
  • #{}:预编译处理,将参数替换为占位符 ?,调用PreparedStatement 的set 方法来赋值,防止 SQL 注入。

  • ${}:字符串替换,直接将参数插入 SQL,存在 SQL 注入风险。

  • 示例

    // #{} 生成:SELECT * FROM user WHERE name = ?
    @Select("SELECT * FROM user WHERE name = #{name}")
    
    // ${} 生成:SELECT * FROM user WHERE name = '张三'
    @Select("SELECT * FROM user WHERE name = '${name}'")
    
5. MyBatis 的缓存机制是怎样的?
  • 一级缓存

    :基于SqlSessionSqlSession的本地缓存,默认开启。

    • 生命周期:与 SqlSession 一致,Session 关闭时缓存清空。
  • 二级缓存

    :全局缓存,基于namespace隔离。

    • 配置方式:在映射文件中添加 标签或使用 @CacheNamespace 注解。
  • 流程:查询时优先从二级缓存获取 → 再从一级缓存获取 → 最后查询数据库。

6. 如何配置 MyBatis 的二级缓存?

(1) MyBatis 的缓存分为一级缓存和 二级缓存。

一级缓存是 SqlSession 级别的缓存,默认开启。

二级缓存是 NameSpace 级别(Mapper)的缓存,多个 SqlSession 可以共享,使用时需要进行配置开启。

(2) 缓存的查找顺序:二级缓存 => 一级缓存 => 数据库

  1. mybatis-config.xml中启用二级缓存:

    <settings>
      <setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 在映射文件中配置缓存:

    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
    
  3. 实体类实现 Serializable 接口。

7. MyBatis 的插件机制是如何工作的?

参考答案

  • 基于 责任链模式动态代理
  • 插件可以拦截 ExecutorStatementHandlerParameterHandlerResultSetHandler 的方法。
  • 自定义插件需实现 Interceptor 接口,使用 @Intercepts@Signature 注解指定拦截点。
8.MyBatis如何获取自动生成的(主)键值

在标签中使用 useGeneratedKeys 和 keyProperty 两个属性来获取自动生成的主键值。

<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
insert into names (name) values (#{name})
</insert>
9.简述Mybatis的动态SQL,列出常用的6个标签及作用
  • Mybatis 动态sql可以让我们在xml映射文件中,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。

  • Mybatis 提供了9中动态sql标签:

    : 进行条件的判断

    :在判断后的 SQL 语句前面添加 WHERE 关键字,并处理 SQL 语句开始位置的 AND 或者 OR 的问题

    :可以在 SQL 语句前后进行添加指定字符 或者去掉指定字符.

    : 主要用于修改操作时出现的逗号问题

    :类似于 java 中的 switch 语句.在所有的条件中选择其一

    :迭代操作

  • 其执行原理:使用OGNL 从sql参数对象中计算表达式的值,更具表达式的值动态拼接sql,以此来完成动态sql功能

10.Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?

不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置namespace,那么 id 不能重复;毕竟 namespace 不是必须的,只是最佳实践而已。原因就是 namespace+id 是作为 Map<String, MappedStatement>的 key 使用的,如果没有namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。

11.Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么?

Mybatis 有三种基本的 Executor 执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

1)SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。

2)ReuseExecutor:执行 update 或 select,以 sql 作为
key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map

3)BatchExecutor:完成批处理。

12.Mybatis 中如何指定使用哪一种 Executor 执行器?

在 Mybatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数。


网站公告

今日签到

点亮在社区的每一天
去签到