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><!-- SpringBoot基础 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis-Plus核心 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><!-- 数据库驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Lombok简化开发 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>
2.3 配置文件
spring:datasource:url: jdbc:mysql://localhost:3306/mp_demo?useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456driver-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) // 雪花算法IDprivate 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("new@example.com");
userMapper.updateById(updateUser);// 条件更新
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.set("email", "admin@example.com").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 {@Overridepublic 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 {/*** 分页插件 + 性能分析插件*/@Beanpublic 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 {@Overridepublic void insertFill(MetaObject metaObject) {this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());}@Overridepublic 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() {@Overridepublic Expression getTenantId() {// 从安全上下文获取租户IDreturn new LongValue(SecurityUtils.getTenantId());}@Overridepublic String getTenantIdColumn() {return "tenant_id";}@Overridepublic 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);}@Overridepublic 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); // 使用Lombokstrategy.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 {@Overridepublic 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();// 反射修改BoundSQLField 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();}@Beanpublic 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 {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass) {List<AbstractMethod> methods = super.getMethodList(mapperClass);methods.add(new FindAll()); // 添加自定义方法return methods;}
}// 自定义方法实现
public class FindAll extends AbstractMethod {@Overridepublic 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 分页失效问题
问题现象:分页查询返回所有结果
排查步骤:
- 检查是否配置分页插件
- 确认Page对象作为第一个参数
- 验证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-Plus作为MyBatis的增强工具,在企业级应用开发中展现出强大价值。本文涵盖了从基础配置到高级特性的全链路实践,重点包含:
- 核心功能深度解析:条件构造器、分页插件、代码生成器
- 企业级方案:多租户、数据权限、动态数据源
- 性能优化:批处理、流式查询、索引优化
- 源码级原理:SQL注入器、条件构造器实现
- 生产环境问题排查
最佳实践建议:
- 复杂查询仍推荐XML方式
- 生产环境关闭SQL日志
- 使用LambdaQueryWrapper避免字段魔法值
- 定期进行SQL性能分析