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性能分析