- 重复CRUD代码编写
- 分页功能实现复杂
- 缺少通用Service层封装
- 动态表名支持困难
- 多租户方案需自行实现
+ 无侵入:只做增强不做改变
+ 强大的CRUD操作:内置通用Mapper/Service
+ 支持Lambda形式调用
+ 主键自动生成策略
+ 全局拦截器(分页/租户/性能分析)
特性 | MyBatis | MyBatis-Plus | JPA |
---|---|---|---|
CRUD简化 | 手动 | 自动生成 | 自动 |
分页插件 | 需集成 | 内置 | 内置 |
代码生成器 | 无 | 强大 | 中等 |
多租户支持 | 手动 | 注解配置 | 需扩展 |
学习曲线 | 中等 | 平滑 | 陡峭 |
技术栈 | 版本 | 说明 |
---|---|---|
SpringBoot | 3.1.0 | 基础框架 |
Java | 17 | LTS版本 |
MySQL | 8.0 | 数据库 |
MyBatis-Plus | 3.5.3.1 | ORM框架 |
<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>
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 # 未删除值
@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);
}
// 插入
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)); // 条件删除
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)); // 批量删除
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);
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);
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");
@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();
# 全局配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 删除值
logic-not-delete-value: 0 # 未删除值
@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());
}
}
@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;
}
// 枚举定义
@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;
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表
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();
}
}
// 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));
}
// 其他方法...
}
// 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 # 减小缓存范围
// 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> {
}
-- 联合索引示例
CREATE INDEX idx_user_age_role ON sys_user(age, role);
-- 覆盖索引查询
EXPLAIN SELECT id, name FROM sys_user WHERE age BETWEEN 20 AND 30;
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;
}
// 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);
}
}
// 核心流程
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);
}
}
// 核心: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(" "));
}
问题现象:实体类字段与数据库列不匹配
解决方案:
// 1. 明确指定映射
@TableField(value = "db_column")
// 2. 关闭自动驼峰转换
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
// 3. 检查字段类型是否匹配
问题现象:分页查询返回所有结果
排查步骤:
解决方案:
# 1. 确认全局配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
# 2. 实体类添加注解
@TableLogic
private Integer deleted;
典型错误:INSERT语句缺少租户ID
解决方案:
// 在TenantLineHandler中实现租户ID获取
@Override
public Expression getTenantId() {
return new LongValue(SecurityUtils.getTenantId());
}
// 确保INSERT操作包含租户字段
MyBatis-Plus作为MyBatis的增强工具,在企业级应用开发中展现出强大价值。本文涵盖了从基础配置到高级特性的全链路实践,重点包含:
最佳实践建议:
- 复杂查询仍推荐XML方式
- 生产环境关闭SQL日志
- 使用LambdaQueryWrapper避免字段魔法值
- 定期进行SQL性能分析