MyBatis-Plus深度全解:从入门到企业级实战

MyBatis-Plus深度全解:从入门到企业级实战

一、为什么选择MyBatis-Plus?

1.1 MyBatis的痛点

- 重复CRUD代码编写
- 分页功能实现复杂
- 缺少通用Service层封装
- 动态表名支持困难
- 多租户方案需自行实现

1.2 MyBatis-Plus核心优势

+ 无侵入:只做增强不做改变
+ 强大的CRUD操作:内置通用Mapper/Service
+ 支持Lambda形式调用
+ 主键自动生成策略
+ 全局拦截器(分页/租户/性能分析)

1.3 技术栈对比

特性 MyBatis MyBatis-Plus JPA
CRUD简化 手动 自动生成 自动
分页插件 需集成 内置 内置
代码生成器 强大 中等
多租户支持 手动 注解配置 需扩展
学习曲线 中等 平滑 陡峭

二、SpringBoot 3.x整合实战

2.1 环境准备

技术栈 版本 说明
SpringBoot 3.1.0 基础框架
Java 17 LTS版本
MySQL 8.0 数据库
MyBatis-Plus 3.5.3.1 ORM框架

2.2 依赖引入

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.5.3.1version>
    dependency>
    
    
    <dependency>
        <groupId>com.mysqlgroupId>
        <artifactId>mysql-connector-jartifactId>
        <scope>runtimescope>
    dependency>
    
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
dependencies>

2.3 配置文件

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mp_demo?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis-Plus配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志
    map-underscore-to-camel-case: true # 下划线转驼峰
  global-config:
    db-config:
      id-type: assign_id # 雪花算法ID生成
      logic-delete-field: deleted # 逻辑删除字段
      logic-delete-value: 1 # 已删除值
      logic-not-delete-value: 0 # 未删除值

2.4 实体类与Mapper

@Data
@TableName("sys_user") // 表名映射
public class User {
    @TableId(type = IdType.ASSIGN_ID) // 雪花算法ID
    private Long id;
    
    @TableField("username") // 字段映射
    private String name;
    
    private Integer age;
    private String email;
    
    @TableField(fill = FieldFill.INSERT) // 自动填充
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

// Mapper接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 自定义方法
    @Select("SELECT * FROM sys_user WHERE age > #{age}")
    List<User> selectUsersByAge(@Param("age") Integer age);
}

三、核心功能深度解析

3.1 CRUD接口详解

3.1.1 Mapper层CRUD
// 插入
User user = new User();
user.setName("John");
user.setAge(30);
userMapper.insert(user); 

// 批量插入
List<User> users = Arrays.asList(new User(...), new User(...));
userMapper.insertBatchSomeColumn(users); // 批处理优化

// 更新
User updateUser = new User();
updateUser.setId(1L);
updateUser.setEmail("[email protected]");
userMapper.updateById(updateUser);

// 条件更新
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.set("email", "[email protected]")
       .eq("name", "admin");
userMapper.update(null, wrapper);

// 删除
userMapper.deleteById(1L); // ID删除
userMapper.delete(new QueryWrapper<User>().eq("age", 18)); // 条件删除
3.1.2 Service层CRUD
public interface UserService extends IService<User> {
    // 自定义业务方法
    List<User> findAdmins();
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    implements UserService {
    
    @Override
    public List<User> findAdmins() {
        LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(User::getRole, "admin");
        return baseMapper.selectList(wrapper);
    }
}

// 使用示例
userService.save(user); // 保存
userService.updateById(user); // 更新
userService.removeByIds(Arrays.asList(1L,2L)); // 批量删除

3.2 条件构造器(Wrapper)

3.2.1 QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "name", "age") // 指定字段
       .like("name", "张")          // 模糊查询
       .between("age", 20, 30)     // 范围查询
       .isNotNull("email")          // 非空判断
       .orderByDesc("create_time"); // 排序
List<User> users = userMapper.selectList(wrapper);
3.2.2 LambdaQueryWrapper(推荐)
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.select(User::getId, User::getName, User::getAge)
             .like(User::getName, "张")
             .ge(User::getAge, 18)
             .orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(lambdaWrapper);
3.2.3 复杂条件示例
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.and(wq -> wq.gt(User::getAge, 18).or().isNull(User::getEmail))
       .nested(nq -> nq.eq(User::getRole, "admin").lt(User::getCreateTime, LocalDateTime.now()))
       .apply("date_format(create_time,'%Y-%m-%d') = {0}", "2023-07-01");

3.3 分页插件(企业级优化)

@Configuration
public class MybatisPlusConfig {
    
    /**
     * 分页插件 + 性能分析插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 分页插件(MySQL方言)
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInterceptor.setMaxLimit(1000L); // 单页最大1000条
        paginationInterceptor.setOverflow(true);   // 超过总页数返回第一页
        
        interceptor.addInnerInterceptor(paginationInterceptor);
        
        // 性能分析插件(仅开发环境)
        if (log.isDebugEnabled()) {
            PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
            performanceInterceptor.setFormat(true);
            performanceInterceptor.setMaxTime(2000); // SQL执行超过2秒记录警告
            interceptor.addInnerInterceptor(performanceInterceptor);
        }
        
        return interceptor;
    }
}

// 使用示例
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getRole, "admin");
Page<User> result = userMapper.selectPage(page, wrapper);

// 结果处理
List<User> records = result.getRecords();
long total = result.getTotal();

四、高级特性实战

4.1 逻辑删除

# 全局配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 逻辑删除字段名
      logic-delete-value: 1        # 删除值
      logic-not-delete-value: 0    # 未删除值

4.2 自动填充

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

4.3 多租户方案

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    
    // 多租户插件
    TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
    tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
        @Override
        public Expression getTenantId() {
            // 从安全上下文获取租户ID
            return new LongValue(SecurityUtils.getTenantId());
        }
        
        @Override
        public String getTenantIdColumn() {
            return "tenant_id";
        }
        
        @Override
        public boolean ignoreTable(String tableName) {
            // 忽略系统表
            return "sys_config".equals(tableName);
        }
    });
    
    interceptor.addInnerInterceptor(tenantInterceptor);
    return interceptor;
}

4.4 枚举处理

// 枚举定义
@Getter
public enum UserStatus {
    ENABLED(1, "启用"),
    DISABLED(0, "禁用");
    
    @EnumValue // 标记数据库存储值
    private final int code;
    private final String desc;
    
    UserStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

// 实体类字段
private UserStatus status;

4.5 动态表名

public class DynamicTableNameParser implements TableNameHandler {
    
    private ThreadLocal<String> tableName = new ThreadLocal<>();
    
    public void setTableName(String tableName) {
        this.tableName.set(tableName);
    }
    
    @Override
    public String dynamicTableName(String sql, String tableName) {
        return this.tableName.get() != null ? this.tableName.get() : tableName;
    }
}

// 使用示例
DynamicTableNameParser parser = new DynamicTableNameParser();
parser.setTableName("user_2023"); // 设置动态表名

TableNameHelper.setTableNameParser(parser);
List<User> users = userMapper.selectList(null); // 操作user_2023表

五、代码生成器(企业级配置)

5.1 生成器配置

public class CodeGenerator {
    
    public static void main(String[] args) {
        AutoGenerator generator = new AutoGenerator();
        
        // 全局配置
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setOutputDir(System.getProperty("user.dir") + "/src/main/java");
        globalConfig.setAuthor("YourName");
        globalConfig.setOpen(false);
        globalConfig.setSwagger2(true); // 开启Swagger注解
        generator.setGlobalConfig(globalConfig);
        
        // 数据源配置
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mp_demo");
        dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
        dataSourceConfig.setUsername("root");
        dataSourceConfig.setPassword("123456");
        generator.setDataSource(dataSourceConfig);
        
        // 包配置
        PackageConfig packageConfig = new PackageConfig();
        packageConfig.setParent("com.example.mp");
        packageConfig.setEntity("entity");
        packageConfig.setMapper("mapper");
        packageConfig.setService("service");
        packageConfig.setController("controller");
        generator.setPackageInfo(packageConfig);
        
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true); // 使用Lombok
        strategy.setRestControllerStyle(true); // RESTful风格
        strategy.setInclude("sys_user", "sys_role"); // 生成表
        
        // 自定义模板
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setController("templates/controller.java");
        templateConfig.setService("templates/service.java");
        templateConfig.setServiceImpl("templates/serviceImpl.java");
        generator.setTemplate(templateConfig);
        
        generator.execute();
    }
}

5.2 自定义模板示例

// templates/controller.java.vm
package ${package.Controller};

@RestController
@RequestMapping("/${table.entityPath}")
@RequiredArgsConstructor
public class ${table.controllerName} {

    private final ${table.serviceName} ${table.entityPath}Service;

    @GetMapping("/{id}")
    public Result<${entity}> getById(@PathVariable ${table.keyType} id) {
        return Result.success(${table.entityPath}Service.getById(id));
    }
    
    // 其他方法...
}

六、性能优化指南

6.1 SQL执行效率优化

// 1. 启用批处理
@Bean
public ConfigurationCustomizer configurationCustomizer() {
    return configuration -> {
        configuration.setDefaultExecutorType(ExecutorType.BATCH); // 批处理模式
    };
}

// 2. 使用流式查询
try (Cursor<User> cursor = userMapper.selectCursor(queryWrapper)) {
    cursor.forEach(user -> process(user));
}

// 3. 禁用XML热加载(生产环境)
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl # 关闭日志
    local-cache-scope: statement # 减小缓存范围

6.2 查询优化技巧

// 1. 只查询必要字段
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.select(User::getId, User::getName);

// 2. 避免N+1查询(关联查询优化)
@Select("SELECT u.*, d.name AS dept_name FROM user u LEFT JOIN dept d ON u.dept_id = d.id")
List<UserVO> selectUserWithDept();

// 3. 使用二级缓存
@CacheNamespace(implementation = MybatisRedisCache.class, eviction = MybatisRedisCache.class)
public interface UserMapper extends BaseMapper<User> {
}

6.3 索引优化建议

-- 联合索引示例
CREATE INDEX idx_user_age_role ON sys_user(age, role);

-- 覆盖索引查询
EXPLAIN SELECT id, name FROM sys_user WHERE age BETWEEN 20 AND 30;

七、企业级实战案例

7.1 数据权限控制

public class DataPermissionInterceptor implements InnerInterceptor {
    
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, 
                            Object parameter, RowBounds rowBounds, 
                            ResultHandler resultHandler, BoundSql boundSql) {
        
        // 获取当前用户数据权限
        DataScope dataScope = SecurityUtils.getDataScope();
        
        if (dataScope != null) {
            // 修改SQL添加权限过滤
            String sql = boundSql.getSql();
            String newSql = sql + " AND " + dataScope.getSqlSegment();
            
            // 反射修改BoundSQL
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, newSql);
        }
    }
}

// 注册拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new DataPermissionInterceptor());
    return interceptor;
}

7.2 多数据源动态切换

// 1. 配置多数据源
@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource dynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource());
        dataSourceMap.put("slave", slaveDataSource());
        
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setDefaultTargetDataSource(masterDataSource());
        dataSource.setTargetDataSources(dataSourceMap);
        return dataSource;
    }
}

// 2. 数据源切换注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DS {
    String value() default "master";
}

// 3. 切面实现
@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(ds)")
    public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
        String dsKey = ds.value();
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return point.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

// 4. 使用示例
@Service
public class UserService {
    
    @DS("slave") // 从库查询
    public User getById(Long id) {
        return userMapper.selectById(id);
    }
    
    @DS("master") // 主库写入
    public void saveUser(User user) {
        userMapper.insert(user);
    }
}

八、源码解析(核心设计思想)

8.1 SQL注入器原理

// 核心流程
AbstractSqlInjector#inspectInject() 
  -> 解析Mapper接口方法
  -> 根据方法名匹配内置方法(selectById等)
  -> 构造对应的SqlMethod
  -> 创建MappedStatement

// 自定义注入示例
public class MySqlInjector extends DefaultSqlInjector {
    
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methods = super.getMethodList(mapperClass);
        methods.add(new FindAll()); // 添加自定义方法
        return methods;
    }
}

// 自定义方法实现
public class FindAll extends AbstractMethod {
    
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, 
                                                Class<?> modelClass, 
                                                TableInfo tableInfo) {
        String sql = "SELECT * FROM %s";
        String formattedSql = String.format(sql, tableInfo.getTableName());
        SqlSource sqlSource = languageDriver.createSqlSource(
                configuration, formattedSql, modelClass);
        return this.addSelectMappedStatementForTable(
                mapperClass, "findAll", sqlSource, tableInfo);
    }
}

8.2 条件构造器原理

// 核心:AbstractWrapper
public abstract class AbstractWrapper<T, R, Children> 
    implements Compare<Children, R>, Nested<Children, Children> {
    
    // 存储条件表达式
    protected List<SqlSegment> expression = new ArrayList<>();
    
    // 条件构建示例
    public Children eq(boolean condition, R column, Object val) {
        if (condition) {
            expression.add(new SimpleSqlSegment(
                () -> columnToString(column), 
                () -> " = ", 
                () -> formatSqlValue(val)
            ));
        }
        return typedThis;
    }
}

// SQL片段生成
public String getSqlSegment() {
    return expression.stream()
        .map(SqlSegment::getSqlSegment)
        .filter(Objects::nonNull)
        .collect(Collectors.joining(" "));
}

九、常见问题排查

9.1 字段映射问题

问题现象:实体类字段与数据库列不匹配
解决方案

// 1. 明确指定映射
@TableField(value = "db_column")

// 2. 关闭自动驼峰转换
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false

// 3. 检查字段类型是否匹配

9.2 分页失效问题

问题现象:分页查询返回所有结果
排查步骤

  1. 检查是否配置分页插件
  2. 确认Page对象作为第一个参数
  3. 验证SQL是否支持分页(无order by可能导致分页异常)

9.3 逻辑删除不生效

解决方案

# 1. 确认全局配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted
      
# 2. 实体类添加注解
@TableLogic
private Integer deleted;

9.4 多租户SQL异常

典型错误:INSERT语句缺少租户ID
解决方案

// 在TenantLineHandler中实现租户ID获取
@Override
public Expression getTenantId() {
    return new LongValue(SecurityUtils.getTenantId());
}

// 确保INSERT操作包含租户字段

十、未来展望(MyBatis-Plus 4.0)

10.1 新特性预览

  • 响应式编程支持:整合Project Reactor
  • GraalVM原生镜像:提升启动速度
  • 增强的多租户方案:支持更复杂的租户隔离
  • 分布式ID生成器:内置更多分布式ID方案

10.2 架构演进

MyBatis Core
MyBatis-Plus 3.x
扩展接口
SQL注入器
元数据处理
拦截器链
4.0插件体系

结语

MyBatis-Plus作为MyBatis的增强工具,在企业级应用开发中展现出强大价值。本文涵盖了从基础配置到高级特性的全链路实践,重点包含:

  1. 核心功能深度解析:条件构造器、分页插件、代码生成器
  2. 企业级方案:多租户、数据权限、动态数据源
  3. 性能优化:批处理、流式查询、索引优化
  4. 源码级原理:SQL注入器、条件构造器实现
  5. 生产环境问题排查

最佳实践建议

  • 复杂查询仍推荐XML方式
  • 生产环境关闭SQL日志
  • 使用LambdaQueryWrapper避免字段魔法值
  • 定期进行SQL性能分析

你可能感兴趣的:(mybatis)