企业级MyBatis-Plus实战教学(详细版)
模块一:环境搭建与配置最佳实践
1.1 详细依赖配置
<!-- pom.xml 详细配置 -->
<dependencies>
<!-- Spring Boot Web Starter - 提供Web开发能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<!-- MyBatis-Plus Starter - 核心依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
<!--
重点说明:
1. mybatis-plus-boot-starter 已经包含了mybatis-spring-boot-starter
2. 无需单独引入MyBatis依赖,避免版本冲突
3. 企业项目建议锁定版本号,避免自动升级导致兼容性问题
-->
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<!--
重点说明:
1. MySQL 8.0+ 需要使用这个版本的驱动
2. 如果使用MySQL 5.x,版本号应改为 5.1.47
3. 注意时区配置,生产环境要设置 serverTimezone
-->
</dependency>
<!-- Lombok - 减少样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
<!--
重点说明:
1. scope=provided 表示编译时需要,运行时不需要
2. 需要在IDE中安装Lombok插件
3. @Data 注解会自动生成getter/setter/toString等方法
-->
</dependency>
<!-- HikariCP 连接池(可选,Spring Boot 2.0+默认使用) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
</dependencies>
1.2 配置文件详细解析
# application.yml 完整配置详解
spring:
datasource:
# 数据库连接URL - 企业级配置要点
url: jdbc:mysql://localhost:3306/enterprise_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
#
# 参数详细说明:
# useUnicode=true&characterEncoding=UTF-8 - 保证中文不乱码
# serverTimezone=Asia/Shanghai - 解决时区问题,避免时间差8小时
# useSSL=false - 开发环境关闭SSL,生产环境应该为true
# allowPublicKeyRetrieval=true - MySQL 8.0连接需要
username: root
password: 123456
# Hikari连接池配置 - 企业性能优化关键
hikari:
# 连接池名称
pool-name: EnterpriseHikariCP
# 最小空闲连接数
minimum-idle: 5
# 最大连接数(根据业务量调整)
maximum-pool-size: 20
# 连接超时时间(毫秒)
connection-timeout: 30000
# 连接最大生命周期
max-lifetime: 1800000
# 空闲连接超时时间
idle-timeout: 600000
# 连接测试查询
connection-test-query: SELECT 1
# MyBatis-Plus 配置详解
mybatis-plus:
# 全局配置
global-config:
db-config:
# 主键类型:
# AUTO-数据库自增, INPUT-手动输入, ASSIGN_ID-雪花算法, ASSIGN_UUID-UUID
id-type: ASSIGN_ID
#
# 重点说明:
# ASSIGN_ID 使用雪花算法生成19位Long类型ID,适合分布式系统
# 如果使用MySQL自增,需要设置为 AUTO,并在数据库字段设置 AUTO_INCREMENT
# 逻辑删除配置
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
#
# 重点说明:
# 逻辑删除不是真正删除数据,而是更新deleted字段
# 查询时会自动加上 WHERE deleted=0 条件
# MyBatis原生配置
configuration:
# 自动驼峰命名规则映射
map-underscore-to-camel-case: true
#
# 重点说明:
# 开启后,数据库字段 user_name 会自动映射到Java属性 userName
# 这是企业开发的标准规范
# 日志实现 - 不同环境的配置
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#
# 日志配置说明:
# 开发环境:StdOutImpl 控制台打印SQL
# 测试环境:Slf4jImpl 输出到日志文件
# 生产环境:NoLoggingImpl 关闭日志,提高性能
# 开启二级缓存
cache-enabled: true
# 配置默认的执行器
default-executor-type: REUSE
#
# 执行器类型说明:
# SIMPLE - 简单执行器,每次执行都创建新的Statement
# REUSE - 复用执行器,复用PreparedStatement(推荐)
# BATCH - 批量执行器,用于批量操作
# Mapper文件位置
mapper-locations: classpath:/mapper/*.xml
# 类型别名包扫描
type-aliases-package: com.enterprise.entity
1.3 配置类代码逐行解析
package com.enterprise.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 配置类
* 重点说明:@Configuration 注解表示这是一个配置类,Spring Boot启动时会自动加载
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus 拦截器配置
* 这是核心配置,所有的插件都在这里添加
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建拦截器实例
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 分页插件 - 必须添加到第一个位置
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型
paginationInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认500条,-1表示不受限制
paginationInterceptor.setMaxLimit(1000L);
// 开启count的join优化,只针对部分left join
paginationInterceptor.setOptimizeJoin(true);
// 添加分页插件到拦截器
interceptor.addInnerInterceptor(paginationInterceptor);
// 2. 乐观锁插件
OptimisticLockerInnerInterceptor optimisticLockerInterceptor =
new OptimisticLockerInnerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInterceptor);
return interceptor;
}
/**
* 元对象字段填充控制器
* 用于自动填充创建时间、更新时间等字段
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
/**
* 插入时的填充策略
*/
@Override
public void insertFill(MetaObject metaObject) {
// 设置创建时间 - 如果字段为空则填充
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 设置更新时间 - 新增时也设置
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 设置创建人 - 实际项目中可以从SecurityContext中获取
this.strictInsertFill(metaObject, "createBy", String.class, "system");
}
/**
* 更新时的填充策略
*/
@Override
public void updateFill(MetaObject metaObject) {
// 设置更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 设置更新人
this.strictUpdateFill(metaObject, "updateBy", String.class, "system");
}
};
}
}
1.4 启动类详细说明
package com.enterprise;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*
* 重点注解说明:
* @SpringBootApplication - 包含以下三个注解:
* @SpringBootConfiguration - 标识这是配置类
* @EnableAutoConfiguration - 开启自动配置
* @ComponentScan - 组件扫描,默认扫描当前包及其子包
*
* @MapperScan - 指定Mapper接口的扫描路径
*/
@SpringBootApplication
@MapperScan("com.enterprise.mapper") // 重点:必须指定Mapper接口所在包
public class EnterpriseApplication {
public static void main(String[] args) {
// 启动Spring Boot应用
SpringApplication.run(EnterpriseApplication.class, args);
System.out.println("企业级MyBatis-Plus应用启动成功!");
System.out.println("Swagger地址: http://localhost:8080/swagger-ui.html");
System.out.println("Druid监控: http://localhost:8080/druid");
}
}
1.5 测试验证配置
package com.enterprise.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 环境配置测试类
* 重点:确保所有配置正确生效
*/
@SpringBootTest
public class EnvironmentTest {
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 测试数据库连接是否正常
*/
@Test
public void testDatabaseConnection() {
try (Connection connection = dataSource.getConnection()) {
System.out.println("数据库连接成功!");
System.out.println("数据库URL: " + connection.getMetaData().getURL());
System.out.println("数据库产品: " + connection.getMetaData().getDatabaseProductName());
System.out.println("驱动版本: " + connection.getMetaData().getDriverVersion());
} catch (SQLException e) {
System.err.println("数据库连接失败: " + e.getMessage());
}
}
/**
* 测试MyBatis-Plus配置是否生效
*/
@Test
public void testMybatisPlusConfig() {
try {
// 执行简单查询测试配置
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
System.out.println("MyBatis-Plus基础配置测试通过,查询结果: " + result);
} catch (Exception e) {
System.err.println("MyBatis-Plus配置测试失败: " + e.getMessage());
}
}
}
1.6 企业级避坑方案详解
坑点1:时区问题
# 错误配置 - 可能导致时间差8小时
url: jdbc:mysql://localhost:3306/test
# 正确配置 - 明确指定时区
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
坑点2:SSL连接问题
# MySQL 8.0 需要明确关闭SSL(开发环境)
url: jdbc:mysql://localhost:3306/test?useSSL=false&allowPublicKeyRetrieval=true
# 生产环境应该使用SSL
url: jdbc:mysql://localhost:3306/test?useSSL=true&verifyServerCertificate=false
坑点3:连接池配置
spring:
datasource:
hikari:
# 避免连接泄漏的配置
leak-detection-threshold: 60000 # 60秒后检测连接泄漏
# 连接存活检查
keepalive-time: 30000 # 30秒发送一次keepalive查询
坑点4:MyBatis-Plus版本兼容性
<!-- 确保版本兼容 -->
<properties>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<spring-boot.version>2.7.0</spring-boot.version>
</properties>
<!-- 避免版本冲突,统一管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
模块二:CRUD接口封装与Lambda查询
2.1 实体类详细设计
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户实体类
* 重点:企业级实体类设计规范
*/
@Data // Lombok注解,自动生成getter、setter、toString、equals、hashCode
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true) // 链式调用:user.setName("张三").setAge(20)
@TableName("sys_user") // 指定数据库表名
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
* 重点:@TableId 注解配置主键策略
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户名
*/
@TableField("user_name") // 指定数据库字段名
private String userName;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
/**
* 部门ID
*/
private Long deptId;
/**
* 逻辑删除字段
* 重点:@TableLogic 实现逻辑删除
*/
@TableLogic
private Integer deleted;
/**
* 版本号 - 乐观锁
* 重点:@Version 实现乐观锁控制
*/
@Version
private Integer version;
/**
* 创建时间
* 重点:@TableField 配置字段自动填充
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
// 构造方法
public User() {}
public User(String userName, Integer age, String email) {
this.userName = userName;
this.age = age;
this.email = email;
}
}
2.2 Mapper接口逐行解析
package com.enterprise.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.enterprise.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 用户Mapper接口
* 重点:继承 BaseMapper 就获得了所有基础CRUD方法
*/
@Mapper // 可省略,因为@MapperScan已经扫描了整个包
public interface UserMapper extends BaseMapper<User> {
/**
* 继承BaseMapper<User>后自动获得的方法:
* int insert(T entity); // 插入一条记录
* int deleteById(Serializable id); // 根据ID删除
* int updateById(T entity); // 根据ID修改
* T selectById(Serializable id); // 根据ID查询
* List<T> selectList(Wrapper<T> queryWrapper); // 查询列表
* ... 等等20多个方法
*/
/**
* 自定义查询:根据用户名查询用户
* 重点:@Select注解实现简单SQL映射
*/
@Select("SELECT * FROM sys_user WHERE user_name = #{userName} AND deleted = 0")
User selectByUserName(@Param("userName") String userName);
/**
* 自定义查询:查询某个部门的所有用户
*/
@Select("SELECT * FROM sys_user WHERE dept_id = #{deptId} AND deleted = 0 ORDER BY create_time DESC")
List<User> selectByDeptId(@Param("deptId") Long deptId);
/**
* 复杂查询:查询年龄范围内的用户
* 重点:使用XML方式处理复杂SQL
*/
List<User> selectUsersByAgeRange(@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
}
2.3 Service层详细实现
package com.enterprise.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.enterprise.entity.User;
/**
* 用户服务接口
* 重点:继承 IService 获得增强的CRUD方法
*/
public interface UserService extends IService<User> {
/**
* 自定义业务方法:根据用户名查询用户
*/
User getUserByUserName(String userName);
/**
* 自定义业务方法:批量创建用户
*/
boolean batchCreateUsers(List<User> users);
/**
* 自定义业务方法:根据部门查询用户
*/
List<User> getUsersByDept(Long deptId);
}
package com.enterprise.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.enterprise.entity.User;
import com.enterprise.mapper.UserMapper;
import com.enterprise.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 用户服务实现类
* 重点:继承 ServiceImpl 并实现自定义接口
*/
@Slf4j // Lombok日志注解
@Service // Spring服务组件
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
/**
* ServiceImpl 已经提供的常用方法:
* save(T entity) - 保存一条记录
* saveBatch(Collection<T> entityList) - 批量保存
* getById(Serializable id) - 根据ID查询
* updateById(T entity) - 根据ID更新
* removeById(Serializable id) - 根据ID删除
* list() - 查询所有
* page(Page<T> page) - 分页查询
*/
@Override
public User getUserByUserName(String userName) {
// 使用BaseMapper的自定义方法
return baseMapper.selectByUserName(userName);
}
@Override
@Transactional(rollbackFor = Exception.class) // 重点:事务管理
public boolean batchCreateUsers(List<User> users) {
try {
// 使用MyBatis-Plus的批量保存
// 重点:saveBatch 内部会分批提交,避免大数据量时的内存问题
return saveBatch(users, 1000); // 每1000条提交一次
} catch (Exception e) {
log.error("批量创建用户失败: {}", e.getMessage(), e);
throw new RuntimeException("批量创建用户失败", e);
}
}
@Override
public List<User> getUsersByDept(Long deptId) {
// 使用Lambda查询,类型安全,编译期检查
return lambdaQuery()
.eq(User::getDeptId, deptId) // WHERE dept_id = #{deptId}
.eq(User::getDeleted, 0) // AND deleted = 0
.orderByDesc(User::getCreateTime) // ORDER BY create_time DESC
.list(); // 执行查询返回List
}
/**
* 复杂的Lambda查询示例
*/
public List<User> complexLambdaQuery(String userName, Integer minAge,
Integer maxAge, Long deptId) {
return lambdaQuery()
.like(userName != null, User::getUserName, userName) // 动态like条件
.ge(minAge != null, User::getAge, minAge) // 动态 >= 条件
.le(maxAge != null, User::getAge, maxAge) // 动态 <= 条件
.eq(deptId != null, User::getDeptId, deptId) // 动态 = 条件
.eq(User::getDeleted, 0) // 固定条件
.orderByAsc(User::getAge) // 按年龄升序
.orderByDesc(User::getCreateTime) // 按创建时间降序
.list();
}
/**
* Lambda更新示例
*/
@Transactional
public boolean updateUserEmail(Long userId, String newEmail) {
return lambdaUpdate()
.eq(User::getId, userId) // WHERE id = #{userId}
.set(User::getEmail, newEmail) // SET email = #{newEmail}
.set(User::getUpdateTime, LocalDateTime.now()) // SET update_time = NOW()
.update();
}
/**
* 链式查询示例
*/
public void chainQueryExample() {
// 链式编程,一步步构建查询
List<User> users = query()
.like("user_name", "张") // WHERE user_name LIKE '%张%'
.between("age", 18, 30) // AND age BETWEEN 18 AND 30
.isNotNull("email") // AND email IS NOT NULL
.orderByDesc("create_time") // ORDER BY create_time DESC
.list();
log.info("查询到 {} 条用户记录", users.size());
}
}
2.4 Controller层完整示例
package com.enterprise.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.enterprise.entity.User;
import com.enterprise.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户控制器
* 重点:RESTful API设计
*/
@Slf4j
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 创建用户 - POST /api/users
*/
@PostMapping
public ApiResult<User> createUser(@RequestBody User user) {
try {
boolean saved = userService.save(user);
if (saved) {
log.info("创建用户成功: {}", user.getUserName());
return ApiResult.success(user);
} else {
return ApiResult.fail("创建用户失败");
}
} catch (Exception e) {
log.error("创建用户异常: {}", e.getMessage(), e);
return ApiResult.fail("系统异常: " + e.getMessage());
}
}
/**
* 根据ID查询用户 - GET /api/users/{id}
*/
@GetMapping("/{id}")
public ApiResult<User> getUserById(@PathVariable Long id) {
User user = userService.getById(id);
if (user != null) {
return ApiResult.success(user);
} else {
return ApiResult.fail("用户不存在");
}
}
/**
* 分页查询用户 - GET /api/users/page
*/
@GetMapping("/page")
public ApiResult<Page<User>> getUserPage(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String userName) {
// 创建分页对象
Page<User> page = new Page<>(pageNum, pageSize);
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (userName != null && !userName.trim().isEmpty()) {
queryWrapper.like("user_name", userName.trim());
}
queryWrapper.eq("deleted", 0)
.orderByDesc("create_time");
// 执行分页查询
Page<User> userPage = userService.page(page, queryWrapper);
return ApiResult.success(userPage);
}
/**
* Lambda查询示例 - GET /api/users/lambda
*/
@GetMapping("/lambda")
public ApiResult<List<User>> getUsersByLambda(
@RequestParam(required = false) String userName,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge) {
List<User> users = userService.lambdaQuery()
.like(userName != null, User::getUserName, userName)
.ge(minAge != null, User::getAge, minAge)
.le(maxAge != null, User::getAge, maxAge)
.eq(User::getDeleted, 0)
.orderByDesc(User::getCreateTime)
.list();
return ApiResult.success(users);
}
/**
* 批量创建用户 - POST /api/users/batch
*/
@PostMapping("/batch")
public ApiResult<String> batchCreateUsers(@RequestBody List<User> users) {
try {
boolean success = userService.batchCreateUsers(users);
if (success) {
return ApiResult.success("批量创建成功,共" + users.size() + "个用户");
} else {
return ApiResult.fail("批量创建失败");
}
} catch (Exception e) {
log.error("批量创建用户异常: {}", e.getMessage(), e);
return ApiResult.fail("批量创建失败: " + e.getMessage());
}
}
/**
* 更新用户邮箱 - PUT /api/users/{id}/email
*/
@PutMapping("/{id}/email")
public ApiResult<String> updateUserEmail(@PathVariable Long id,
@RequestParam String email) {
boolean updated = userService.lambdaUpdate()
.eq(User::getId, id)
.set(User::getEmail, email)
.update();
if (updated) {
return ApiResult.success("邮箱更新成功");
} else {
return ApiResult.fail("邮箱更新失败,用户可能不存在");
}
}
}
/**
* 统一API响应结果封装
*/
class ApiResult<T> {
private boolean success;
private String message;
private T data;
private long timestamp;
// 构造方法、getter、setter 省略
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.setSuccess(true);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> ApiResult<T> fail(String message) {
ApiResult<T> result = new ApiResult<>();
result.setSuccess(false);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
2.5 完整的测试用例
package com.enterprise.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.enterprise.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* 用户服务测试类
* 重点:完整的单元测试覆盖所有CRUD操作
*/
@Slf4j
@SpringBootTest
@Transactional // 测试完成后回滚数据,避免污染数据库
class UserServiceTest {
@Autowired
private UserService userService;
/**
* 测试插入用户
*/
@Test
void testInsertUser() {
User user = new User();
user.setUserName("测试用户");
user.setAge(25);
user.setEmail("test@example.com");
boolean saved = userService.save(user);
// 断言测试结果
assert saved : "用户保存失败";
assert user.getId() != null : "用户ID未生成";
assert user.getCreateTime() != null : "创建时间未自动填充";
log.info("测试用户插入成功,ID: {}", user.getId());
}
/**
* 测试根据ID查询
*/
@Test
void testSelectById() {
// 先插入测试数据
User user = createTestUser();
userService.save(user);
// 根据ID查询
User dbUser = userService.getById(user.getId());
assert dbUser != null : "查询结果为空";
assert "测试用户".equals(dbUser.getUserName()) : "用户名不匹配";
assert dbUser.getAge().equals(25) : "年龄不匹配";
log.info("根据ID查询测试通过");
}
/**
* 测试Lambda查询
*/
@Test
void testLambdaQuery() {
// 准备测试数据
createTestUsers();
// 执行Lambda查询
List<User> users = userService.lambdaQuery()
.like(User::getUserName, "测试")
.ge(User::getAge, 20)
.le(User::getAge, 30)
.orderByDesc(User::getCreateTime)
.list();
assert users != null : "查询结果为空";
assert users.size() >= 2 : "查询结果数量不正确";
log.info("Lambda查询测试通过,查询到 {} 条记录", users.size());
}
/**
* 测试批量操作
*/
@Test
void testBatchOperations() {
List<User> users = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
User user = new User();
user.setUserName("批量用户" + i);
user.setAge(20 + i);
user.setEmail("batch" + i + "@example.com");
users.add(user);
}
// 批量保存
boolean batchResult = userService.saveBatch(users);
assert batchResult : "批量保存失败";
// 验证批量插入的数据
List<User> dbUsers = userService.lambdaQuery()
.like(User::getUserName, "批量用户")
.list();
assert dbUsers.size() == 5 : "批量插入数据数量不正确";
log.info("批量操作测试通过,成功插入 {} 条记录", dbUsers.size());
}
/**
* 测试Lambda更新
*/
@Test
void testLambdaUpdate() {
// 先创建测试用户
User user = createTestUser();
userService.save(user);
// 使用Lambda更新
boolean updated = userService.lambdaUpdate()
.eq(User::getId, user.getId())
.set(User::getEmail, "updated@example.com")
.set(User::getAge, 30)
.update();
assert updated : "Lambda更新失败";
// 验证更新结果
User updatedUser = userService.getById(user.getId());
assert "updated@example.com".equals(updatedUser.getEmail()) : "邮箱更新失败";
assert updatedUser.getAge().equals(30) : "年龄更新失败";
log.info("Lambda更新测试通过");
}
/**
* 测试逻辑删除
*/
@Test
void testLogicDelete() {
User user = createTestUser();
userService.save(user);
Long userId = user.getId();
// 执行逻辑删除
boolean deleted = userService.removeById(userId);
assert deleted : "逻辑删除失败";
// 验证逻辑删除后查询不到
User deletedUser = userService.getById(userId);
assert deletedUser == null : "逻辑删除后仍能查询到数据";
// 但数据库实际数据还在,只是deleted字段变为1
log.info("逻辑删除测试通过,用户ID: {}", userId);
}
// 辅助方法:创建测试用户
private User createTestUser() {
User user = new User();
user.setUserName("测试用户");
user.setAge(25);
user.setEmail("test@example.com");
return user;
}
// 辅助方法:创建多个测试用户
private void createTestUsers() {
List<User> users = new ArrayList<>();
users.add(createUser("测试用户A", 20, "a@test.com"));
users.add(createUser("测试用户B", 25, "b@test.com"));
users.add(createUser("其他用户", 30, "c@test.com"));
userService.saveBatch(users);
}
private User createUser(String name, Integer age, String email) {
User user = new User();
user.setUserName(name);
user.setAge(age);
user.setEmail(email);
return user;
}
}
2.6 企业级避坑方案详解
坑点1:ID生成策略选择
// 错误做法:混合使用不同的ID策略
@TableId(type = IdType.AUTO) // 数据库自增
private Long id;
// 同时又有
user.setId(123L); // 手动设置ID,与自增冲突
// 正确做法:统一策略
@TableId(type = IdType.ASSIGN_ID) // 分布式ID,自动生成
private Long id;
// 或者
@TableId(type = IdType.AUTO) // 数据库自增,但不手动设置ID
private Long id;
坑点2:字段自动填充时机
// 错误:手动设置自动填充字段
user.setCreateTime(new Date()); // 这样会覆盖自动填充
// 正确:依赖框架自动填充
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; // 框架在插入时自动设置
坑点3:Lambda表达式性能
// 错误:在循环中创建LambdaQueryWrapper
for (String name : names) {
List<User> users = userService.lambdaQuery()
.eq(User::getUserName, name)
.list(); // 每次循环都创建新的Wrapper,性能差
}
// 正确:批量查询或使用IN语句
List<User> users = userService.lambdaQuery()
.in(User::getUserName, names) // 一次查询所有
.list();
坑点4:事务管理
// 错误:在Controller中处理事务
@RestController
public class UserController {
@Autowired
private UserMapper userMapper; // 直接使用Mapper
@PostMapping
public void createUser(@RequestBody User user) {
userMapper.insert(user); // 没有事务管理,出错无法回滚
}
}
// 正确:在Service层使用事务
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class) // 声明式事务
public void createUser(User user) {
userMapper.insert(user);
// 其他业务操作...
}
}
企业级MyBatis-Plus实战教学(续)
模块三:条件构造器(QueryWrapper)高级用法
3.1 QueryWrapper 基础用法详细解析
/**
* QueryWrapper 基础用法详解
* 重点:QueryWrapper 是 MyBatis-Plus 最强大的功能之一,用于动态构建SQL条件
*/
@Service
@Slf4j
public class QueryWrapperService {
@Autowired
private UserMapper userMapper;
/**
* 1. 基础等值查询
*/
public void basicEqualsQuery() {
// 创建 QueryWrapper 实例
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// eq: 等于查询
queryWrapper.eq("user_name", "张三");
// 生成的SQL: WHERE user_name = '张三'
// ne: 不等于查询
queryWrapper.ne("age", 18);
// 生成的SQL: WHERE user_name = '张三' AND age != 18
List<User> users = userMapper.selectList(queryWrapper);
log.info("基础等值查询结果: {} 条", users.size());
}
/**
* 2. 范围查询详解
*/
public void rangeQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// gt: 大于 greater than
queryWrapper.gt("age", 18);
// 生成的SQL: WHERE age > 18
// ge: 大于等于 greater than or equal
queryWrapper.ge("age", 18);
// 生成的SQL: WHERE age >= 18
// lt: 小于 less than
queryWrapper.lt("age", 65);
// 生成的SQL: WHERE age >= 18 AND age < 65
// le: 小于等于 less than or equal
queryWrapper.le("age", 65);
// 生成的SQL: WHERE age >= 18 AND age <= 65
// between: 区间查询
queryWrapper.between("age", 18, 30);
// 生成的SQL: WHERE age BETWEEN 18 AND 30
// notBetween: 不在区间内
queryWrapper.notBetween("age", 18, 30);
// 生成的SQL: WHERE age NOT BETWEEN 18 AND 30
List<User> users = userMapper.selectList(queryWrapper);
log.info("范围查询结果: {} 条", users.size());
}
/**
* 3. 模糊查询详解
*/
public void fuzzyQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// like: 前后模糊匹配
queryWrapper.like("user_name", "张");
// 生成的SQL: WHERE user_name LIKE '%张%'
// likeLeft: 左模糊匹配
queryWrapper.likeLeft("user_name", "张");
// 生成的SQL: WHERE user_name LIKE '%张'
// likeRight: 右模糊匹配
queryWrapper.likeRight("user_name", "张");
// 生成的SQL: WHERE user_name LIKE '张%'
// notLike: 不包含
queryWrapper.notLike("user_name", "测试");
// 生成的SQL: WHERE user_name NOT LIKE '%测试%'
List<User> users = userMapper.selectList(queryWrapper);
log.info("模糊查询结果: {} 条", users.size());
}
/**
* 4. 空值判断查询
*/
public void nullValueQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// isNull: 字段为NULL
queryWrapper.isNull("email");
// 生成的SQL: WHERE email IS NULL
// isNotNull: 字段不为NULL
queryWrapper.isNotNull("email");
// 生成的SQL: WHERE email IS NOT NULL
List<User> users = userMapper.selectList(queryWrapper);
log.info("空值查询结果: {} 条", users.size());
}
/**
* 5. IN 和 NOT IN 查询
*/
public void inQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// in: 在列表中
List<Integer> ages = Arrays.asList(18, 20, 22, 25);
queryWrapper.in("age", ages);
// 生成的SQL: WHERE age IN (18, 20, 22, 25)
// notIn: 不在列表中
queryWrapper.notIn("age", Arrays.asList(60, 65, 70));
// 生成的SQL: WHERE age NOT IN (60, 65, 70)
List<User> users = userMapper.selectList(queryWrapper);
log.info("IN查询结果: {} 条", users.size());
}
/**
* 6. 分组和排序
*/
public void groupAndOrderQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// groupBy: 分组
queryWrapper.groupBy("dept_id");
// 生成的SQL: GROUP BY dept_id
// having: 分组后过滤
queryWrapper.having("COUNT(*) > 5");
// 生成的SQL: HAVING COUNT(*) > 5
// orderByAsc: 升序排序
queryWrapper.orderByAsc("age");
// 生成的SQL: ORDER BY age ASC
// orderByDesc: 降序排序
queryWrapper.orderByDesc("create_time");
// 生成的SQL: ORDER BY create_time DESC
// 多字段排序
queryWrapper.orderByAsc("dept_id", "age")
.orderByDesc("create_time");
// 生成的SQL: ORDER BY dept_id ASC, age ASC, create_time DESC
List<User> users = userMapper.selectList(queryWrapper);
log.info("分组排序查询结果: {} 条", users.size());
}
}
3.2 QueryWrapper 高级用法详细解析
/**
* QueryWrapper 高级用法详解
* 重点:复杂查询条件的构建
*/
@Service
@Slf4j
public class AdvancedQueryWrapperService {
@Autowired
private UserMapper userMapper;
/**
* 1. 动态条件构建
* 重点:根据业务参数动态构建查询条件
*/
public List<User> dynamicQuery(String userName, Integer minAge,
Integer maxAge, Long deptId) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 动态添加条件:只有当参数不为空时才添加条件
if (StringUtils.isNotBlank(userName)) {
queryWrapper.like("user_name", userName);
}
if (minAge != null) {
queryWrapper.ge("age", minAge);
}
if (maxAge != null) {
queryWrapper.le("age", maxAge);
}
if (deptId != null) {
queryWrapper.eq("dept_id", deptId);
}
// 固定条件:逻辑删除过滤
queryWrapper.eq("deleted", 0);
// 排序
queryWrapper.orderByDesc("create_time");
return userMapper.selectList(queryWrapper);
}
/**
* 2. 链式条件构建
* 重点:使用链式调用构建复杂条件
*/
public List<User> chainQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "user_name", "age", "email") // 指定查询字段
.like("user_name", "张") // 模糊查询
.between("age", 18, 30) // 范围查询
.isNotNull("email") // 非空查询
.eq("deleted", 0) // 等值查询
.orderByDesc("create_time") // 排序
.last("LIMIT 100"); // 最后拼接
return userMapper.selectList(queryWrapper);
}
/**
* 3. 嵌套查询(子查询)
* 重点:使用 apply 和 exists 进行子查询
*/
public List<User> nestedQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 使用 apply 进行子查询
queryWrapper.apply("id IN (SELECT user_id FROM user_role WHERE role_id = {0}", 1);
// 生成的SQL: WHERE id IN (SELECT user_id FROM user_role WHERE role_id = 1)
// 使用 exists 进行存在性查询
queryWrapper.exists("SELECT 1 FROM sys_dept WHERE id = dept_id AND status = 1");
// 生成的SQL: WHERE EXISTS (SELECT 1 FROM sys_dept WHERE id = dept_id AND status = 1)
return userMapper.selectList(queryWrapper);
}
/**
* 4. OR 条件组合
* 重点:构建 OR 逻辑的复杂查询
*/
public List<User> orQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 方式1:直接使用 or()
queryWrapper.eq("dept_id", 1)
.or()
.eq("dept_id", 2);
// 生成的SQL: WHERE dept_id = 1 OR dept_id = 2
// 方式2:嵌套 OR 条件
queryWrapper.nested(i -> i.eq("dept_id", 1).or().eq("dept_id", 2))
.like("user_name", "张");
// 生成的SQL: WHERE (dept_id = 1 OR dept_id = 2) AND user_name LIKE '%张%'
// 方式3:复杂的 OR 条件组合
queryWrapper.and(i -> i.eq("status", 1).gt("age", 18))
.or(i -> i.eq("status", 2).lt("age", 60));
// 生成的SQL: WHERE (status = 1 AND age > 18) OR (status = 2 AND age < 60)
return userMapper.selectList(queryWrapper);
}
/**
* 5. 函数查询
* 重点:使用数据库函数进行查询
*/
public List<User> functionQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 使用日期函数
queryWrapper.apply("DATE(create_time) = {0}", "2023-01-01");
// 生成的SQL: WHERE DATE(create_time) = '2023-01-01'
// 使用字符串函数
queryWrapper.apply("LENGTH(user_name) > {0}", 5);
// 生成的SQL: WHERE LENGTH(user_name) > 5
// 使用数学函数
queryWrapper.apply("MOD(age, 2) = {0}", 0);
// 生成的SQL: WHERE MOD(age, 2) = 0
return userMapper.selectList(queryWrapper);
}
/**
* 6. 查询字段控制
* 重点:精确控制查询的字段,提升性能
*/
public List<Map<String, Object>> selectSpecificFields() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 指定查询字段
queryWrapper.select("id", "user_name", "age", "email")
.eq("deleted", 0)
.orderByDesc("create_time");
// 返回 Map 列表,只包含指定字段
return userMapper.selectMaps(queryWrapper);
}
/**
* 7. 聚合查询
* 重点:使用聚合函数进行统计查询
*/
public void aggregateQuery() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 计数查询
queryWrapper.select("COUNT(*) as user_count")
.eq("deleted", 0);
Map<String, Object> countMap = userMapper.selectMaps(queryWrapper).get(0);
log.info("用户总数: {}", countMap.get("user_count"));
// 分组统计
queryWrapper = new QueryWrapper<>();
queryWrapper.select("dept_id, COUNT(*) as dept_user_count")
.eq("deleted", 0)
.groupBy("dept_id")
.having("COUNT(*) > 0");
List<Map<String, Object>> deptStats = userMapper.selectMaps(queryWrapper);
for (Map<String, Object> stat : deptStats) {
log.info("部门 {} 有 {} 个用户",
stat.get("dept_id"), stat.get("dept_user_count"));
}
}
}
3.3 LambdaQueryWrapper 详细解析
/**
* LambdaQueryWrapper 详细用法
* 重点:类型安全的查询条件构建,避免硬编码字段名
*/
@Service
@Slf4j
public class LambdaQueryWrapperService {
@Autowired
private UserMapper userMapper;
/**
* 1. LambdaQueryWrapper 基础用法
*/
public void basicLambdaQuery() {
// 创建 LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 等值查询 - 使用方法引用,编译期检查
lambdaWrapper.eq(User::getUserName, "张三");
// 生成的SQL: WHERE user_name = '张三'
// 范围查询
lambdaWrapper.between(User::getAge, 18, 30);
// 生成的SQL: WHERE age BETWEEN 18 AND 30
// 模糊查询
lambdaWrapper.like(User::getUserName, "张");
// 生成的SQL: WHERE user_name LIKE '%张%'
List<User> users = userMapper.selectList(lambdaWrapper);
log.info("Lambda查询结果: {} 条", users.size());
}
/**
* 2. 动态Lambda查询
* 重点:类型安全的动态条件构建
*/
public List<User> dynamicLambdaQuery(UserQueryDTO queryDTO) {
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 动态条件构建 - 类型安全,IDE智能提示
lambdaWrapper.eq(queryDTO.getDeptId() != null, User::getDeptId, queryDTO.getDeptId())
.like(StringUtils.isNotBlank(queryDTO.getUserName()),
User::getUserName, queryDTO.getUserName())
.ge(queryDTO.getMinAge() != null, User::getAge, queryDTO.getMinAge())
.le(queryDTO.getMaxAge() != null, User::getAge, queryDTO.getMaxAge())
.eq(User::getDeleted, 0)
.orderByDesc(User::getCreateTime);
return userMapper.selectList(lambdaWrapper);
}
/**
* 3. Lambda查询字段选择
* 重点:类型安全的字段选择
*/
public List<User> lambdaSelectFields() {
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 指定查询字段 - 编译期检查字段名正确性
lambdaWrapper.select(User::getId, User::getUserName, User::getAge, User::getEmail)
.eq(User::getDeleted, 0)
.orderByDesc(User::getCreateTime);
return userMapper.selectList(lambdaWrapper);
}
/**
* 4. 复杂的Lambda条件组合
*/
public List<User> complexLambdaCondition() {
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 复杂的 AND/OR 条件组合
lambdaWrapper.and(wrapper -> wrapper
.eq(User::getStatus, 1)
.or()
.eq(User::getStatus, 2))
.and(wrapper -> wrapper
.ge(User::getAge, 18)
.le(User::getAge, 60))
.like(User::getUserName, "张")
.orderByAsc(User::getAge)
.orderByDesc(User::getCreateTime);
return userMapper.selectList(lambdaWrapper);
}
/**
* 5. Lambda表达式与普通条件混合使用
*/
public List<User> mixedConditionQuery() {
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// Lambda表达式条件
lambdaWrapper.eq(User::getDeleted, 0)
.ge(User::getAge, 18);
// 普通条件(复杂条件或函数条件)
lambdaWrapper.apply("LENGTH(user_name) > {0}", 2)
.like("user_name", "张");
return userMapper.selectList(lambdaWrapper);
}
}
/**
* 查询参数DTO
*/
@Data
class UserQueryDTO {
private String userName;
private Integer minAge;
private Integer maxAge;
private Long deptId;
private Integer pageNum = 1;
private Integer pageSize = 10;
}
3.4 QueryWrapper 底层原理深度解析
/**
* QueryWrapper 底层原理分析
* 重点:理解 MyBatis-Plus 如何将 Java 条件转换为 SQL
*/
public class QueryWrapperPrincipleAnalysis {
/**
* 1. QueryWrapper 核心结构分析
*/
public void analyzeQueryWrapperStructure() {
/*
QueryWrapper 继承结构:
QueryWrapper -> AbstractWrapper -> Wrapper
核心字段:
- List<Segment> expression: 存储所有条件表达式
- String sqlSelect: 存储查询字段
- String sqlComment: 存储注释
- String sqlFirst: 存储前置SQL(如 FOR UPDATE)
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", "张三")
.gt("age", 18)
.orderByDesc("create_time");
// 底层会构建这样的表达式树:
// [
// EqSegment(column=user_name, value=张三),
// GtSegment(column=age, value=18),
// OrderBySegment(column=create_time, isAsc=false)
// ]
}
/**
* 2. SQL 生成过程分析
*/
public void analyzeSqlGeneration() {
/*
SQL 生成步骤:
1. 解析 QueryWrapper 中的表达式列表
2. 根据表达式类型生成对应的 SQL 片段
3. 拼接 WHERE 条件
4. 处理排序、分组等
5. 参数化处理,防止 SQL 注入
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", "张三")
.gt("age", 18);
// 生成的 SQL 模板:
// SELECT * FROM sys_user WHERE user_name = ? AND age > ?
// 参数列表:
// [0] = "张三"
// [1] = 18
}
/**
* 3. LambdaQueryWrapper 原理分析
*/
public void analyzeLambdaQueryWrapper() {
/*
LambdaQueryWrapper 原理:
1. 使用方法引用(如 User::getUserName)
2. 通过 SerializedLambda 解析方法引用
3. 获取对应的字段名(如 user_name)
4. 后续处理与 QueryWrapper 相同
*/
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getUserName, "张三");
// 底层过程:
// 1. User::getUserName -> 方法引用
// 2. LambdaUtils.resolve(User::getUserName) -> 获取字段信息
// 3. 转换为 "user_name" 字符串
// 4. 构建 EqSegment(column=user_name, value=张三)
}
}
3.5 与原生 MyBatis 对比详细示例
/**
* QueryWrapper 与原生 MyBatis 对比
* 重点:展示两种方式的差异和优劣
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 方式1:使用 MyBatis-Plus QueryWrapper
* 优点:代码简洁,动态条件构建方便
*/
default List<User> findUsersByWrapper(String userName, Integer minAge,
Integer maxAge, Long deptId) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(userName)) {
queryWrapper.like("user_name", userName);
}
if (minAge != null) {
queryWrapper.ge("age", minAge);
}
if (maxAge != null) {
queryWrapper.le("age", maxAge);
}
if (deptId != null) {
queryWrapper.eq("dept_id", deptId);
}
queryWrapper.eq("deleted", 0)
.orderByDesc("create_time");
return selectList(queryWrapper);
}
/**
* 方式2:使用原生 MyBatis XML 配置
* 优点:复杂SQL更清晰,SQL调优更方便
*/
List<User> findUsersByXML(@Param("userName") String userName,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
@Param("deptId") Long deptId);
}
<!-- UserMapper.xml - 原生 MyBatis 动态 SQL -->
<mapper namespace="com.enterprise.mapper.UserMapper">
<select id="findUsersByXML" resultType="User">
SELECT id, user_name, age, email, dept_id, create_time
FROM sys_user
WHERE deleted = 0
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
ORDER BY create_time DESC
</select>
</mapper>
3.6 企业级避坑方案详细解析
/**
* QueryWrapper 企业级避坑方案
* 重点:生产环境中常见问题和解决方案
*/
@Service
@Slf4j
public class QueryWrapperBestPractice {
@Autowired
private UserMapper userMapper;
/**
* 坑点1:SQL注入风险
*/
public void avoidSqlInjection() {
// 错误做法:直接拼接用户输入
String userInput = "张三'; DROP TABLE sys_user; --";
QueryWrapper<User> wrongWrapper = new QueryWrapper<>();
wrongWrapper.apply("user_name = '" + userInput + "'"); // 危险!
// 正确做法1:使用参数化查询
QueryWrapper<User> safeWrapper1 = new QueryWrapper<>();
safeWrapper1.apply("user_name = {0}", userInput); // 安全
// 正确做法2:使用预定义方法
QueryWrapper<User> safeWrapper2 = new QueryWrapper<>();
safeWrapper2.eq("user_name", userInput); // 安全
}
/**
* 坑点2:条件覆盖问题
*/
public void avoidConditionOverride() {
// 错误做法:在循环中重复创建Wrapper
List<String> names = Arrays.asList("张三", "李四", "王五");
QueryWrapper<User> wrapper = new QueryWrapper<>();
for (String name : names) {
wrapper = new QueryWrapper<>(); // 每次循环覆盖之前条件
wrapper.eq("user_name", name);
List<User> users = userMapper.selectList(wrapper); // 每次只查一个名字
}
// 正确做法:使用IN查询或OR条件
QueryWrapper<User> correctWrapper = new QueryWrapper<>();
correctWrapper.in("user_name", names); // 一次查询所有
List<User> allUsers = userMapper.selectList(correctWrapper);
}
/**
* 坑点3:N+1查询问题
*/
public void avoidNPlusOneQuery() {
// 错误做法:循环中查询关联数据
List<User> users = userMapper.selectList(new QueryWrapper<>());
for (User user : users) {
// 为每个用户查询部门信息 - N+1查询
// String deptName = deptMapper.selectDeptNameById(user.getDeptId());
}
// 正确做法:使用JOIN查询一次获取所有数据
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("u.*", "d.dept_name")
.eq("u.deleted", 0)
.last("LEFT JOIN sys_dept d ON u.dept_id = d.id");
List<Map<String, Object>> userWithDept = userMapper.selectMaps(wrapper);
}
/**
* 坑点4:索引失效问题
*/
public void avoidIndexInvalidation() {
// 错误做法:对索引列使用函数
QueryWrapper<User> wrongWrapper = new QueryWrapper<>();
wrongWrapper.apply("YEAR(create_time) = 2023"); // 可能导致索引失效
// 正确做法:使用范围查询
QueryWrapper<User> correctWrapper = new QueryWrapper<>();
correctWrapper.between("create_time",
LocalDateTime.of(2023, 1, 1, 0, 0),
LocalDateTime.of(2023, 12, 31, 23, 59));
}
/**
* 坑点5:分页性能问题
*/
public void avoidPaginationPerformanceIssue() {
// 错误做法:直接对大偏移量分页
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", 0)
.orderByDesc("create_time");
// 当 pageNum 很大时性能极差
Page<User> page = new Page<>(10000, 10);
IPage<User> userPage = userMapper.selectPage(page, wrapper);
// 正确做法:使用游标分页或子查询优化
QueryWrapper<User> optimizedWrapper = new QueryWrapper<>();
optimizedWrapper.eq("deleted", 0)
.gt("id", 100000) // 基于ID的游标分页
.orderByAsc("id")
.last("LIMIT 10");
List<User> users = userMapper.selectList(optimizedWrapper);
}
/**
* 坑点6:内存溢出问题
*/
public void avoidMemoryOverflow() {
// 错误做法:一次性查询大量数据
QueryWrapper<User> wrongWrapper = new QueryWrapper<>();
wrongWrapper.eq("deleted", 0);
List<User> allUsers = userMapper.selectList(wrongWrapper); // 可能OOM
// 正确做法:分批次查询
int batchSize = 1000;
long total = userMapper.selectCount(new QueryWrapper<>());
for (int i = 0; i < total; i += batchSize) {
QueryWrapper<User> batchWrapper = new QueryWrapper<>();
batchWrapper.eq("deleted", 0)
.last("LIMIT " + batchSize + " OFFSET " + i);
List<User> batchUsers = userMapper.selectList(batchWrapper);
// 处理批次数据
processBatchUsers(batchUsers);
}
}
private void processBatchUsers(List<User> users) {
// 处理批次数据
log.info("处理 {} 条用户数据", users.size());
}
/**
* 最佳实践:统一的查询构建方法
*/
public QueryWrapper<User> buildUserQueryWrapper(UserQueryDTO queryDTO) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 基础条件
wrapper.eq("deleted", 0);
// 动态条件
if (StringUtils.isNotBlank(queryDTO.getUserName())) {
wrapper.like("user_name", queryDTO.getUserName().trim());
}
if (queryDTO.getMinAge() != null && queryDTO.getMinAge() > 0) {
wrapper.ge("age", queryDTO.getMinAge());
}
if (queryDTO.getMaxAge() != null && queryDTO.getMaxAge() > 0) {
wrapper.le("age", queryDTO.getMaxAge());
}
if (queryDTO.getDeptId() != null) {
wrapper.eq("dept_id", queryDTO.getDeptId());
}
// 排序
wrapper.orderByDesc("create_time");
// 限制最大查询数量(防止恶意查询)
if (queryDTO.getPageSize() != null && queryDTO.getPageSize() > 1000) {
wrapper.last("LIMIT 1000");
}
return wrapper;
}
}
3.7 完整测试用例
/**
* QueryWrapper 完整测试用例
* 重点:覆盖所有重要场景的测试
*/
@SpringBootTest
@Slf4j
class QueryWrapperTest {
@Autowired
private UserMapper userMapper;
@Autowired
private QueryWrapperService queryWrapperService;
@BeforeEach
void setUp() {
// 准备测试数据
prepareTestData();
}
@Test
void testBasicQueryWrapper() {
// 测试基础查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", "测试用户1")
.eq("deleted", 0);
List<User> users = userMapper.selectList(wrapper);
assertNotNull(users);
assertEquals(1, users.size());
assertEquals("测试用户1", users.get(0).getUserName());
}
@Test
void testRangeQuery() {
// 测试范围查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age", 20, 30)
.eq("deleted", 0);
List<User> users = userMapper.selectList(wrapper);
assertNotNull(users);
assertTrue(users.size() >= 2);
// 验证所有结果都在指定范围内
for (User user : users) {
assertTrue(user.getAge() >= 20 && user.getAge() <= 30);
}
}
@Test
void testLambdaQueryWrapper() {
// 测试Lambda查询
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getUserName, "测试用户1")
.eq(User::getDeleted, 0);
List<User> users = userMapper.selectList(lambdaWrapper);
assertNotNull(users);
assertEquals(1, users.size());
assertEquals("测试用户1", users.get(0).getUserName());
}
@Test
void testDynamicQuery() {
// 测试动态查询
UserQueryDTO queryDTO = new UserQueryDTO();
queryDTO.setUserName("测试");
queryDTO.setMinAge(20);
queryDTO.setMaxAge(30);
List<User> users = queryWrapperService.dynamicQuery(
queryDTO.getUserName(),
queryDTO.getMinAge(),
queryDTO.getMaxAge(),
null
);
assertNotNull(users);
assertTrue(users.size() >= 2);
}
@Test
void testComplexCondition() {
// 测试复杂条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "user_name", "age")
.like("user_name", "测试")
.in("age", Arrays.asList(20, 25, 30))
.eq("deleted", 0)
.orderByDesc("create_time")
.last("LIMIT 5");
List<User> users = userMapper.selectList(wrapper);
assertNotNull(users);
assertTrue(users.size() <= 5);
// 验证字段选择
for (User user : users) {
assertNotNull(user.getId());
assertNotNull(user.getUserName());
assertNotNull(user.getAge());
assertNull(user.getEmail()); // 未选择email字段,应该为null
}
}
@Test
void testOrCondition() {
// 测试OR条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("dept_id", 1)
.or()
.eq("dept_id", 2)
.eq("deleted", 0);
List<User> users = userMapper.selectList(wrapper);
assertNotNull(users);
// 验证所有结果都满足条件
for (User user : users) {
assertTrue(user.getDeptId() == 1 || user.getDeptId() == 2);
}
}
private void prepareTestData() {
// 清理测试数据
userMapper.delete(new QueryWrapper<User>().like("user_name", "测试用户"));
// 插入测试数据
List<User> testUsers = Arrays.asList(
createUser("测试用户1", 20, "test1@example.com", 1L),
createUser("测试用户2", 25, "test2@example.com", 1L),
createUser("测试用户3", 30, "test3@example.com", 2L),
createUser("测试用户4", 35, "test4@example.com", 2L),
createUser("其他用户", 40, "other@example.com", 3L)
);
for (User user : testUsers) {
userMapper.insert(user);
}
log.info("测试数据准备完成,共插入 {} 条记录", testUsers.size());
}
private User createUser(String name, Integer age, String email, Long deptId) {
User user = new User();
user.setUserName(name);
user.setAge(age);
user.setEmail(email);
user.setDeptId(deptId);
return user;
}
}
企业级MyBatis-Plus实战教学(续)
模块四:分页插件实现原理与性能优化
4.1 分页插件详细配置
/**
* 分页插件深度配置详解
* 重点:企业级分页配置和性能调优
*/
@Configuration
@Slf4j
public class PaginationConfig {
/**
* MyBatis-Plus 分页拦截器详细配置
* 重点:拦截器执行顺序和配置参数详解
*/
@Bean
@Order(1) // 重点:确保分页拦截器在第一个执行
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 分页插件配置(必须第一个添加)
PaginationInnerInterceptor paginationInterceptor = createPaginationInterceptor();
interceptor.addInnerInterceptor(paginationInterceptor);
// 2. 乐观锁插件
OptimisticLockerInnerInterceptor optimisticLockerInterceptor =
new OptimisticLockerInnerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInterceptor);
// 3. 防止全表更新与删除插件
BlockAttackInnerInterceptor blockAttackInterceptor =
new BlockAttackInnerInterceptor();
interceptor.addInnerInterceptor(blockAttackInterceptor);
log.info("MyBatis-Plus 拦截器配置完成,包含分页、乐观锁、防全表操作插件");
return interceptor;
}
/**
* 创建分页拦截器详细配置
*/
private PaginationInnerInterceptor createPaginationInterceptor() {
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor();
// 1. 设置数据库类型
paginationInterceptor.setDbType(DbType.MYSQL);
log.info("设置数据库类型: {}", DbType.MYSQL);
// 2. 设置最大单页限制数量
paginationInterceptor.setMaxLimit(1000L);
log.info("设置最大单页限制: {} 条", 1000);
// 3. 开启溢出处理
paginationInterceptor.setOverflow(true);
log.info("开启溢出处理: {}", true);
// 溢出处理说明:当请求页码大于最大页码时,true返回首页,false继续请求默认false
// 4. 开启 count 查询 join 优化
paginationInterceptor.setOptimizeJoin(true);
log.info("开启count查询join优化: {}", true);
// 优化说明:只针对部分 left join 进行优化
// 5. 设置默认分页参数(可选)
// paginationInterceptor.setProperties(new Properties());
return paginationInterceptor;
}
/**
* SQL 性能分析插件(仅开发环境使用)
* 重点:生产环境必须关闭,否则有性能和安全风险
*/
@Bean
@Profile({"dev", "test"}) // 只在开发测试环境生效
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
// 设置SQL执行最大时间,单位:ms
performanceInterceptor.setMaxTime(1000L);
log.info("设置SQL执行最大时间: {}ms", 1000);
// 设置是否格式化SQL
performanceInterceptor.setFormat(true);
log.info("设置SQL格式化: {}", true);
// 设置SQL输出到控制台
performanceInterceptor.setWriteInLog(true);
log.info("设置SQL日志输出: {}", true);
return performanceInterceptor;
}
}
4.2 分页查询基础用法详细解析
/**
* 分页查询基础用法详解
* 重点:各种分页场景的完整实现
*/
@Service
@Slf4j
public class PaginationBasicService {
@Autowired
private UserMapper userMapper;
/**
* 1. 基础分页查询
* 重点:Page 对象的创建和使用
*/
public Page<User> basicPagination(Integer pageNum, Integer pageSize) {
// 创建分页对象
// 重点:Page 构造函数参数 (当前页码, 每页显示条数)
Page<User> page = new Page<>(pageNum, pageSize);
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0)
.orderByDesc("create_time");
log.info("执行基础分页查询 - 页码: {}, 页大小: {}", pageNum, pageSize);
// 执行分页查询
// 重点:selectPage 方法会自动执行两条SQL:
// 1. SELECT COUNT(*) FROM sys_user WHERE deleted = 0
// 2. SELECT * FROM sys_user WHERE deleted = 0 ORDER BY create_time DESC LIMIT ?
Page<User> resultPage = userMapper.selectPage(page, queryWrapper);
// 分页结果分析
log.info("分页查询结果 - 总记录数: {}, 总页数: {}, 当前页记录数: {}",
resultPage.getTotal(), resultPage.getPages(), resultPage.getRecords().size());
return resultPage;
}
/**
* 2. 不进行 count 查询的分页
* 重点:提升性能,适用于不需要知道总记录数的场景
*/
public Page<User> paginationWithoutCount(Integer pageNum, Integer pageSize) {
// 创建不分页对象,第三个参数为 false 表示不执行 count 查询
Page<User> page = new Page<>(pageNum, pageSize, false);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0)
.orderByDesc("create_time");
log.info("执行不分页查询 - 页码: {}, 页大小: {}, 不执行count查询", pageNum, pageSize);
Page<User> resultPage = userMapper.selectPage(page, queryWrapper);
// 注意:不执行count查询时,getTotal() 返回 0,getPages() 返回 0
log.info("不分页查询结果 - 当前页记录数: {}", resultPage.getRecords().size());
return resultPage;
}
/**
* 3. 自定义 count 查询的分页
* 重点:复杂查询时优化 count 查询性能
*/
public Page<User> paginationWithCustomCount(Integer pageNum, Integer pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0)
.like("user_name", "张")
.orderByDesc("create_time");
log.info("执行自定义count分页查询");
// 对于复杂查询,可以在XML中自定义count查询
Page<User> resultPage = userMapper.selectUserPageWithCustomCount(page, queryWrapper);
return resultPage;
}
/**
* 4. 多表关联分页查询
* 重点:关联查询的分页处理
*/
public Page<UserVO> paginationWithJoin(Integer pageNum, Integer pageSize) {
Page<UserVO> page = new Page<>(pageNum, pageSize);
log.info("执行多表关联分页查询");
// 使用自定义的关联查询方法
Page<UserVO> resultPage = userMapper.selectUserWithDeptPage(page);
log.info("关联分页查询结果 - 总记录数: {}, 总页数: {}",
resultPage.getTotal(), resultPage.getPages());
return resultPage;
}
/**
* 5. Lambda 表达式分页查询
* 重点:类型安全的分页查询
*/
public Page<User> lambdaPagination(Integer pageNum, Integer pageSize, String userName) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getDeleted, 0)
.like(StringUtils.isNotBlank(userName), User::getUserName, userName)
.orderByDesc(User::getCreateTime);
log.info("执行Lambda分页查询 - 用户名过滤: {}", userName);
Page<User> resultPage = userMapper.selectPage(page, lambdaWrapper);
return resultPage;
}
}
4.3 自定义分页 Mapper 详细实现
/**
* 自定义分页 Mapper 详解
* 重点:复杂分页查询的XML配置和接口定义
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 1. 基础分页查询(使用BaseMapper自带方法)
* 无需额外定义,直接使用 selectPage 方法
*/
/**
* 2. 自定义 count 查询的分页方法
* 重点:使用 @Param("ew") 注解接收 QueryWrapper
*/
IPage<User> selectUserPageWithCustomCount(IPage<User> page,
@Param("ew") QueryWrapper<User> queryWrapper);
/**
* 3. 多表关联分页查询
* 重点:返回VO对象的分页查询
*/
IPage<UserVO> selectUserWithDeptPage(IPage<UserVO> page);
/**
* 4. 复杂条件分页查询
*/
IPage<User> selectUsersByComplexCondition(IPage<User> page,
@Param("userName") String userName,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
@Param("deptIds") List<Long> deptIds);
/**
* 5. 分页查询返回 Map
* 重点:灵活的结果集处理
*/
IPage<Map<String, Object>> selectUserMapsPage(IPage<Map<String, Object>> page,
@Param("ew") QueryWrapper<User> queryWrapper);
}
<!-- UserMapper.xml 分页查询详细配置 -->
<mapper namespace="com.enterprise.mapper.UserMapper">
<!-- 1. 基础分页查询 - 使用MyBatis-Plus自动生成 -->
<!-- 2. 自定义 count 查询的分页 -->
<select id="selectUserPageWithCustomCount" resultType="User">
SELECT
id, user_name, age, email, dept_id, create_time
FROM sys_user
<!-- 重点:使用 ${ew.customSqlSegment} 注入QueryWrapper条件 -->
${ew.customSqlSegment}
</select>
<!-- 自定义 count 查询 -->
<select id="selectUserPageWithCustomCount_count" resultType="java.lang.Long">
SELECT COUNT(*)
FROM sys_user
${ew.customSqlSegment}
</select>
<!-- 3. 多表关联分页查询 -->
<select id="selectUserWithDeptPage" resultType="UserVO">
SELECT
u.id,
u.user_name,
u.age,
u.email,
u.create_time,
d.dept_name,
d.leader
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id
WHERE u.deleted = 0 AND d.deleted = 0
ORDER BY u.create_time DESC
</select>
<!-- 关联查询的 count -->
<select id="selectUserWithDeptPage_count" resultType="java.lang.Long">
SELECT COUNT(*)
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id
WHERE u.deleted = 0 AND d.deleted = 0
</select>
<!-- 4. 复杂条件分页查询 -->
<select id="selectUsersByComplexCondition" resultType="User">
SELECT
id, user_name, age, email, dept_id, create_time
FROM sys_user
WHERE deleted = 0
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="deptIds != null and deptIds.size() > 0">
AND dept_id IN
<foreach collection="deptIds" item="deptId" open="(" close=")" separator=",">
#{deptId}
</foreach>
</if>
ORDER BY create_time DESC
</select>
<!-- 5. 分页查询返回 Map -->
<select id="selectUserMapsPage" resultType="java.util.Map">
SELECT
id, user_name, age, email
FROM sys_user
${ew.customSqlSegment}
</select>
</select>
4.4 分页结果封装和响应处理
/**
* 分页结果统一封装
* 重点:企业级分页响应格式标准化
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 分页数据
*/
private PageData<T> data;
/**
* 响应时间戳
*/
private Long timestamp;
/**
* 成功响应
*/
public static <T> PageResult<T> success(Page<T> page) {
return PageResult.<T>builder()
.code(200)
.message("success")
.data(PageData.of(page))
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 失败响应
*/
public static <T> PageResult<T> error(String message) {
return PageResult.<T>builder()
.code(500)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
}
/**
* 分页数据封装
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageData<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页
*/
private Long current;
/**
* 每页大小
*/
private Long size;
/**
* 总记录数
*/
private Long total;
/**
* 总页数
*/
private Long pages;
/**
* 数据列表
*/
private List<T> records;
/**
* 是否有上一页
*/
private Boolean hasPrevious;
/**
* 是否有下一页
*/
private Boolean hasNext;
public static <T> PageData<T> of(Page<T> page) {
return PageData.<T>builder()
.current(page.getCurrent())
.size(page.getSize())
.total(page.getTotal())
.pages(page.getPages())
.records(page.getRecords())
.hasPrevious(page.getCurrent() > 1)
.hasNext(page.getCurrent() < page.getPages())
.build();
}
}
/**
* 分页查询参数基类
*/
@Data
public class BasePageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码,默认第一页
*/
@Min(value = 1, message = "页码不能小于1")
private Integer pageNum = 1;
/**
* 每页数量,默认10条
*/
@Min(value = 1, message = "每页数量不能小于1")
@Max(value = 1000, message = "每页数量不能大于1000")
private Integer pageSize = 10;
/**
* 排序字段
*/
private String sortBy;
/**
* 排序方向:ASC-升序,DESC-降序
*/
private String sortDirection = "DESC";
/**
* 转换为 MyBatis-Plus Page 对象
*/
public <T> Page<T> toPage() {
Page<T> page = new Page<>(pageNum, pageSize);
// 设置排序
if (StringUtils.isNotBlank(sortBy)) {
if ("ASC".equalsIgnoreCase(sortDirection)) {
page.addOrder(OrderItem.asc(sortBy));
} else {
page.addOrder(OrderItem.desc(sortBy));
}
}
return page;
}
/**
* 计算偏移量
*/
public Long getOffset() {
return (long) (pageNum - 1) * pageSize;
}
}
/**
* 用户分页查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPageQuery extends BasePageQuery {
/**
* 用户名模糊查询
*/
private String userName;
/**
* 年龄范围查询 - 最小值
*/
@Min(value = 0, message = "年龄不能小于0")
private Integer minAge;
/**
* 年龄范围查询 - 最大值
*/
@Min(value = 0, message = "年龄不能小于0")
private Integer maxAge;
/**
* 部门ID
*/
private Long deptId;
/**
* 邮箱模糊查询
*/
private String email;
/**
* 创建时间范围 - 开始时间
*/
private LocalDateTime createTimeStart;
/**
* 创建时间范围 - 结束时间
*/
private LocalDateTime createTimeEnd;
}
4.5 分页插件底层原理深度解析
/**
* 分页插件底层原理深度解析
* 重点:理解分页查询的完整执行流程
*/
@Slf4j
public class PaginationPrincipleAnalysis {
/**
* 分页拦截器执行流程分析
*/
public void analyzePaginationProcess() {
/*
分页拦截器(PaginationInnerInterceptor)执行流程:
1. 拦截 Executor 的 query 方法
2. 判断是否需要进行分页(方法参数中是否包含 IPage 对象)
3. 如果需要分页:
a. 执行 count 查询获取总记录数
b. 根据数据库类型重写原始 SQL,添加分页条件
c. 执行分页查询
d. 将结果封装到 IPage 对象中
4. 如果不需要分页,直接执行原始查询
*/
}
/**
* 分页 SQL 生成原理
*/
public void analyzePaginationSqlGeneration() {
/*
MySQL 分页 SQL 生成:
原始SQL: SELECT * FROM sys_user WHERE deleted = 0 ORDER BY create_time DESC
分页SQL: SELECT * FROM sys_user WHERE deleted = 0 ORDER BY create_time DESC LIMIT 0, 10
CountSQL: SELECT COUNT(*) FROM sys_user WHERE deleted = 0
生成步骤:
1. 解析原始 SQL,识别查询主体
2. 生成 count 查询 SQL
3. 根据分页参数生成分页 SQL
4. 参数化处理,防止 SQL 注入
*/
}
/**
* 不同数据库的分页差异处理
*/
public void analyzeDifferentDatabasePagination() {
/*
MySQL: LIMIT #{offset}, #{pageSize}
PostgreSQL: LIMIT #{pageSize} OFFSET #{offset}
Oracle:
SELECT * FROM (
SELECT tmp.*, ROWNUM row_num FROM (
SELECT * FROM table ORDER BY id
) tmp WHERE ROWNUM <= #{end}
) WHERE row_num > #{start}
SQL Server: OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
*/
}
/**
* 模拟分页拦截器核心代码
*/
public class MockPaginationInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取方法参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 检查是否包含 IPage 参数
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (page != null) {
// 执行 count 查询
Long total = executeCountQuery(invocation, ms, parameter);
page.setTotal(total);
// 重写 SQL 添加分页条件
BoundSql boundSql = (BoundSql) args[5];
String originalSql = boundSql.getSql();
String paginationSql = buildPaginationSql(originalSql, page);
// 使用反射修改 BoundSql 的 SQL
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, paginationSql);
}
// 继续执行查询
return invocation.proceed();
}
private Long executeCountQuery(Invocation invocation, MappedStatement ms, Object parameter) {
// 创建 count 查询的 MappedStatement
MappedStatement countMs = createCountMappedStatement(ms);
// 执行 count 查询
// ... 具体实现
return 100L; // 模拟返回
}
private String buildPaginationSql(String originalSql, IPage<?> page) {
// 根据数据库类型构建分页 SQL
long offset = (page.getCurrent() - 1) * page.getSize();
return originalSql + " LIMIT " + offset + ", " + page.getSize();
}
}
}
4.6 分页性能优化详细方案
/**
* 分页性能优化详细方案
* 重点:企业级大数据量分页性能优化
*/
@Service
@Slf4j
public class PaginationOptimizationService {
@Autowired
private UserMapper userMapper;
/**
* 1. 游标分页(基于ID的连续分页)
* 适用场景:大数据量下的连续分页,如无限滚动
*/
public List<User> cursorPagination(Long lastId, Integer pageSize) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.gt(User::getId, lastId) // 从上一次最后一条记录的ID开始
.eq(User::getDeleted, 0)
.orderByAsc(User::getId) // 必须按ID排序
.last("LIMIT " + pageSize);
List<User> users = userMapper.selectList(queryWrapper);
log.info("游标分页查询 - 最后ID: {}, 页大小: {}, 结果数量: {}",
lastId, pageSize, users.size());
return users;
}
/**
* 2. 延迟关联分页优化
* 适用场景:多表关联查询的大数据量分页
*/
public Page<UserVO> delayedJoinPagination(Integer pageNum, Integer pageSize) {
// 第一步:先分页查询主表ID
Page<User> idPage = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> idWrapper = new LambdaQueryWrapper<>();
idWrapper.select(User::getId)
.eq(User::getDeleted, 0)
.orderByDesc(User::getCreateTime);
Page<User> idResult = userMapper.selectPage(idPage, idWrapper);
// 第二步:根据ID列表查询完整数据(包含关联信息)
List<Long> userIds = idResult.getRecords().stream()
.map(User::getId)
.collect(Collectors.toList());
if (userIds.isEmpty()) {
return new Page<>(pageNum, pageSize);
}
// 执行关联查询
List<UserVO> userVOs = userMapper.selectUserWithDeptByIds(userIds);
// 第三步:组装分页结果
Page<UserVO> resultPage = new Page<>(pageNum, pageSize);
resultPage.setRecords(userVOs);
resultPage.setTotal(idResult.getTotal());
resultPage.setPages(idResult.getPages());
log.info("延迟关联分页优化 - 页码: {}, 页大小: {}, 总记录数: {}",
pageNum, pageSize, resultPage.getTotal());
return resultPage;
}
/**
* 3. 覆盖索引分页优化
* 适用场景:只需要部分字段的分页查询
*/
public Page<Map<String, Object>> coverIndexPagination(Integer pageNum, Integer pageSize) {
Page<Map<String, Object>> page = new Page<>(pageNum, pageSize);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "user_name", "create_time") // 只查询需要的字段
.eq("deleted", 0)
.orderByDesc("create_time");
IPage<Map<String, Object>> resultPage = userMapper.selectMapsPage(page, queryWrapper);
log.info("覆盖索引分页优化 - 查询字段数量: 3, 总记录数: {}", resultPage.getTotal());
return (Page<Map<String, Object>>) resultPage;
}
/**
* 4. 子查询分页优化(大偏移量优化)
* 适用场景:深度分页性能优化
*/
public List<User> subqueryPagination(Integer pageNum, Integer pageSize) {
long offset = (long) (pageNum - 1) * pageSize;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.inSql("id",
"SELECT id FROM sys_user WHERE deleted = 0 ORDER BY id LIMIT " +
offset + ", " + pageSize)
.orderByAsc("id");
List<User> users = userMapper.selectList(queryWrapper);
log.info("子查询分页优化 - 偏移量: {}, 页大小: {}, 结果数量: {}",
offset, pageSize, users.size());
return users;
}
/**
* 5. 分批查询大数据量
* 适用场景:数据导出、批量处理等需要处理全量数据的场景
*/
public void batchProcessLargeData() {
int batchSize = 1000;
long totalCount = userMapper.selectCount(new QueryWrapper<User>().eq("deleted", 0));
long totalPages = (totalCount + batchSize - 1) / batchSize;
log.info("开始分批处理大数据量 - 总记录数: {}, 批次大小: {}, 总批次数: {}",
totalCount, batchSize, totalPages);
for (long pageNum = 1; pageNum <= totalPages; pageNum++) {
Page<User> page = new Page<>(pageNum, batchSize, false); // 不执行count查询
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0);
Page<User> batchPage = userMapper.selectPage(page, queryWrapper);
processUserBatch(batchPage.getRecords(), pageNum, totalPages);
}
log.info("分批处理大数据量完成");
}
private void processUserBatch(List<User> users, long currentPage, long totalPages) {
// 模拟处理逻辑
log.info("处理第 {}/{} 批次数据,本批数量: {}", currentPage, totalPages, users.size());
// 实际业务处理
for (User user : users) {
// 处理每个用户
processSingleUser(user);
}
}
private void processSingleUser(User user) {
// 单用户处理逻辑
}
/**
* 6. 缓存分页结果优化
* 适用场景:数据变化不频繁的分页查询
*/
@Cacheable(value = "userPage", key = "#pageNum + '-' + #pageSize + '-' + #userName")
public Page<User> cachedPagination(Integer pageNum, Integer pageSize, String userName) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getDeleted, 0)
.like(StringUtils.isNotBlank(userName), User::getUserName, userName)
.orderByDesc(User::getCreateTime);
Page<User> resultPage = userMapper.selectPage(page, queryWrapper);
log.info("执行分页查询并缓存 - 页码: {}, 页大小: {}, 用户名: {}",
pageNum, pageSize, userName);
return resultPage;
}
}
4.7 分页查询完整测试用例
/**
* 分页查询完整测试用例
* 重点:覆盖所有分页场景的测试
*/
@SpringBootTest
@Slf4j
class PaginationTest {
@Autowired
private UserMapper userMapper;
@Autowired
private PaginationBasicService paginationService;
@Autowired
private PaginationOptimizationService optimizationService;
@BeforeEach
void setUp() {
prepareTestData();
}
@Test
void testBasicPagination() {
// 测试基础分页
Page<User> page = paginationService.basicPagination(1, 10);
assertNotNull(page);
assertNotNull(page.getRecords());
assertTrue(page.getTotal() >= 0);
assertTrue(page.getPages() >= 0);
assertTrue(page.getRecords().size() <= 10);
log.info("基础分页测试通过 - 总记录数: {}, 总页数: {}", page.getTotal(), page.getPages());
}
@Test
void testPaginationWithoutCount() {
// 测试不进行count查询的分页
Page<User> page = paginationService.paginationWithoutCount(1, 10);
assertNotNull(page);
assertNotNull(page.getRecords());
assertEquals(0, page.getTotal()); // 不执行count查询,total为0
assertEquals(0, page.getPages()); // 不执行count查询,pages为0
assertTrue(page.getRecords().size() <= 10);
log.info("不分页查询测试通过 - 当前页记录数: {}", page.getRecords().size());
}
@Test
void testLambdaPagination() {
// 测试Lambda分页查询
Page<User> page = paginationService.lambdaPagination(1, 10, "测试");
assertNotNull(page);
assertNotNull(page.getRecords());
// 验证查询结果都包含"测试"
for (User user : page.getRecords()) {
assertTrue(user.getUserName().contains("测试"));
}
log.info("Lambda分页测试通过 - 结果数量: {}", page.getRecords().size());
}
@Test
void testCursorPagination() {
// 测试游标分页
Long lastId = 0L;
Integer pageSize = 5;
List<User> users = optimizationService.cursorPagination(lastId, pageSize);
assertNotNull(users);
assertTrue(users.size() <= pageSize);
// 验证ID是递增的
for (int i = 1; i < users.size(); i++) {
assertTrue(users.get(i).getId() > users.get(i-1).getId());
}
log.info("游标分页测试通过 - 结果数量: {}", users.size());
}
@Test
void testDelayedJoinPagination() {
// 测试延迟关联分页
Page<UserVO> page = optimizationService.delayedJoinPagination(1, 10);
assertNotNull(page);
assertNotNull(page.getRecords());
// 验证关联数据
for (UserVO userVO : page.getRecords()) {
assertNotNull(userVO.getDeptName());
}
log.info("延迟关联分页测试通过 - 结果数量: {}", page.getRecords().size());
}
@Test
void testCoverIndexPagination() {
// 测试覆盖索引分页
Page<Map<String, Object>> page = optimizationService.coverIndexPagination(1, 10);
assertNotNull(page);
assertNotNull(page.getRecords());
// 验证只返回指定字段
for (Map<String, Object> record : page.getRecords()) {
assertTrue(record.containsKey("id"));
assertTrue(record.containsKey("user_name"));
assertTrue(record.containsKey("create_time"));
assertFalse(record.containsKey("email")); // 未选择的字段
}
log.info("覆盖索引分页测试通过 - 结果数量: {}", page.getRecords().size());
}
@Test
void testBatchProcessLargeData() {
// 测试分批处理
assertDoesNotThrow(() -> optimizationService.batchProcessLargeData());
log.info("分批处理大数据量测试通过");
}
@Test
void testPageResultWrapper() {
// 测试分页结果封装
Page<User> page = paginationService.basicPagination(1, 10);
PageResult<User> pageResult = PageResult.success(page);
assertNotNull(pageResult);
assertEquals(200, pageResult.getCode().intValue());
assertEquals("success", pageResult.getMessage());
assertNotNull(pageResult.getData());
assertNotNull(pageResult.getTimestamp());
log.info("分页结果封装测试通过");
}
private void prepareTestData() {
// 清理测试数据
userMapper.delete(new QueryWrapper<User>().like("user_name", "测试用户"));
// 插入测试数据
List<User> testUsers = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
User user = new User();
user.setUserName("测试用户" + i);
user.setAge(20 + i % 30);
user.setEmail("test" + i + "@example.com");
user.setDeptId((long) (i % 3 + 1));
testUsers.add(user);
}
for (User user : testUsers) {
userMapper.insert(user);
}
log.info("分页测试数据准备完成,共插入 {} 条记录", testUsers.size());
}
}
4.8 企业级分页避坑方案
/**
* 分页功能企业级避坑方案
* 重点:生产环境中分页查询的常见问题和解决方案
*/
@Service
@Slf4j
public class PaginationBestPractice {
@Autowired
private UserMapper userMapper;
/**
* 坑点1:深度分页性能问题
*/
public void avoidDeepPagination() {
int pageNum = 10000;
int pageSize = 10;
// 错误做法:直接大偏移量分页
Page<User> wrongPage = new Page<>(pageNum, pageSize);
QueryWrapper<User> wrongWrapper = new QueryWrapper<>();
Page<User> wrongResult = userMapper.selectPage(wrongPage, wrongWrapper); // 性能极差
// 正确做法1:使用游标分页
Long lastId = getLastIdFromPreviousPage(); // 获取上一页最后一条记录的ID
List<User> users = userMapper.selectList(new QueryWrapper<User>()
.gt("id", lastId)
.orderByAsc("id")
.last("LIMIT " + pageSize));
// 正确做法2:使用子查询优化
List<User> optimizedUsers = userMapper.selectList(new QueryWrapper<User>()
.inSql("id",
"SELECT id FROM sys_user ORDER BY id LIMIT " +
((pageNum - 1) * pageSize) + ", " + pageSize))
.orderByAsc("id"));
}
/**
* 坑点2:不必要的 count 查询
*/
public void avoidUnnecessaryCount() {
// 场景1:只需要判断是否有数据,不需要知道具体数量
Page<User> page = new Page<>(1, 1, false); // 不执行count查询
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0);
Page<User> userPage = userMapper.selectPage(page, queryWrapper);
boolean hasData = !userPage.getRecords().isEmpty(); // 只需要知道是否有数据
// 场景2:移动端下拉刷新,通常只需要第一页数据
Page<User> firstPage = new Page<>(1, 20, false); // 不关心总数
Page<User> result = userMapper.selectPage(firstPage, queryWrapper);
}
/**
* 坑点3:分页参数安全验证
*/
public Page<User> safePagination(Integer pageNum, Integer pageSize, String sortBy) {
// 参数验证和默认值设置
if (pageNum == null || pageNum < 1) {
pageNum = 1;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10;
}
// 限制最大页大小,防止恶意查询
if (pageSize > 1000) {
pageSize = 1000;
log.warn("页大小超过限制,自动设置为最大值: {}", pageSize);
}
// 排序字段白名单验证
Set<String> allowedSortFields = new HashSet<>(
Arrays.asList("id", "user_name", "age", "create_time", "update_time"));
Page<User> page = new Page<>(pageNum, pageSize);
if (StringUtils.isNotBlank(sortBy) && allowedSortFields.contains(sortBy)) {
page.addOrder(OrderItem.desc(sortBy));
} else {
page.addOrder(OrderItem.desc("create_time")); // 默认排序
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0);
return userMapper.selectPage(page, queryWrapper);
}
/**
* 坑点4:分布式环境下的分页一致性问题
*/
public void handleDistributedPagination() {
// 问题:在分页查询过程中,如果有数据插入/删除,可能导致数据重复或丢失
// 解决方案1:使用基于ID的游标分页,避免偏移量分页
Long lastId = getLastIdFromPreviousPage();
List<User> users = userMapper.selectList(new QueryWrapper<User>()
.gt("id", lastId)
.orderByAsc("id")
.last("LIMIT 10"));
// 解决方案2:使用事务隔离级别保证一致性
// @Transactional(isolation = Isolation.REPEATABLE_READ)
// public Page<User> consistentPagination() { ... }
}
/**
* 坑点5:内存分页的风险
*/
public void avoidMemoryPagination() {
// 错误做法:先查询所有数据,再在内存中分页
List<User> allUsers = userMapper.selectList(new QueryWrapper<User>().eq("deleted", 0));
List<User> pageUsers = allUsers.stream()
.skip(10) // 内存分页
.limit(10)
.collect(Collectors.toList()); // 数据量大时会导致OOM
// 正确做法:使用数据库分页
Page<User> page = new Page<>(2, 10); // 第二页,每页10条
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0);
Page<User> result = userMapper.selectPage(page, queryWrapper); // 数据库分页
}
/**
* 坑点6:count查询性能优化
*/
public void optimizeCountQuery() {
// 对于复杂查询,count查询可能很慢
// 方案1:使用近似值(如Redis计数器)
// Long approximateCount = redisTemplate.opsForValue().get("user_count");
// 方案2:定期统计,缓存count结果
// @Cacheable("user_count")
// public Long getCachedUserCount() { ... }
// 方案3:对于不需要精确count的场景,使用估算值
Page<User> page = new Page<>(1, 10, false); // 不执行count查询
// 或者使用 EXPLAIN 获取近似值
}
/**
* 最佳实践:统一的分页查询模板
*/
public <T> PageResult<T> executePageQuery(PageQueryFunction<T> queryFunction,
BasePageQuery query) {
try {
// 1. 参数验证
validatePageParams(query);
// 2. 执行分页查询
Page<T> page = queryFunction.apply(query.toPage());
// 3. 返回统一结果
return PageResult.success(page);
} catch (Exception e) {
log.error("分页查询异常 - 参数: {}, 错误: {}", query, e.getMessage(), e);
return PageResult.error("分页查询失败: " + e.getMessage());
}
}
private void validatePageParams(BasePageQuery query) {
if (query.getPageNum() == null || query.getPageNum() < 1) {
query.setPageNum(1);
}
if (query.getPageSize() == null || query.getPageSize() < 1) {
query.setPageSize(10);
}
if (query.getPageSize() > 1000) {
log.warn("页大小过大,自动限制为1000 - 原始值: {}", query.getPageSize());
query.setPageSize(1000);
}
}
@FunctionalInterface
public interface PageQueryFunction<T> {
Page<T> apply(Page<T> page);
}
private Long getLastIdFromPreviousPage() {
// 实际项目中应该从上一页响应中获取
return 0L;
}
}
企业级MyBatis-Plus实战教学(续)
模块五:代码生成器实战配置
5.1 代码生成器核心配置详解
/**
* 企业级代码生成器详细配置
* 重点:完整的代码生成解决方案,支持自定义模板和扩展
*/
@Slf4j
public class EnterpriseCodeGenerator {
/**
* 代码生成器核心配置
* 重点:详细参数说明和企业级最佳实践
*/
public static void main(String[] args) {
// 1. 项目根路径获取
String projectPath = System.getProperty("user.dir");
log.info("项目根路径: {}", projectPath);
// 2. 数据源详细配置
String url = "jdbc:mysql://localhost:3306/enterprise_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true";
String username = "root";
String password = "123456";
log.info("数据源配置 - URL: {}, 用户名: {}", url, username);
// 3. 使用 FastAutoGenerator 进行代码生成
FastAutoGenerator.create(url, username, password)
// ==================== 全局配置 ====================
.globalConfig(builder -> {
builder.author("EnterpriseDev") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.disableOpenDir() // 生成后不打开目录
.dateType(DateType.ONLY_DATE) // 时间策略
.commentDate("yyyy-MM-dd") // 注释日期
.outputDir(projectPath + "/src/main/java") // 输出目录
.build();
log.info("全局配置完成 - 作者: EnterpriseDev, 输出目录: {}",
projectPath + "/src/main/java");
})
// ==================== 包配置 ====================
.packageConfig(builder -> {
builder.parent("com.enterprise") // 父包名
.moduleName("system") // 模块名
.entity("domain.entity") // 实体类包名
.service("service") // Service包名
.serviceImpl("service.impl") // Service实现类包名
.mapper("mapper") // Mapper包名
.controller("controller") // Controller包名
.other("domain.dto") // 其他包名(如DTO)
.pathInfo(Collections.singletonMap(
OutputFile.xml,
projectPath + "/src/main/resources/mapper"
)) // XML输出路径
.build();
log.info("包配置完成 - 父包: com.enterprise, 模块: system");
})
// ==================== 策略配置 ====================
.strategyConfig(builder -> {
// 3.1 数据库表配置
builder.addInclude(getTables("sys_user,sys_role,sys_dept,sys_menu")) // 设置需要生成的表名
.addTablePrefix("sys_") // 设置过滤表前缀
.build();
// 3.2 实体类策略配置
builder.entityBuilder()
.enableLombok() // 开启 lombok
.enableTableFieldAnnotation() // 开启字段注解
.naming(NamingStrategy.underline_to_camel) // 表名映射策略
.columnNaming(NamingStrategy.underline_to_camel) // 列名映射策略
.idType(IdType.ASSIGN_ID) // 主键策略
.formatFileName("%sEntity") // 实体类文件名称格式
.addTableFills(
new Column("create_time", FieldFill.INSERT),
new Column("update_time", FieldFill.INSERT_UPDATE)
) // 自动填充配置
.versionColumnName("version") // 乐观锁字段名
.versionPropertyName("version") // 乐观锁属性名
.logicDeleteColumnName("deleted") // 逻辑删除字段名
.logicDeletePropertyName("deleted") // 逻辑删除属性名
.addIgnoreColumns("tenant_id") // 忽略字段
.build();
// 3.3 Controller 策略配置
builder.controllerBuilder()
.enableHyphenStyle() // 开启驼峰转连字符
.enableRestStyle() // 开启生成 @RestController
.formatFileName("%sController") // 控制器文件名称格式
.build();
// 3.4 Service 策略配置
builder.serviceBuilder()
.formatServiceFileName("%sService") // Service接口名称格式
.formatServiceImplFileName("%sServiceImpl") // Service实现类名称格式
.build();
// 3.5 Mapper 策略配置
builder.mapperBuilder()
.enableBaseResultMap() // 启用 BaseResultMap
.enableBaseColumnList() // 启用 BaseColumnList
.formatMapperFileName("%sMapper") // Mapper接口名称格式
.formatXmlFileName("%sMapper") // Mapper XML名称格式
.build();
log.info("策略配置完成 - 包含表: {}, 前缀过滤: sys_",
getTables("sys_user,sys_role,sys_dept,sys_menu"));
})
// ==================== 模板配置 ====================
.templateConfig(builder -> {
builder.entity("/templates/entity.java") // 实体类模板
.service("/templates/service.java") // Service接口模板
.serviceImpl("/templates/serviceImpl.java") // Service实现类模板
.mapper("/templates/mapper.java") // Mapper接口模板
.mapperXml("/templates/mapper.xml") // Mapper XML模板
.controller("/templates/controller.java") // Controller模板
.build();
log.info("模板配置完成 - 使用自定义模板");
})
// ==================== 注入配置 ====================
.injectionConfig(builder -> {
// 4.1 自定义参数注入
Map<String, Object> customMap = new HashMap<>();
customMap.put("company", "Enterprise Ltd.");
customMap.put("projectVersion", "1.0.0");
customMap.put("basePackage", "com.enterprise");
builder.customMap(customMap);
// 4.2 自定义文件生成
Map<String, String> customFiles = new HashMap<>();
customFiles.put("DTO.java", "/templates/entityDTO.java.ftl");
customFiles.put("VO.java", "/templates/entityVO.java.ftl");
customFiles.put("Query.java", "/templates/entityQuery.java.ftl");
builder.customFile(customFiles);
// 4.3 文件生成前回调
builder.beforeOutputFile((tableInfo, objectMap) -> {
log.info("开始生成表: {}, 实体类: {}",
tableInfo.getName(), tableInfo.getEntityName());
// 添加表注释到自定义参数
objectMap.put("tableComment", tableInfo.getComment());
// 根据表名设置模块名
String moduleName = getModuleName(tableInfo.getName());
objectMap.put("moduleName", moduleName);
});
log.info("注入配置完成 - 自定义参数: {}, 自定义文件: {}",
customMap.keySet(), customFiles.keySet());
})
// ==================== 模板引擎配置 ====================
.templateEngine(new FreemarkerTemplateEngine())
.execute();
log.info("代码生成完成!");
}
/**
* 处理表名字符串,支持逗号分隔
*/
private static List<String> getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}
/**
* 根据表名获取模块名
*/
private static String getModuleName(String tableName) {
if (tableName.startsWith("sys_")) {
return "system";
} else if (tableName.startsWith("biz_")) {
return "business";
} else if (tableName.startsWith("org_")) {
return "organization";
}
return "common";
}
}
5.2 自定义模板详细实现
/**
* 自定义模板配置
* 重点:企业级定制化模板,符合项目规范
*/
public class CustomTemplateConfig {
/**
* 1. 实体类自定义模板
* 位置:resources/templates/entity.java.ftl
*/
public static final String ENTITY_TEMPLATE =
"/**\n" +
" * <p>\n" +
" * ${table.comment!} 实体类\n" +
" * </p>\n" +
" *\n" +
" * @author ${author}\n" +
" * @since ${date}\n" +
" * @company ${company!\"\"}\n" +
" * @version ${projectVersion!\"1.0.0\"}\n" +
" */\n" +
"@Data\n" +
"@TableName(\"${table.name}\")\n" +
"@ApiModel(value = \"${entity}对象\", description = \"${table.comment!}\")\n" +
"@EqualsAndHashCode(callSuper = false)\n" +
"@Accessors(chain = true)\n" +
"public class ${entity} implements Serializable {\n" +
"\n" +
" private static final long serialVersionUID = 1L;\n" +
"\n" +
"<#list table.fields as field>\n" +
" <#if field.comment!?length gt 0>\n" +
" @ApiModelProperty(value = \"${field.comment}\")\n" +
" </#if>\n" +
" <#if field.keyFlag>\n" +
" <#if field.keyIdentityFlag>\n" +
" @TableId(value = \"${field.name}\", type = IdType.AUTO)\n" +
" <#elseif idType??>\n" +
" @TableId(value = \"${field.name}\", type = IdType.${idType})\n" +
" <#else>\n" +
" @TableId(\"${field.name}\")\n" +
" </#if>\n" +
" <#elseif field.fill??>\n" +
" <#if field.convert>\n" +
" @TableField(value = \"${field.name}\", fill = FieldFill.${field.fill})\n" +
" <#else>\n" +
" @TableField(fill = FieldFill.${field.fill})\n" +
" </#if>\n" +
" <#elseif field.convert>\n" +
" @TableField(\"${field.name}\")\n" +
" </#if>\n" +
" <#if field.versionField>\n" +
" @Version\n" +
" </#if>\n" +
" <#if field.logicDeleteField>\n" +
" @TableLogic\n" +
" </#if>\n" +
" private ${field.propertyType} ${field.propertyName};\n" +
"</#list>\n" +
"\n" +
"<#if !entityLombokModel>\n" +
" <#list table.fields as field>\n" +
" <#if field.propertyType == \"boolean\">\n" +
" <#assign getprefix=\"is\"/>\n" +
" <#else>\n" +
" <#assign getprefix=\"get\"/>\n" +
" </#if>\n" +
" public ${field.propertyType} ${getprefix}${field.capitalName}() {\n" +
" return ${field.propertyName};\n" +
" }\n" +
"\n" +
" public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {\n" +
" this.${field.propertyName} = ${field.propertyName};\n" +
" return this;\n" +
" }\n" +
" </#list>\n" +
"</#if>\n" +
"}";
/**
* 2. Service接口自定义模板
* 位置:resources/templates/service.java.ftl
*/
public static final String SERVICE_TEMPLATE =
"/**\n" +
" * <p>\n" +
" * ${table.comment!} 服务接口\n" +
" * </p>\n" +
" *\n" +
" * @author ${author}\n" +
" * @since ${date}\n" +
" */\n" +
"public interface ${table.serviceName} extends IService<${entity}> {\n" +
"\n" +
" /**\n" +
" * 根据ID查询${table.comment!}\n" +
" */\n" +
" ${entity} get${entity}ById(Long id);\n" +
"\n" +
" /**\n" +
" * 分页查询${table.comment!}\n" +
" */\n" +
" Page<${entity}> get${entity}Page(${entity}Query query);\n" +
"\n" +
" /**\n" +
" * 创建${table.comment!}\n" +
" */\n" +
" boolean create${entity}(${entity}DTO dto);\n" +
"\n" +
" /**\n" +
" * 更新${table.comment!}\n" +
" */\n" +
" boolean update${entity}(${entity}DTO dto);\n" +
"\n" +
" /**\n" +
" * 删除${table.comment!}\n" +
" */\n" +
" boolean delete${entity}(Long id);\n" +
"}";
/**
* 3. Controller自定义模板
* 位置:resources/templates/controller.java.ftl
*/
public static final String CONTROLLER_TEMPLATE =
"/**\n" +
" * <p>\n" +
" * ${table.comment!} 前端控制器\n" +
" * </p>\n" +
" *\n" +
" * @author ${author}\n" +
" * @since ${date}\n" +
" */\n" +
"@Slf4j\n" +
"@RestController\n" +
"@RequestMapping(\"/api/${moduleName}/${table.entityPath}\")\n" +
"@Api(tags = \"${table.comment!}管理\")\n" +
"public class ${table.controllerName} {\n" +
"\n" +
" @Autowired\n" +
" private ${table.serviceName} ${table.serviceName?uncap_first};\n" +
"\n" +
" @GetMapping(\"/{id}\")\n" +
" @ApiOperation(\"根据ID查询${table.comment!}\")\n" +
" public ApiResult<${entity}VO> getById(@PathVariable Long id) {\n" +
" try {\n" +
" ${entity} entity = ${table.serviceName?uncap_first}.getById(id);\n" +
" if (entity == null) {\n" +
" return ApiResult.fail(\"${table.comment!}不存在\");\n" +
" }\n" +
" ${entity}VO vo = ${entity}Converter.INSTANCE.toVO(entity);\n" +
" return ApiResult.success(vo);\n" +
" } catch (Exception e) {\n" +
" log.error(\"查询${table.comment!}失败: {}\", e.getMessage(), e);\n" +
" return ApiResult.fail(\"查询失败\");\n" +
" }\n" +
" }\n" +
"\n" +
" @PostMapping(\"/page\")\n" +
" @ApiOperation(\"分页查询${table.comment!}\")\n" +
" public ApiResult<Page<${entity}VO>> getPage(@RequestBody ${entity}Query query) {\n" +
" try {\n" +
" Page<${entity}> page = ${table.serviceName?uncap_first}.get${entity}Page(query);\n" +
" Page<${entity}VO> voPage = ${entity}Converter.INSTANCE.toVOPage(page);\n" +
" return ApiResult.success(voPage);\n" +
" } catch (Exception e) {\n" +
" log.error(\"分页查询${table.comment!}失败: {}\", e.getMessage(), e);\n" +
" return ApiResult.fail(\"分页查询失败\");\n" +
" }\n" +
" }\n" +
"\n" +
" @PostMapping\n" +
" @ApiOperation(\"创建${table.comment!}\")\n" +
" public ApiResult<String> create(@RequestBody ${entity}DTO dto) {\n" +
" try {\n" +
" boolean success = ${table.serviceName?uncap_first}.create${entity}(dto);\n" +
" return success ? ApiResult.success(\"创建成功\") : ApiResult.fail(\"创建失败\");\n" +
" } catch (Exception e) {\n" +
" log.error(\"创建${table.comment!}失败: {}\", e.getMessage(), e);\n" +
" return ApiResult.fail(\"创建失败: \" + e.getMessage());\n" +
" }\n" +
" }\n" +
"\n" +
" @PutMapping\n" +
" @ApiOperation(\"更新${table.comment!}\")\n" +
" public ApiResult<String> update(@RequestBody ${entity}DTO dto) {\n" +
" try {\n" +
" boolean success = ${table.serviceName?uncap_first}.update${entity}(dto);\n" +
" return success ? ApiResult.success(\"更新成功\") : ApiResult.fail(\"更新失败\");\n" +
" } catch (Exception e) {\n" +
" log.error(\"更新${table.comment!}失败: {}\", e.getMessage(), e);\n" +
" return ApiResult.fail(\"更新失败: \" + e.getMessage());\n" +
" }\n" +
" }\n" +
"\n" +
" @DeleteMapping(\"/{id}\")\n" +
" @ApiOperation(\"删除${table.comment!}\")\n" +
" public ApiResult<String> delete(@PathVariable Long id) {\n" +
" try {\n" +
" boolean success = ${table.serviceName?uncap_first}.delete${entity}(id);\n" +
" return success ? ApiResult.success(\"删除成功\") : ApiResult.fail(\"删除失败\");\n" +
" } catch (Exception e) {\n" +
" log.error(\"删除${table.comment!}失败: {}\", e.getMessage(), e);\n" +
" return ApiResult.fail(\"删除失败: \" + e.getMessage());\n" +
" }\n" +
" }\n" +
"}";
/**
* 4. DTO类模板
* 位置:resources/templates/entityDTO.java.ftl
*/
public static final String DTO_TEMPLATE =
"/**\n" +
" * <p>\n" +
" * ${table.comment!} DTO\n" +
" * </p>\n" +
" *\n" +
" * @author ${author}\n" +
" * @since ${date}\n" +
" */\n" +
"@Data\n" +
"@ApiModel(value = \"${entity}DTO对象\", description = \"${table.comment!}传输对象\")\n" +
"@EqualsAndHashCode(callSuper = false)\n" +
"public class ${entity}DTO implements Serializable {\n" +
"\n" +
" private static final long serialVersionUID = 1L;\n" +
"\n" +
"<#list table.fields as field>\n" +
" <#if field.propertyName != \"id\" && \n" +
" field.propertyName != \"version\" && \n" +
" field.propertyName != \"deleted\" && \n" +
" field.propertyName != \"createTime\" && \n" +
" field.propertyName != \"updateTime\" && \n" +
" field.propertyName != \"createBy\" && \n" +
" field.propertyName != \"updateBy\">\n" +
" @ApiModelProperty(value = \"${field.comment}\")\n" +
" <#if field.propertyType == \"String\">\n" +
" @NotBlank(message = \"${field.comment}不能为空\")\n" +
" <#else>\n" +
" @NotNull(message = \"${field.comment}不能为空\")\n" +
" </#if>\n" +
" private ${field.propertyType} ${field.propertyName};\n" +
" </#if>\n" +
"</#list>\n" +
"}";
}
5.3 高级代码生成器配置
/**
* 高级代码生成器配置
* 重点:支持多数据源、自定义规则、批量生成等高级功能
*/
@Slf4j
public class AdvancedCodeGenerator {
/**
* 多数据源代码生成
*/
public static void generateFromMultipleDataSource() {
// 数据源列表
List<DataSourceConfig> dataSources = Arrays.asList(
new DataSourceConfig("jdbc:mysql://localhost:3306/enterprise_system", "root", "123456"),
new DataSourceConfig("jdbc:mysql://localhost:3306/enterprise_business", "root", "123456"),
new DataSourceConfig("jdbc:mysql://localhost:3306/enterprise_org", "root", "123456")
);
for (DataSourceConfig dataSource : dataSources) {
log.info("开始从数据源生成代码: {}", dataSource.getUrl());
String databaseName = extractDatabaseName(dataSource.getUrl());
generateForDataSource(dataSource, databaseName);
}
}
/**
* 为单个数据源生成代码
*/
private static void generateForDataSource(DataSourceConfig dataSource, String databaseName) {
String projectPath = System.getProperty("user.dir");
FastAutoGenerator.create(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword())
.globalConfig(builder -> {
builder.author("EnterpriseDev")
.outputDir(projectPath + "/src/main/java")
.fileOverride()
.build();
})
.packageConfig(builder -> {
builder.parent("com.enterprise." + databaseName)
.entity("domain.entity")
.service("service")
.serviceImpl("service.impl")
.mapper("mapper")
.controller("controller")
.pathInfo(Collections.singletonMap(
OutputFile.xml,
projectPath + "/src/main/resources/mapper/" + databaseName
))
.build();
})
.strategyConfig(builder -> {
builder.addInclude(getAllTables(dataSource))
.addTablePrefix(databaseName + "_")
.entityBuilder()
.enableLombok()
.enableTableFieldAnnotation()
.controllerBuilder()
.enableRestStyle()
.build();
})
.templateEngine(new FreemarkerTemplateEngine())
.execute();
log.info("数据源 {} 代码生成完成", databaseName);
}
/**
* 条件代码生成 - 只生成指定的表结构
*/
public static void conditionalGeneration() {
String projectPath = System.getProperty("user.dir");
FastAutoGenerator.create(
"jdbc:mysql://localhost:3306/enterprise_db", "root", "123456")
.globalConfig(builder -> {
builder.author("EnterpriseDev")
.outputDir(projectPath + "/src/main/java")
.build();
})
.packageConfig(builder -> {
builder.parent("com.enterprise")
.build();
})
.strategyConfig(builder -> {
// 只生成包含特定字段的表
builder.addInclude(getTablesWithSpecificColumns())
.entityBuilder()
.enableLombok()
.addIgnoreColumns("tenant_id") // 忽略租户字段
.build();
})
.injectionConfig(builder -> {
builder.beforeOutputFile((tableInfo, objectMap) -> {
// 根据表注释决定是否生成
if (shouldGenerateTable(tableInfo)) {
log.info("生成表: {}", tableInfo.getName());
} else {
// 跳过该表
throw new RuntimeException("跳过表: " + tableInfo.getName());
}
});
})
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
/**
* 增量代码生成 - 只生成新增的表
*/
public static void incrementalGeneration() {
String projectPath = System.getProperty("user.dir");
Set<String> existingTables = getExistingGeneratedTables(projectPath);
FastAutoGenerator.create(
"jdbc:mysql://localhost:3306/enterprise_db", "root", "123456")
.globalConfig(builder -> {
builder.author("EnterpriseDev")
.outputDir(projectPath + "/src/main/java")
.build();
})
.packageConfig(builder -> {
builder.parent("com.enterprise")
.build();
})
.strategyConfig(builder -> {
builder.addInclude(getNewTables(existingTables))
.entityBuilder()
.enableLombok()
.build();
})
.injectionConfig(builder -> {
builder.beforeOutputFile((tableInfo, objectMap) -> {
log.info("增量生成表: {}", tableInfo.getName());
});
})
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
// ==================== 工具方法 ====================
private static String extractDatabaseName(String url) {
// 从JDBC URL中提取数据库名
Pattern pattern = Pattern.compile("jdbc:mysql://[^/]+/([^?]+)");
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
return matcher.group(1);
}
return "unknown";
}
private static List<String> getAllTables(DataSourceConfig dataSource) {
// 获取数据源中的所有表
// 实际实现需要连接数据库查询
return Collections.emptyList();
}
private static List<String> getTablesWithSpecificColumns() {
// 获取包含特定字段的表
// 实际实现需要连接数据库查询
return Arrays.asList("sys_user", "sys_role");
}
private static boolean shouldGenerateTable(TableInfo tableInfo) {
// 根据表信息决定是否生成代码
return !tableInfo.getName().startsWith("tmp_") &&
!tableInfo.getName().endsWith("_bak");
}
private static Set<String> getExistingGeneratedTables(String projectPath) {
// 获取已生成代码的表
Set<String> tables = new HashSet<>();
File entityDir = new File(projectPath + "/src/main/java/com/enterprise/domain/entity");
if (entityDir.exists()) {
File[] entityFiles = entityDir.listFiles((dir, name) -> name.endsWith("Entity.java"));
if (entityFiles != null) {
for (File file : entityFiles) {
String tableName = convertEntityNameToTableName(file.getName());
tables.add(tableName);
}
}
}
return tables;
}
private static List<String> getNewTables(Set<String> existingTables) {
// 获取新增的表(数据库中存在但代码中不存在的表)
// 实际实现需要连接数据库查询并与existingTables比较
return Collections.emptyList();
}
private static String convertEntityNameToTableName(String entityFileName) {
// 将实体类文件名转换为表名
return entityFileName.replace("Entity.java", "")
.replaceAll("([a-z])([A-Z])", "$1_$2")
.toLowerCase();
}
/**
* 数据源配置内部类
*/
@Data
@AllArgsConstructor
private static class DataSourceConfig {
private String url;
private String username;
private String password;
}
}
5.4 代码生成器底层原理深度解析
/**
* 代码生成器底层原理分析
* 重点:理解代码生成器的工作机制和扩展点
*/
@Slf4j
public class CodeGeneratorPrincipleAnalysis {
/**
* 1. 代码生成器核心流程分析
*/
public void analyzeGeneratorWorkflow() {
/*
代码生成器工作流程:
1. 初始化配置
- 全局配置(GlobalConfig)
- 包配置(PackageConfig)
- 策略配置(StrategyConfig)
- 模板配置(TemplateConfig)
- 注入配置(InjectionConfig)
2. 数据源连接和元数据获取
- 连接数据库
- 获取表信息(TableInfo)
- 获取字段信息(TableField)
3. 模板引擎处理
- 加载模板文件
- 绑定数据模型
- 渲染模板生成代码
4. 文件输出
- 创建目录结构
- 写入生成的文件
*/
}
/**
* 2. 表信息解析过程
*/
public void analyzeTableInfoParsing() {
/*
TableInfo 解析过程:
1. 从数据库元数据获取表基本信息
- 表名
- 表注释
- 引擎类型
- 字符集
2. 解析字段信息
- 字段名 -> 属性名(驼峰转换)
- 字段类型 -> Java类型
- 字段注释 -> 属性注释
- 主键识别
- 自增识别
3. 应用策略配置
- 表前缀过滤
- 字段忽略
- 命名策略应用
*/
}
/**
* 3. 自定义代码生成器示例
*/
public static class CustomCodeGenerator {
/**
* 自定义表信息处理
*/
public TableInfo customizeTableInfo(TableInfo tableInfo) {
// 添加自定义字段
TableField customField = new TableField();
customField.setColumnName("tenant_id");
customField.setPropertyName("tenantId");
customField.setPropertyType("Long");
customField.setComment("租户ID");
customField.setKeyFlag(false);
customField.setKeyIdentityFlag(false);
tableInfo.getFields().add(customField);
// 修改表注释
if (tableInfo.getComment() == null || tableInfo.getComment().isEmpty()) {
tableInfo.setComment("系统表 - " + tableInfo.getName());
}
return tableInfo;
}
/**
* 自定义文件生成逻辑
*/
public void generateCustomFiles(TableInfo tableInfo, Map<String, Object> objectMap) {
String projectPath = System.getProperty("user.dir");
// 生成Converter类
generateConverterClass(tableInfo, objectMap, projectPath);
// 生成VO类
generateVOClass(tableInfo, objectMap, projectPath);
// 生成Query类
generateQueryClass(tableInfo, objectMap, projectPath);
}
private void generateConverterClass(TableInfo tableInfo, Map<String, Object> objectMap, String projectPath) {
try {
String converterTemplate = loadTemplate("/templates/converter.java.ftl");
String rendered = renderTemplate(converterTemplate, objectMap);
String filePath = projectPath + "/src/main/java/com/enterprise/domain/converter/"
+ tableInfo.getEntityName() + "Converter.java";
writeToFile(filePath, rendered);
log.info("生成Converter类: {}", filePath);
} catch (Exception e) {
log.error("生成Converter类失败: {}", e.getMessage(), e);
}
}
private void generateVOClass(TableInfo tableInfo, Map<String, Object> objectMap, String projectPath) {
// 类似生成Converter的逻辑
}
private void generateQueryClass(TableInfo tableInfo, Map<String, Object> objectMap, String projectPath) {
// 类似生成Converter的逻辑
}
private String loadTemplate(String templatePath) {
// 加载模板文件
return "";
}
private String renderTemplate(String template, Map<String, Object> dataModel) {
// 渲染模板
return "";
}
private void writeToFile(String filePath, String content) {
// 写入文件
}
}
/**
* 4. 模板引擎原理分析
*/
public void analyzeTemplateEngine() {
/*
模板引擎工作流程(以Freemarker为例):
1. 模板加载
- 从classpath或文件系统加载模板文件
- 解析模板语法
2. 数据模型绑定
- TableInfo -> 表信息
- GlobalConfig -> 全局配置
- PackageConfig -> 包配置
- 自定义参数
3. 模板渲染
- 处理条件语句(<#if>)
- 处理循环语句(<#list>)
- 处理变量替换(${})
4. 输出生成
- 生成Java源代码
- 生成XML配置文件
*/
}
/**
* 5. 扩展点分析
*/
public void analyzeExtensionPoints() {
/*
代码生成器主要扩展点:
1. IFileCreate - 文件创建策略
- 控制是否覆盖已存在文件
- 自定义文件创建逻辑
2. ITemplate - 自定义模板
- 完全自定义模板内容
- 支持多种模板引擎
3. InjectionConfig - 注入配置
- 自定义文件生成
- 自定义参数注入
- 生成前后回调
4. DataSourceConfig - 数据源配置
- 支持多种数据库
- 自定义连接池
5. StrategyConfig - 策略配置
- 表过滤策略
- 字段过滤策略
- 命名策略
*/
}
}
5.5 企业级代码生成器最佳实践
/**
* 企业级代码生成器最佳实践
* 重点:生产环境中代码生成器的使用规范和注意事项
*/
@Slf4j
public class CodeGeneratorBestPractice {
/**
* 最佳实践1:配置文件外置
* 将生成配置放在配置文件中,便于管理和版本控制
*/
@Component
public static class ConfigFileCodeGenerator {
@Value("${code.generator.datasource.url}")
private String dataSourceUrl;
@Value("${code.generator.datasource.username}")
private String dataSourceUsername;
@Value("${code.generator.datasource.password}")
private String dataSourcePassword;
@Value("${code.generator.author:System}")
private String author;
@Value("${code.generator.base-package:com.enterprise}")
private String basePackage;
@Value("${code.generator.tables:all}")
private String tables;
public void generateFromConfig() {
log.info("从配置文件加载代码生成配置");
FastAutoGenerator.create(dataSourceUrl, dataSourceUsername, dataSourcePassword)
.globalConfig(builder -> {
builder.author(author)
.outputDir(System.getProperty("user.dir") + "/src/main/java")
.fileOverride(false) // 生产环境谨慎使用覆盖
.build();
})
.packageConfig(builder -> {
builder.parent(basePackage)
.build();
})
.strategyConfig(builder -> {
if (!"all".equals(tables)) {
builder.addInclude(Arrays.asList(tables.split(",")));
}
builder.entityBuilder()
.enableLombok()
.enableTableFieldAnnotation()
.build();
})
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}
/**
* 最佳实践2:版本控制集成
* 生成代码前检查版本,避免覆盖手动修改
*/
public static class VersionControlAwareGenerator {
public void generateWithVersionCheck() {
String projectPath = System.getProperty("user.dir");
// 检查Git状态
if (hasUncommittedChanges(projectPath)) {
log.warn("存在未提交的更改,建议先提交再生成代码");
return;
}
// 创建生成分支
String branchName = "feature/code-gen-" + LocalDate.now().toString();
createBranch(branchName);
try {
// 执行代码生成
generateCode();
// 提交生成的代码
commitGeneratedCode(branchName, "自动生成代码");
log.info("代码生成完成,已提交到分支: {}", branchName);
} catch (Exception e) {
log.error("代码生成失败,回滚更改", e);
rollbackChanges();
}
}
private boolean hasUncommittedChanges(String projectPath) {
// 检查Git状态
return false;
}
private void createBranch(String branchName) {
// 创建Git分支
}
private void commitGeneratedCode(String branchName, String message) {
// 提交代码
}
private void rollbackChanges() {
// 回滚更改
}
private void generateCode() {
// 执行代码生成
}
}
/**
* 最佳实践3:模板版本管理
* 管理不同版本的模板,支持回滚和对比
*/
public static class TemplateVersionManager {
private final String templateBasePath = "templates/version";
public void useTemplateVersion(String version) {
String templatePath = templateBasePath + "/" + version;
FastAutoGenerator.create("jdbc:mysql://localhost:3306/enterprise_db", "root", "123456")
.templateConfig(builder -> {
builder.entity(templatePath + "/entity.java")
.service(templatePath + "/service.java")
.controller(templatePath + "/controller.java")
.build();
})
// 其他配置...
.execute();
}
public void backupCurrentTemplates(String version) {
String sourcePath = "templates";
String targetPath = templateBasePath + "/" + version;
// 备份当前模板
copyTemplates(sourcePath, targetPath);
log.info("模板已备份到: {}", targetPath);
}
private void copyTemplates(String sourcePath, String targetPath) {
// 复制模板文件
}
}
/**
* 最佳实践4:生成代码质量检查
*/
public static class CodeQualityChecker {
public void checkGeneratedCode(String projectPath) {
log.info("开始检查生成代码质量");
// 1. 编译检查
boolean compileSuccess = checkCompilation(projectPath);
if (!compileSuccess) {
log.error("生成代码编译失败");
return;
}
// 2. 代码规范检查
boolean codeStylePass = checkCodeStyle(projectPath);
if (!codeStylePass) {
log.warn("生成代码风格检查未通过");
}
// 3. 重复代码检查
boolean noDuplication = checkDuplication(projectPath);
if (!noDuplication) {
log.warn("发现重复代码");
}
log.info("代码质量检查完成");
}
private boolean checkCompilation(String projectPath) {
// 执行编译检查
return true;
}
private boolean checkCodeStyle(String projectPath) {
// 检查代码风格
return true;
}
private boolean checkDuplication(String projectPath) {
// 检查重复代码
return true;
}
}
/**
* 最佳实践5:生成报告生成
*/
public static class GenerationReportGenerator {
public void generateReport(List<TableInfo> generatedTables) {
log.info("生成代码生成报告");
StringBuilder report = new StringBuilder();
report.append("代码生成报告\n");
report.append("生成时间: ").append(LocalDateTime.now()).append("\n\n");
report.append("生成的表:\n");
for (TableInfo table : generatedTables) {
report.append("- ").append(table.getName())
.append(" (").append(table.getComment()).append(")\n");
report.append(" 实体类: ").append(table.getEntityName()).append("Entity.java\n");
report.append(" Mapper: ").append(table.getMapperName()).append(".java\n");
report.append(" Service: ").append(table.getServiceName()).append(".java\n");
report.append(" Controller: ").append(table.getControllerName()).append(".java\n\n");
}
// 保存报告到文件
saveReportToFile(report.toString());
log.info("代码生成报告已保存");
}
private void saveReportToFile(String report) {
String reportPath = System.getProperty("user.dir") + "/code-generation-report.txt";
try {
Files.write(Paths.get(reportPath), report.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
log.error("保存报告失败: {}", e.getMessage(), e);
}
}
}
/**
* 避坑指南总结:
*
* 1. 文件覆盖风险
* - 生产环境谨慎使用 fileOverride()
* - 建议先备份再生成
*
* 2. 模板兼容性问题
* - 不同版本的MyBatis-Plus模板可能不兼容
* - 建议对模板进行版本管理
*
* 3. 数据库连接安全
* - 不要在代码中硬编码数据库密码
* - 使用配置中心或环境变量
*
* 4. 生成代码规范
* - 生成后运行代码检查
* - 确保符合团队编码规范
*
* 5. 版本控制集成
* - 在独立分支生成代码
* - 代码审查后再合并
*/
}
企业级MyBatis-Plus实战教学(续)
模块六:乐观锁/逻辑删除实现机制
6.1 乐观锁详细实现原理
6.1.1 乐观锁配置详解
/**
* 乐观锁完整配置详解
* 重点:理解乐观锁的实现原理和配置细节
*/
@Configuration
@Slf4j
public class OptimisticLockConfig {
/**
* 乐观锁插件详细配置
* 重点:乐观锁拦截器的工作原理和配置参数
*/
@Bean
@Order(2) // 在分页插件之后执行
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 乐观锁插件配置
OptimisticLockerInnerInterceptor optimisticLockerInterceptor =
createOptimisticLockerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInterceptor);
log.info("乐观锁插件配置完成");
return interceptor;
}
/**
* 创建乐观锁拦截器详细配置
*/
private OptimisticLockerInnerInterceptor createOptimisticLockerInterceptor() {
OptimisticLockerInnerInterceptor interceptor =
new OptimisticLockerInnerInterceptor();
// 详细说明乐观锁工作原理:
/*
乐观锁执行流程:
1. 从实体对象中获取版本号字段值
2. 在UPDATE语句的SET部分添加 version = version + 1
3. 在WHERE条件中添加 version = 旧版本号
4. 如果更新影响行数为0,说明版本号不匹配,抛出异常或返回false
*/
log.info("乐观锁拦截器创建完成,版本号字段名: version");
return interceptor;
}
/**
* 乐观锁重试机制配置
* 重点:处理乐观锁冲突时的重试策略
*/
@Bean
public OptimisticLockRetryTemplate optimisticLockRetryTemplate() {
return new OptimisticLockRetryTemplate(3, 100L); // 最大重试3次,间隔100ms
}
}
/**
* 乐观锁重试模板
* 重点:企业级乐观锁冲突解决方案
*/
@Slf4j
public class OptimisticLockRetryTemplate {
private final int maxRetries;
private final long retryInterval;
public OptimisticLockRetryTemplate(int maxRetries, long retryInterval) {
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
}
/**
* 执行带重试的乐观锁操作
*/
public <T> T executeWithRetry(Supplier<T> operation) {
int retryCount = 0;
while (retryCount <= maxRetries) {
try {
return operation.get();
} catch (OptimisticLockingFailureException e) {
retryCount++;
if (retryCount > maxRetries) {
log.error("乐观锁操作失败,已达到最大重试次数: {}", maxRetries);
throw e;
}
log.warn("乐观锁冲突,进行第 {} 次重试", retryCount);
try {
Thread.sleep(retryInterval);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
}
}
throw new OptimisticLockingFailureException("乐观锁操作失败");
}
}
/**
* 自定义乐观锁异常
*/
public class OptimisticLockingFailureException extends RuntimeException {
public OptimisticLockingFailureException(String message) {
super(message);
}
public OptimisticLockingFailureException(String message, Throwable cause) {
super(message, cause);
}
}
6.1.2 乐观锁实体类设计
/**
* 乐观锁实体类详细设计
* 重点:版本号字段的设计规范和最佳实践
*/
@Data
@TableName("sys_product")
@ApiModel(value = "产品实体", description = "产品信息表,使用乐观锁控制并发")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
* 重点:使用分布式ID生成策略
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
@ApiModelProperty(value = "主键ID")
private Long id;
/**
* 产品名称
*/
@TableField("product_name")
@ApiModelProperty(value = "产品名称", required = true)
@NotBlank(message = "产品名称不能为空")
@Size(max = 100, message = "产品名称长度不能超过100个字符")
private String productName;
/**
* 产品库存
* 重点:库存字段经常发生并发更新,需要乐观锁保护
*/
@ApiModelProperty(value = "产品库存")
@Min(value = 0, message = "库存不能小于0")
private Integer stock;
/**
* 产品价格(分)
*/
@ApiModelProperty(value = "产品价格(分)")
@Min(value = 0, message = "价格不能小于0")
private Long price;
/**
* 版本号 - 乐观锁核心字段
* 重点:@Version 注解标记乐观锁版本号字段
* 规则:
* 1. 字段类型必须是数值类型(Integer, Long, etc.)
* 2. 新增时版本号默认为1(如果为null)
* 3. 更新时自动执行 version = version + 1
* 4. WHERE条件中自动添加 version = 旧版本号
*/
@Version
@TableField("version")
@ApiModelProperty(value = "版本号", hidden = true) // 隐藏版本号,不暴露给前端
private Integer version;
/**
* 逻辑删除字段
*/
@TableLogic
@TableField("deleted")
@ApiModelProperty(value = "逻辑删除", hidden = true)
private Integer deleted;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间", hidden = true)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间", hidden = true)
private LocalDateTime updateTime;
/**
* 构造方法 - 初始化版本号
* 重点:确保新增实体时版本号不为null
*/
public Product() {
this.version = 1; // 新实体默认版本号为1
}
/**
* 带参数构造方法
*/
public Product(String productName, Integer stock, Long price) {
this();
this.productName = productName;
this.stock = stock;
this.price = price;
}
}
/**
* 库存变更记录实体
* 重点:记录库存变更历史,用于审计和问题排查
*/
@Data
@TableName("sys_product_stock_log")
@ApiModel(value = "库存变更记录", description = "产品库存变更记录表")
public class ProductStockLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 产品ID
*/
@TableField("product_id")
private Long productId;
/**
* 变更前库存
*/
@TableField("before_stock")
private Integer beforeStock;
/**
* 变更后库存
*/
@TableField("after_stock")
private Integer afterStock;
/**
* 变更数量(正数表示入库,负数表示出库)
*/
@TableField("change_quantity")
private Integer changeQuantity;
/**
* 变更类型:1-入库 2-出库 3-调整
*/
@TableField("change_type")
private Integer changeType;
/**
* 变更原因
*/
@TableField("change_reason")
private String changeReason;
/**
* 业务单号
*/
@TableField("biz_number")
private String bizNumber;
/**
* 操作人
*/
@TableField("operator")
private String operator;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
6.1.3 乐观锁业务服务实现
/**
* 乐观锁业务服务详细实现
* 重点:各种业务场景下的乐观锁应用
*/
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class ProductService extends ServiceImpl<ProductMapper, Product> {
@Autowired
private ProductMapper productMapper;
@Autowired
private ProductStockLogMapper stockLogMapper;
@Autowired
private OptimisticLockRetryTemplate retryTemplate;
/**
* 1. 基础乐观锁更新
* 重点:理解乐观锁更新失败的处理机制
*/
public boolean updateProductWithOptimisticLock(Product product) {
log.info("开始乐观锁更新产品,产品ID: {}, 当前版本: {}",
product.getId(), product.getVersion());
// 执行更新操作
// 生成的SQL: UPDATE sys_product SET product_name=?, stock=?, price=?, version=version+1
// WHERE id=? AND version=?
boolean success = updateById(product);
if (!success) {
log.warn("乐观锁更新失败,产品ID: {}, 版本号已变更", product.getId());
throw new OptimisticLockingFailureException("数据已被其他用户修改,请刷新后重试");
}
log.info("乐观锁更新成功,产品ID: {}, 新版本: {}", product.getId(), product.getVersion() + 1);
return true;
}
/**
* 2. 库存扣减 - 带重试机制的乐观锁
* 重点:高并发场景下的库存扣减解决方案
*/
public boolean deductStockWithRetry(Long productId, Integer quantity) {
log.info("开始扣减库存,产品ID: {}, 扣减数量: {}", productId, quantity);
return retryTemplate.executeWithRetry(() -> {
// 在重试逻辑中执行库存扣减
return deductStock(productId, quantity);
});
}
/**
* 3. 库存扣减核心逻辑
* 重点:完整的库存扣减业务流程
*/
private boolean deductStock(Long productId, Integer quantity) {
// 1. 查询当前产品信息(包含版本号)
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("产品不存在,ID: " + productId);
}
// 2. 检查库存是否充足
if (product.getStock() < quantity) {
throw new RuntimeException(String.format(
"库存不足,当前库存: %d,需要扣减: %d", product.getStock(), quantity));
}
// 3. 记录库存变更前状态
Integer beforeStock = product.getStock();
Integer afterStock = beforeStock - quantity;
// 4. 更新库存(乐观锁保护)
product.setStock(afterStock);
boolean updateSuccess = updateById(product);
if (!updateSuccess) {
// 乐观锁冲突,抛出异常触发重试
throw new OptimisticLockingFailureException("库存扣减冲突,版本号已变更");
}
// 5. 记录库存变更日志
recordStockChange(productId, beforeStock, afterStock, -quantity,
2, "销售出库", "SALE_" + System.currentTimeMillis(), "system");
log.info("库存扣减成功,产品ID: {}, 扣减数量: {}, 剩余库存: {}",
productId, quantity, afterStock);
return true;
}
/**
* 4. 批量库存扣减
* 重点:多个产品库存同时扣减的事务处理
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchDeductStock(Map<Long, Integer> productQuantities) {
log.info("开始批量扣减库存,产品数量: {}", productQuantities.size());
for (Map.Entry<Long, Integer> entry : productQuantities.entrySet()) {
Long productId = entry.getKey();
Integer quantity = entry.getValue();
boolean success = deductStockWithRetry(productId, quantity);
if (!success) {
// 任何一个扣减失败,整个事务回滚
throw new RuntimeException("批量扣减库存失败,产品ID: " + productId);
}
}
log.info("批量扣减库存成功,涉及产品: {} 个", productQuantities.size());
return true;
}
/**
* 5. 库存增加 - 入库操作
*/
public boolean increaseStock(Long productId, Integer quantity, String reason, String bizNumber) {
log.info("开始增加库存,产品ID: {}, 增加数量: {}, 原因: {}",
productId, quantity, reason);
return retryTemplate.executeWithRetry(() -> {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("产品不存在,ID: " + productId);
}
Integer beforeStock = product.getStock();
Integer afterStock = beforeStock + quantity;
product.setStock(afterStock);
boolean updateSuccess = updateById(product);
if (!updateSuccess) {
throw new OptimisticLockingFailureException("库存增加冲突,版本号已变更");
}
// 记录库存变更日志
recordStockChange(productId, beforeStock, afterStock, quantity,
1, reason, bizNumber, "system");
log.info("库存增加成功,产品ID: {}, 增加数量: {}, 当前库存: {}",
productId, quantity, afterStock);
return true;
});
}
/**
* 6. 库存调整 - 直接设置库存值
*/
public boolean adjustStock(Long productId, Integer newStock, String reason) {
log.info("开始调整库存,产品ID: {}, 新库存: {}, 原因: {}",
productId, newStock, reason);
return retryTemplate.executeWithRetry(() -> {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("产品不存在,ID: " + productId);
}
Integer beforeStock = product.getStock();
if (beforeStock.equals(newStock)) {
log.info("库存无需调整,当前库存与目标库存相同");
return true;
}
Integer changeQuantity = newStock - beforeStock;
product.setStock(newStock);
boolean updateSuccess = updateById(product);
if (!updateSuccess) {
throw new OptimisticLockingFailureException("库存调整冲突,版本号已变更");
}
// 记录库存变更日志
recordStockChange(productId, beforeStock, newStock, changeQuantity,
3, reason, "ADJUST_" + System.currentTimeMillis(), "system");
log.info("库存调整成功,产品ID: {}, 调整数量: {}, 新库存: {}",
productId, changeQuantity, newStock);
return true;
});
}
/**
* 记录库存变更日志
*/
private void recordStockChange(Long productId, Integer beforeStock, Integer afterStock,
Integer changeQuantity, Integer changeType,
String changeReason, String bizNumber, String operator) {
ProductStockLog stockLog = new ProductStockLog();
stockLog.setProductId(productId);
stockLog.setBeforeStock(beforeStock);
stockLog.setAfterStock(afterStock);
stockLog.setChangeQuantity(changeQuantity);
stockLog.setChangeType(changeType);
stockLog.setChangeReason(changeReason);
stockLog.setBizNumber(bizNumber);
stockLog.setOperator(operator);
stockLogMapper.insert(stockLog);
}
/**
* 7. 获取产品库存变更历史
*/
public List<ProductStockLog> getStockChangeHistory(Long productId, LocalDateTime startTime,
LocalDateTime endTime) {
QueryWrapper<ProductStockLog> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id", productId)
.between("create_time", startTime, endTime)
.orderByDesc("create_time");
return stockLogMapper.selectList(queryWrapper);
}
/**
* 8. 检查库存是否足够
* 重点:只读操作,不需要乐观锁
*/
public boolean checkStockSufficient(Long productId, Integer requiredQuantity) {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("产品不存在,ID: " + productId);
}
return product.getStock() >= requiredQuantity;
}
/**
* 9. 获取库存预警产品列表
*/
public List<Product> getLowStockProducts(Integer warningThreshold) {
LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.le(Product::getStock, warningThreshold)
.eq(Product::getDeleted, 0)
.orderByAsc(Product::getStock);
return productMapper.selectList(queryWrapper);
}
}
6.2 逻辑删除详细实现
6.2.1 逻辑删除配置详解
# application.yml 逻辑删除详细配置
mybatis-plus:
global-config:
db-config:
# 逻辑删除配置
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 逻辑删除扩展配置(3.5.0+)
logic-delete-strategy: logic # 逻辑删除策略:logic-逻辑删除,physical-物理删除
configuration:
# 日志配置(开发环境查看逻辑删除SQL)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
/**
* 逻辑删除配置类详解
* 重点:逻辑删除的底层实现原理和扩展配置
*/
@Configuration
@Slf4j
public class LogicDeleteConfig {
/**
* 逻辑删除自动填充处理器
* 重点:逻辑删除字段的自动填充策略
*/
@Bean
public MetaObjectHandler logicDeleteMetaObjectHandler() {
return new MetaObjectHandler() {
/**
* 插入时自动填充逻辑删除字段
*/
@Override
public void insertFill(MetaObject metaObject) {
log.debug("插入时自动填充逻辑删除字段");
// 如果逻辑删除字段为null,设置为未删除状态
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
// 设置删除时间为null
this.strictInsertFill(metaObject, "deleteTime", LocalDateTime.class, null);
// 设置删除人为null
this.strictInsertFill(metaObject, "deleteBy", String.class, null);
}
/**
* 更新时不需要填充逻辑删除字段
*/
@Override
public void updateFill(MetaObject metaObject) {
// 逻辑删除字段在更新时不需要自动填充
}
};
}
/**
* 自定义逻辑删除处理器
* 重点:扩展逻辑删除功能,支持删除人、删除时间记录
*/
@Component
public static class CustomLogicDeleteHandler implements LogicSqlInjector {
/**
* 注入自定义的SQL方法
*/
@Override
public void injectSql(ISqlInjector sqlInjector) {
// 可以在这里注入自定义的SQL方法
log.info("注入自定义逻辑删除SQL方法");
}
}
}
/**
* 基础实体类 - 包含完整的逻辑删除字段
* 重点:企业级逻辑删除字段设计规范
*/
@Data
public class BaseLogicDeleteEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 逻辑删除标志
* 重点:使用 @TableLogic 注解标记逻辑删除字段
* 规则:
* 1. 查询时自动添加 WHERE deleted = 0 条件
* 2. 删除时自动执行 UPDATE SET deleted = 1
* 3. 支持自定义删除值和未删除值
*/
@TableLogic
@TableField(value = "deleted")
@ApiModelProperty(value = "逻辑删除标志", hidden = true)
private Integer deleted;
/**
* 删除时间
* 重点:记录逻辑删除的具体时间
*/
@TableField(value = "delete_time")
@ApiModelProperty(value = "删除时间", hidden = true)
private LocalDateTime deleteTime;
/**
* 删除人
* 重点:记录执行逻辑删除的操作人
*/
@TableField(value = "delete_by")
@ApiModelProperty(value = "删除人", hidden = true)
private String deleteBy;
/**
* 默认构造方法 - 初始化逻辑删除字段
*/
public BaseLogicDeleteEntity() {
this.deleted = 0; // 默认未删除
}
}
/**
* 用户实体类 - 继承基础逻辑删除实体
* 重点:展示如何在业务实体中使用逻辑删除
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
@ApiModel(value = "用户实体", description = "用户信息表,使用逻辑删除")
public class User extends BaseLogicDeleteEntity {
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty(value = "用户ID")
private Long id;
@TableField("user_name")
@ApiModelProperty(value = "用户名", required = true)
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50个字符之间")
private String userName;
@TableField("real_name")
@ApiModelProperty(value = "真实姓名")
@Size(max = 50, message = "真实姓名长度不能超过50个字符")
private String realName;
@ApiModelProperty(value = "年龄")
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@ApiModelProperty(value = "邮箱")
@Email(message = "邮箱格式不正确")
private String email;
@TableField("phone_number")
@ApiModelProperty(value = "手机号")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phoneNumber;
@TableField("dept_id")
@ApiModelProperty(value = "部门ID")
private Long deptId;
@Version
@TableField("version")
@ApiModelProperty(value = "版本号", hidden = true)
private Integer version;
@TableField(value = "create_time", fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间", hidden = true)
private LocalDateTime createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间", hidden = true)
private LocalDateTime updateTime;
@TableField(value = "create_by", fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人", hidden = true)
private String createBy;
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新人", hidden = true)
private String updateBy;
}
6.2.2 逻辑删除业务服务实现
/**
* 逻辑删除业务服务详细实现
* 重点:各种业务场景下的逻辑删除应用
*/
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class UserService extends ServiceImpl<UserMapper, User> {
@Autowired
private UserMapper userMapper;
/**
* 1. 逻辑删除用户
* 重点:理解逻辑删除与物理删除的区别
*/
public boolean logicDeleteUser(Long userId, String operator) {
log.info("开始逻辑删除用户,用户ID: {}, 操作人: {}", userId, operator);
// 方法1:使用MyBatis-Plus的removeById方法
// boolean success = removeById(userId);
// 方法2:自定义逻辑删除,记录删除人和删除时间
User user = new User();
user.setId(userId);
user.setDeleteTime(LocalDateTime.now());
user.setDeleteBy(operator);
// 执行逻辑删除
// 生成的SQL: UPDATE sys_user SET delete_time=?, delete_by=?, deleted=1 WHERE id=? AND deleted=0
boolean success = updateById(user);
if (success) {
log.info("逻辑删除用户成功,用户ID: {}", userId);
} else {
log.warn("逻辑删除用户失败,用户ID: {} 可能不存在或已被删除", userId);
}
return success;
}
/**
* 2. 批量逻辑删除用户
*/
public boolean batchLogicDeleteUsers(List<Long> userIds, String operator) {
log.info("开始批量逻辑删除用户,用户数量: {}, 操作人: {}", userIds.size(), operator);
if (userIds == null || userIds.isEmpty()) {
log.warn("用户ID列表为空,跳过批量删除");
return true;
}
// 构建更新条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", userIds)
.eq("deleted", 0) // 只删除未删除的用户
.set("delete_time", LocalDateTime.now())
.set("delete_by", operator)
.set("deleted", 1);
int affectedRows = userMapper.update(null, updateWrapper);
log.info("批量逻辑删除完成,成功删除 {} 个用户", affectedRows);
return affectedRows > 0;
}
/**
* 3. 恢复被逻辑删除的用户
*/
public boolean recoverUser(Long userId, String operator) {
log.info("开始恢复用户,用户ID: {}, 操作人: {}", userId, operator);
User user = new User();
user.setId(userId);
user.setDeleted(0); // 恢复为未删除状态
user.setDeleteTime(null); // 清空删除时间
user.setDeleteBy(null); // 清空删除人
user.setUpdateBy(operator);
boolean success = updateById(user);
if (success) {
log.info("恢复用户成功,用户ID: {}", userId);
} else {
log.warn("恢复用户失败,用户ID: {}", userId);
}
return success;
}
/**
* 4. 查询用户(自动过滤已删除用户)
* 重点:MyBatis-Plus自动添加 deleted=0 条件
*/
public User getUserById(Long userId) {
log.debug("查询用户,用户ID: {}", userId);
// 自动生成的SQL: SELECT * FROM sys_user WHERE id=? AND deleted=0
User user = getById(userId);
if (user == null) {
log.debug("用户不存在或已被删除,用户ID: {}", userId);
}
return user;
}
/**
* 5. 查询所有用户(包括已删除用户)
* 重点:使用自定义查询方法,忽略逻辑删除条件
*/
public List<User> getAllUsersIncludingDeleted() {
log.debug("查询所有用户(包括已删除用户)");
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 不添加 deleted=0 条件,查询所有记录
return userMapper.selectList(queryWrapper);
// 生成的SQL: SELECT * FROM sys_user
}
/**
* 6. 查询已删除用户
*/
public List<User> getDeletedUsers() {
log.debug("查询已删除用户");
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 1)
.orderByDesc("delete_time");
return userMapper.selectList(queryWrapper);
// 生成的SQL: SELECT * FROM sys_user WHERE deleted=1 ORDER BY delete_time DESC
}
/**
* 7. 物理删除用户(谨慎使用)
* 重点:真正的数据库删除操作,数据不可恢复
*/
@Transactional(rollbackFor = Exception.class)
public boolean physicalDeleteUser(Long userId) {
log.warn("执行物理删除用户,用户ID: {}", userId);
// 先备份重要数据
backupUserData(userId);
// 执行物理删除
int affectedRows = userMapper.physicalDeleteById(userId);
if (affectedRows > 0) {
log.warn("物理删除用户完成,用户ID: {}", userId);
return true;
} else {
log.warn("物理删除用户失败,用户ID: {}", userId);
return false;
}
}
/**
* 8. 清理长时间已逻辑删除的用户
* 重点:定期清理逻辑删除数据,避免数据膨胀
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
@Transactional(rollbackFor = Exception.class)
public void cleanDeletedUsers() {
log.info("开始清理长时间已逻辑删除的用户");
// 清理30天前删除的用户
LocalDateTime threshold = LocalDateTime.now().minusDays(30);
// 查询需要清理的用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 1)
.le("delete_time", threshold)
.last("LIMIT 1000"); // 每次最多清理1000条
List<User> deletedUsers = userMapper.selectList(queryWrapper);
if (deletedUsers.isEmpty()) {
log.info("没有需要清理的已删除用户");
return;
}
// 备份数据
for (User user : deletedUsers) {
backupUserData(user.getId());
}
// 执行物理删除
List<Long> userIds = deletedUsers.stream()
.map(User::getId)
.collect(Collectors.toList());
int deletedCount = userMapper.physicalDeleteBatchIds(userIds);
log.info("清理已删除用户完成,共清理 {} 条记录", deletedCount);
}
/**
* 9. 用户统计(排除已删除用户)
*/
public UserStatistics getUserStatistics() {
log.debug("获取用户统计信息");
UserStatistics statistics = new UserStatistics();
// 总用户数(排除已删除)
Integer totalUsers = userMapper.selectCount(new QueryWrapper<User>().eq("deleted", 0));
statistics.setTotalUsers(totalUsers);
// 今日新增用户
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
Integer todayNewUsers = userMapper.selectCount(new QueryWrapper<User>()
.eq("deleted", 0)
.ge("create_time", todayStart));
statistics.setTodayNewUsers(todayNewUsers);
// 已删除用户数
Integer deletedUsers = userMapper.selectCount(new QueryWrapper<User>().eq("deleted", 1));
statistics.setDeletedUsers(deletedUsers);
log.debug("用户统计完成: 总数={}, 今日新增={}, 已删除={}",
totalUsers, todayNewUsers, deletedUsers);
return statistics;
}
/**
* 备份用户数据
*/
private void backupUserData(Long userId) {
// 在实际项目中,这里应该将用户数据备份到历史表或归档存储
log.debug("备份用户数据,用户ID: {}", userId);
try {
User user = userMapper.selectById(userId);
if (user != null) {
// 备份到历史表
// userHistoryMapper.insert(user);
log.info("用户数据备份完成,用户ID: {}", userId);
}
} catch (Exception e) {
log.error("备份用户数据失败,用户ID: {}", userId, e);
throw new RuntimeException("备份用户数据失败", e);
}
}
}
/**
* 用户统计信息
*/
@Data
class UserStatistics {
private Integer totalUsers;
private Integer todayNewUsers;
private Integer deletedUsers;
private Integer activeUsers;
}
6.3 乐观锁与逻辑删除的底层原理深度解析
/**
* 乐观锁与逻辑删除底层原理深度解析
* 重点:理解MyBatis-Plus如何实现乐观锁和逻辑删除
*/
@Slf4j
public class OptimisticLockAndLogicDeletePrinciple {
/**
* 1. 乐观锁拦截器原理分析
*/
public void analyzeOptimisticLockInterceptor() {
/*
乐观锁拦截器(OptimisticLockerInnerInterceptor)工作流程:
1. 拦截 Executor 的 update 方法
2. 检查实体类是否有 @Version 注解字段
3. 如果有版本号字段:
a. 获取当前版本号值
b. 在 SET 部分添加 version = version + 1
c. 在 WHERE 条件中添加 version = 当前版本号
d. 执行更新操作
e. 检查影响行数,如果为0则抛出异常
4. 源码关键方法:
- beforeUpdate(): 更新前处理
- processUpdate(): 处理更新逻辑
*/
// 模拟乐观锁SQL生成过程
String originalSql = "UPDATE sys_product SET product_name=?, stock=? WHERE id=?";
String optimisticLockSql = "UPDATE sys_product SET product_name=?, stock=?, version=version+1 WHERE id=? AND version=?";
log.info("乐观锁SQL转换: {} -> {}", originalSql, optimisticLockSql);
}
/**
* 2. 逻辑删除拦截器原理分析
*/
public void analyzeLogicDeleteInterceptor() {
/*
逻辑删除拦截器工作流程:
1. 拦截 Executor 的 query 和 update 方法
2. 对于查询操作(SELECT):
a. 解析原始SQL
b. 检查是否已经包含逻辑删除条件
c. 如果没有,自动添加 WHERE deleted=0 条件
d. 修改 BoundSql 的 SQL 内容
3. 对于删除操作(DELETE):
a. 将 DELETE 语句转换为 UPDATE 语句
b. 设置 deleted=1 和其他删除相关字段
c. 修改 MappedStatement
4. 源码关键方法:
- beforeQuery(): 查询前处理
- beforeUpdate(): 更新前处理
- logicDelete(): 处理逻辑删除
*/
// 模拟逻辑删除SQL转换过程
String originalSelectSql = "SELECT * FROM sys_user";
String logicDeleteSelectSql = "SELECT * FROM sys_user WHERE deleted=0";
String originalDeleteSql = "DELETE FROM sys_user WHERE id=1";
String logicDeleteUpdateSql = "UPDATE sys_user SET deleted=1, delete_time=NOW() WHERE id=1";
log.info("逻辑删除SELECT转换: {} -> {}", originalSelectSql, logicDeleteSelectSql);
log.info("逻辑删除DELETE转换: {} -> {}", originalDeleteSql, logicDeleteUpdateSql);
}
/**
* 3. 自定义逻辑删除处理器示例
*/
public static class CustomLogicDeleteHandler extends AbstractLogicMethodHandler {
/**
* 重写删除方法,支持自定义逻辑删除逻辑
*/
@Override
public void logicDelete(LogicDelete logicDelete, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 获取逻辑删除字段配置
String logicDeleteField = tableInfo.getLogicDeleteField();
String logicDeleteValue = tableInfo.getLogicDeleteValue();
String logicNotDeleteValue = tableInfo.getLogicNotDeleteValue();
log.info("自定义逻辑删除处理 - 字段: {}, 删除值: {}, 未删除值: {}",
logicDeleteField, logicDeleteValue, logicNotDeleteValue);
// 可以在这里添加自定义逻辑,如记录删除日志等
}
}
/**
* 4. 乐观锁冲突检测机制
*/
public void analyzeOptimisticLockConflictDetection() {
/*
乐观锁冲突检测机制:
场景:两个线程同时更新同一条记录
线程A:version=1 -> 执行更新,version变为2
线程B:version=1 -> 执行更新,WHERE version=1 条件不满足,更新0行
检测方式:
1. 执行更新操作
2. 检查 affectedRows(影响行数)
3. 如果 affectedRows == 0,说明版本号不匹配
4. 抛出 OptimisticLockException 或返回 false
企业级处理:
1. 重试机制
2. 告警通知
3. 冲突记录
*/
// 模拟乐观锁冲突场景
int affectedRows = 0; // 模拟更新影响0行
if (affectedRows == 0) {
throw new OptimisticLockingFailureException("数据已被其他用户修改");
}
}
/**
* 5. 逻辑删除性能优化分析
*/
public void analyzeLogicDeletePerformance() {
/*
逻辑删除性能考虑:
优势:
1. 数据可恢复,避免误删
2. 保留数据历史,便于审计
3. 关联数据完整性
劣势:
1. 表数据量会不断增长
2. 查询需要额外过滤条件
3. 索引需要包含 deleted 字段
优化方案:
1. 定期归档已删除数据
2. 为 deleted 字段建立索引
3. 使用分区表分离活跃和删除数据
4. 查询时明确指定 deleted 条件
*/
// 建议的索引设计
String recommendedIndex = "CREATE INDEX idx_user_deleted ON sys_user(deleted, create_time)";
log.info("逻辑删除性能优化建议索引: {}", recommendedIndex);
}
/**
* 6. 混合使用乐观锁和逻辑删除
*/
public void analyzeMixedUsage() {
/*
乐观锁 + 逻辑删除的SQL示例:
原始更新SQL:
UPDATE sys_product SET stock=90 WHERE id=1
添加乐观锁和逻辑删除后:
UPDATE sys_product SET stock=90, version=version+1
WHERE id=1 AND version=1 AND deleted=0
这样同时保证了:
1. 数据并发安全(乐观锁)
2. 不会更新已删除的数据(逻辑删除)
3. 版本号自动递增
*/
String mixedSql = "UPDATE sys_product SET stock=90, version=version+1 " +
"WHERE id=1 AND version=1 AND deleted=0";
log.info("乐观锁+逻辑删除混合SQL: {}", mixedSql);
}
}
6.4 企业级避坑方案详细解析
/**
* 乐观锁和逻辑删除企业级避坑方案
* 重点:生产环境中常见问题和解决方案
*/
@Service
@Slf4j
public class OptimisticLockAndLogicDeleteBestPractice {
@Autowired
private ProductService productService;
@Autowired
private UserService userService;
/**
* 避坑1:乐观锁版本号初始化
*/
public void avoidVersionNullIssue() {
// 错误做法:新增实体时版本号为null
Product product = new Product();
// product.setVersion(null); // 默认就是null
// 正确做法:在构造方法或设置默认值
Product safeProduct = new Product("测试产品", 100, 1000L);
// version 在构造方法中初始化为1
log.info("产品版本号: {}", safeProduct.getVersion()); // 输出: 1
}
/**
* 避坑2:乐观锁更新前检查
*/
public boolean safeUpdateProduct(Product product) {
// 错误做法:直接更新,不检查数据是否存在
// return productService.updateById(product);
// 正确做法:先查询确认数据存在且版本号匹配
Product existingProduct = productService.getById(product.getId());
if (existingProduct == null) {
throw new RuntimeException("产品不存在,ID: " + product.getId());
}
// 检查版本号(可选,框架会自动检查)
if (!existingProduct.getVersion().equals(product.getVersion())) {
throw new OptimisticLockingFailureException("数据版本不匹配,请刷新后重试");
}
return productService.updateById(product);
}
/**
* 避坑3:逻辑删除关联数据处理
*/
@Transactional(rollbackFor = Exception.class)
public boolean safeDeleteUserWithRelations(Long userId, String operator) {
// 错误做法:只删除用户,不处理关联数据
// return userService.logicDeleteUser(userId, operator);
// 正确做法:检查并处理所有关联数据
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
// 1. 检查用户是否有未完成的订单
// boolean hasPendingOrders = orderService.hasPendingOrders(userId);
// if (hasPendingOrders) {
// throw new RuntimeException("用户有待处理订单,无法删除");
// }
// 2. 逻辑删除用户的关联数据
// userRoleService.logicDeleteByUserId(userId, operator);
// userAddressService.logicDeleteByUserId(userId, operator);
// 3. 记录删除操作日志
// auditService.logUserDelete(user, operator);
// 4. 执行用户逻辑删除
return userService.logicDeleteUser(userId, operator);
}
/**
* 避坑4:批量操作乐观锁处理
*/
@Transactional(rollbackFor = Exception.class)
public boolean safeBatchUpdateProducts(List<Product> products) {
// 错误做法:直接批量更新,某个失败不会回滚
// for (Product product : products) {
// productService.updateById(product);
// }
// 正确做法:使用事务,任何一个失败就回滚
for (Product product : products) {
boolean success = productService.updateById(product);
if (!success) {
throw new OptimisticLockingFailureException(
"批量更新失败,产品ID: " + product.getId() + " 版本号冲突");
}
}
return true;
}
/**
* 避坑5:逻辑删除数据的查询优化
*/
public List<User> optimizeQueryWithLogicDelete() {
// 错误做法:查询所有字段,包括已删除数据
// return userService.getAllUsersIncludingDeleted();
// 正确做法1:明确指定只查询未删除数据
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 0)
.select("id", "user_name", "email", "create_time"); // 只查询需要的字段
// 正确做法2:使用索引优化的查询条件
queryWrapper.orderByDesc("create_time")
.last("LIMIT 100");
return userService.list(queryWrapper);
}
/**
* 避坑6:物理删除的风险控制
*/
@Transactional(rollbackFor = Exception.class)
public boolean safePhysicalDelete(Long userId) {
// 错误做法:直接物理删除
// return userService.physicalDeleteUser(userId);
// 正确做法:多重确认和备份
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
// 1. 权限检查(只有管理员可以物理删除)
// if (!hasPhysicalDeletePermission()) {
// throw new RuntimeException("没有物理删除权限");
// }
// 2. 重要数据备份
userService.backupUserData(userId);
// 3. 记录物理删除操作
log.warn("执行物理删除用户,用户ID: {}, 操作人: {}", userId, getCurrentUser());
// auditService.logPhysicalDelete(user, getCurrentUser());
// 4. 执行物理删除
return userService.physicalDeleteUser(userId);
}
/**
* 避坑7:乐观锁重试策略配置
*/
public void configureOptimisticLockRetry() {
// 错误做法:无限制重试或重试间隔不合理
// while (true) {
// try {
// deductStock(productId, quantity);
// break;
// } catch (OptimisticLockingFailureException e) {
// // 无限重试
// }
// }
// 正确做法:合理的重试次数和间隔
OptimisticLockRetryTemplate retryTemplate = new OptimisticLockRetryTemplate(3, 100L);
// 使用重试模板执行操作
retryTemplate.executeWithRetry(() -> {
return productService.deductStock(1L, 1);
});
}
/**
* 避坑8:逻辑删除数据的统计准确性
*/
public UserStatistics getAccurateUserStatistics() {
// 错误做法:使用默认的count方法,可能包含已删除数据
// int totalUsers = userService.count();
// 正确做法:明确指定统计条件
QueryWrapper<User> countWrapper = new QueryWrapper<>();
countWrapper.eq("deleted", 0);
int totalUsers = userService.count(countWrapper);
UserStatistics statistics = new UserStatistics();
statistics.setTotalUsers(totalUsers);
return statistics;
}
private String getCurrentUser() {
// 获取当前登录用户
return "system";
}
}
企业级MyBatis-Plus实战教学(续)
模块七:多表关联查询解决方案
7.1 关联查询基础概念详解
/**
* 关联查询基础概念详解
* 重点:理解各种关联类型和使用场景
*/
@Slf4j
public class AssociationQueryConcepts {
/**
* 关联查询类型说明:
*
* 1. 一对一(One-to-One)
* - 一个用户对应一个身份证
* - 一个订单对应一个物流信息
*
* 2. 一对多(One-to-Many)
* - 一个部门对应多个用户
* - 一个订单对应多个订单项
*
* 3. 多对一(Many-to-One)
* - 多个用户属于一个部门
* - 多个订单项属于一个订单
*
* 4. 多对多(Many-to-Many)
* - 多个用户对应多个角色
* - 多个商品对应多个分类
*/
public void explainAssociationTypes() {
log.info("""
==================== 关联查询类型说明 ====================
一对一:使用 @One 注解或 <association> 标签
一对多:使用 @Many 注解或 <collection> 标签
多对一:本质是一对多的反向关系
多对多:需要通过中间表实现
""");
}
}
7.2 VO(View Object)类详细设计
/**
* 关联查询VO类详细设计
* 重点:VO类用于封装关联查询结果,避免污染实体类
*/
@Data
@ApiModel(value = "用户详情VO", description = "用户详细信息展示对象")
public class UserDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
// ==================== 用户基本信息 ====================
@ApiModelProperty(value = "用户ID")
private Long id;
@ApiModelProperty(value = "用户名")
private String userName;
@ApiModelProperty(value = "真实姓名")
private String realName;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "手机号")
private String phoneNumber;
@ApiModelProperty(value = "用户状态:0-正常 1-禁用")
private Integer status;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
// ==================== 部门信息(一对一) ====================
@ApiModelProperty(value = "部门信息")
private DeptVO dept;
@ApiModelProperty(value = "部门ID")
private Long deptId;
@ApiModelProperty(value = "部门名称")
private String deptName;
@ApiModelProperty(value = "部门负责人")
private String deptLeader;
// ==================== 角色列表(一对多) ====================
@ApiModelProperty(value = "角色列表")
private List<RoleVO> roles;
@ApiModelProperty(value = "角色名称列表(逗号分隔)")
private String roleNames;
@ApiModelProperty(value = "角色Key列表(逗号分隔)")
private String roleKeys;
// ==================== 权限信息(多对多) ====================
@ApiModelProperty(value = "权限列表")
private List<PermissionVO> permissions;
@ApiModelProperty(value = "权限标识列表")
private List<String> permissionKeys;
// ==================== 统计信息 ====================
@ApiModelProperty(value = "订单数量")
private Integer orderCount;
@ApiModelProperty(value = "最后登录时间")
private LocalDateTime lastLoginTime;
@ApiModelProperty(value = "登录次数")
private Integer loginCount;
}
/**
* 部门VO
*/
@Data
@ApiModel(value = "部门VO", description = "部门信息展示对象")
public class DeptVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "部门ID")
private Long id;
@ApiModelProperty(value = "部门名称")
private String deptName;
@ApiModelProperty(value = "部门编码")
private String deptCode;
@ApiModelProperty(value = "负责人")
private String leader;
@ApiModelProperty(value = "联系电话")
private String phone;
@ApiModelProperty(value = "部门状态:0-正常 1-停用")
private Integer status;
@ApiModelProperty(value = "父部门ID")
private Long parentId;
@ApiModelProperty(value = "祖级列表")
private String ancestors;
@ApiModelProperty(value = "显示顺序")
private Integer orderNum;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
}
/**
* 角色VO
*/
@Data
@ApiModel(value = "角色VO", description = "角色信息展示对象")
public class RoleVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "角色ID")
private Long id;
@ApiModelProperty(value = "角色名称")
private String roleName;
@ApiModelProperty(value = "角色权限字符串")
private String roleKey;
@ApiModelProperty(value = "显示顺序")
private Integer roleSort;
@ApiModelProperty(value = "角色状态:0-正常 1-停用")
private Integer status;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
}
/**
* 权限VO
*/
@Data
@ApiModel(value = "权限VO", description = "权限信息展示对象")
public class PermissionVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "权限ID")
private Long id;
@ApiModelProperty(value = "权限名称")
private String permissionName;
@ApiModelProperty(value = "权限标识")
private String permissionKey;
@ApiModelProperty(value = "权限类型:M-菜单 B-按钮")
private String permissionType;
@ApiModelProperty(value = "显示顺序")
private Integer orderNum;
@ApiModelProperty(value = "父权限ID")
private Long parentId;
}
7.3 注解方式关联查询详细实现
/**
* 注解方式关联查询详细实现
* 重点:使用MyBatis注解实现各种关联查询
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 1. 一对一关联查询:用户和部门
* 重点:@One注解实现一对一关联
*/
@Select("SELECT * FROM sys_user WHERE id = #{userId} AND deleted = 0")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "user_name", property = "userName"),
@Result(column = "real_name", property = "realName"),
@Result(column = "age", property = "age"),
@Result(column = "email", property = "email"),
@Result(column = "phone_number", property = "phoneNumber"),
@Result(column = "dept_id", property = "deptId"),
@Result(column = "status", property = "status"),
@Result(column = "create_time", property = "createTime"),
// 一对一关联:部门信息
@Result(column = "dept_id", property = "dept",
one = @One(select = "com.enterprise.mapper.DeptMapper.selectById",
fetchType = FetchType.EAGER))
})
UserDetailVO selectUserWithDept(Long userId);
/**
* 2. 一对多关联查询:用户和角色
* 重点:@Many注解实现一对多关联
*/
@Select("SELECT * FROM sys_user WHERE id = #{userId} AND deleted = 0")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "user_name", property = "userName"),
@Result(column = "real_name", property = "realName"),
// 一对多关联:角色列表
@Result(column = "id", property = "roles",
many = @Many(select = "com.enterprise.mapper.RoleMapper.selectRolesByUserId",
fetchType = FetchType.LAZY))
})
UserDetailVO selectUserWithRoles(Long userId);
/**
* 3. 多对多关联查询:用户和权限(通过角色中间表)
* 重点:复杂的多对多关联实现
*/
@Select("SELECT * FROM sys_user WHERE id = #{userId} AND deleted = 0")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "user_name", property = "userName"),
// 多对多关联:权限列表(通过角色)
@Result(column = "id", property = "permissions",
many = @Many(select = "com.enterprise.mapper.PermissionMapper.selectPermissionsByUserId",
fetchType = FetchType.LAZY))
})
UserDetailVO selectUserWithPermissions(Long userId);
/**
* 4. 复杂关联查询:用户完整信息
* 重点:同时包含一对一、一对多、多对多关联
*/
@Select("SELECT * FROM sys_user WHERE id = #{userId} AND deleted = 0")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "user_name", property = "userName"),
@Result(column = "real_name", property = "realName"),
@Result(column = "age", property = "age"),
@Result(column = "email", property = "email"),
@Result(column = "phone_number", property = "phoneNumber"),
@Result(column = "dept_id", property = "deptId"),
@Result(column = "status", property = "status"),
@Result(column = "create_time", property = "createTime"),
// 一对一:部门信息
@Result(column = "dept_id", property = "dept",
one = @One(select = "com.enterprise.mapper.DeptMapper.selectById")),
// 一对多:角色列表
@Result(column = "id", property = "roles",
many = @Many(select = "com.enterprise.mapper.RoleMapper.selectRolesByUserId")),
// 多对多:权限列表
@Result(column = "id", property = "permissions",
many = @Many(select = "com.enterprise.mapper.PermissionMapper.selectPermissionsByUserId"))
})
UserDetailVO selectUserDetail(Long userId);
/**
* 5. 关联查询分页
* 重点:使用@SelectProvider实现复杂的分页关联查询
*/
@SelectProvider(type = UserSqlProvider.class, method = "selectUserPageWithAssociations")
List<UserDetailVO> selectUserPageWithAssociations(@Param("page") Page<UserDetailVO> page,
@Param("query") UserQueryDTO query);
}
/**
* 部门Mapper - 关联查询相关方法
*/
@Mapper
public interface DeptMapper extends BaseMapper<Dept> {
@Select("SELECT * FROM sys_dept WHERE id = #{deptId} AND deleted = 0")
DeptVO selectById(Long deptId);
}
/**
* 角色Mapper - 关联查询相关方法
*/
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
@Select("SELECT r.* FROM sys_role r " +
"INNER JOIN sys_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId} AND r.deleted = 0 " +
"ORDER BY r.role_sort")
List<RoleVO> selectRolesByUserId(Long userId);
}
/**
* 权限Mapper - 关联查询相关方法
*/
@Mapper
public interface PermissionMapper extends BaseMapper<Permission> {
@Select("SELECT DISTINCT p.* FROM sys_permission p " +
"INNER JOIN sys_role_permission rp ON p.id = rp.permission_id " +
"INNER JOIN sys_user_role ur ON rp.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId} AND p.status = 0 AND p.deleted = 0 " +
"ORDER BY p.order_num")
List<PermissionVO> selectPermissionsByUserId(Long userId);
}
/**
* SQL提供类 - 动态SQL生成
*/
@Slf4j
public class UserSqlProvider {
/**
* 构建用户分页关联查询SQL
*/
public String selectUserPageWithAssociations(Map<String, Object> params) {
Page<?> page = (Page<?>) params.get("page");
UserQueryDTO query = (UserQueryDTO) params.get("query");
StringBuilder sql = new StringBuilder();
sql.append("SELECT ");
sql.append("u.id, u.user_name, u.real_name, u.age, u.email, u.phone_number, ");
sql.append("u.dept_id, u.status, u.create_time, ");
sql.append("d.dept_name, d.leader as dept_leader, ");
sql.append("(SELECT COUNT(*) FROM sys_order o WHERE o.user_id = u.id AND o.deleted = 0) as order_count, ");
sql.append("(SELECT MAX(login_time) FROM sys_login_log l WHERE l.user_id = u.id) as last_login_time ");
sql.append("FROM sys_user u ");
sql.append("LEFT JOIN sys_dept d ON u.dept_id = d.id AND d.deleted = 0 ");
sql.append("WHERE u.deleted = 0 ");
// 动态条件
if (StringUtils.isNotBlank(query.getUserName())) {
sql.append("AND u.user_name LIKE CONCAT('%', #{query.userName}, '%') ");
}
if (StringUtils.isNotBlank(query.getRealName())) {
sql.append("AND u.real_name LIKE CONCAT('%', #{query.realName}, '%') ");
}
if (query.getDeptId() != null) {
sql.append("AND u.dept_id = #{query.deptId} ");
}
if (query.getStatus() != null) {
sql.append("AND u.status = #{query.status} ");
}
if (query.getStartTime() != null) {
sql.append("AND u.create_time >= #{query.startTime} ");
}
if (query.getEndTime() != null) {
sql.append("AND u.create_time <= #{query.endTime} ");
}
// 排序
sql.append("ORDER BY u.create_time DESC ");
// 分页
if (page != null) {
long offset = (page.getCurrent() - 1) * page.getSize();
sql.append("LIMIT ").append(offset).append(", ").append(page.getSize());
}
log.debug("生成的SQL: {}", sql.toString());
return sql.toString();
}
}
7.4 XML配置方式关联查询详细实现
<!-- UserMapper.xml - 详细的XML关联查询配置 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.enterprise.mapper.UserMapper">
<!-- ==================== 基础结果映射 ==================== -->
<!-- 用户基础结果映射 -->
<resultMap id="UserBaseResultMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="real_name" property="realName"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
<result column="phone_number" property="phoneNumber"/>
<result column="dept_id" property="deptId"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="deleted" property="deleted"/>
</resultMap>
<!-- 用户详情VO结果映射 -->
<resultMap id="UserDetailVOResultMap" type="UserDetailVO">
<!-- 用户基础字段 -->
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="real_name" property="realName"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
<result column="phone_number" property="phoneNumber"/>
<result column="dept_id" property="deptId"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<!-- 部门字段(直接映射) -->
<result column="dept_name" property="deptName"/>
<result column="dept_leader" property="deptLeader"/>
<result column="order_count" property="orderCount"/>
<result column="last_login_time" property="lastLoginTime"/>
<!-- 一对一关联:部门信息 -->
<association property="dept" javaType="DeptVO"
select="com.enterprise.mapper.DeptMapper.selectById"
column="dept_id" fetchType="lazy"/>
<!-- 一对多关联:角色列表 -->
<collection property="roles" ofType="RoleVO"
select="com.enterprise.mapper.RoleMapper.selectRolesByUserId"
column="id" fetchType="lazy"/>
<!-- 多对多关联:权限列表 -->
<collection property="permissions" ofType="PermissionVO"
select="com.enterprise.mapper.PermissionMapper.selectPermissionsByUserId"
column="id" fetchType="lazy"/>
</resultMap>
<!-- 用户与部门JOIN结果映射 -->
<resultMap id="UserWithDeptResultMap" type="UserDetailVO">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="real_name" property="realName"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
<result column="phone_number" property="phoneNumber"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<!-- 部门信息 -->
<result column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<result column="dept_leader" property="deptLeader"/>
<result column="dept_status" property="deptStatus"/>
</resultMap>
<!-- 用户完整信息结果映射 -->
<resultMap id="UserFullInfoResultMap" type="UserDetailVO" extends="UserWithDeptResultMap">
<!-- 角色信息(嵌套结果) -->
<collection property="roles" ofType="RoleVO" resultMap="RoleBaseResultMap"
columnPrefix="role_"/>
</resultMap>
<!-- 角色基础结果映射 -->
<resultMap id="RoleBaseResultMap" type="RoleVO">
<id column="id" property="id"/>
<result column="role_name" property="roleName"/>
<result column="role_key" property="roleKey"/>
<result column="role_sort" property="roleSort"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- ==================== 关联查询SQL定义 ==================== -->
<!-- 1. 一对一关联:用户和部门 -->
<select id="selectUserWithDeptByXml" resultMap="UserWithDeptResultMap">
SELECT
u.id,
u.user_name,
u.real_name,
u.age,
u.email,
u.phone_number,
u.status,
u.create_time,
u.dept_id,
d.dept_name,
d.leader as dept_leader,
d.status as dept_status
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id AND d.deleted = 0
WHERE u.id = #{userId} AND u.deleted = 0
</select>
<!-- 2. 一对多关联:用户和角色(嵌套结果) -->
<select id="selectUserWithRolesByXml" resultMap="UserFullInfoResultMap">
SELECT
u.id,
u.user_name,
u.real_name,
u.age,
u.email,
u.phone_number,
u.status,
u.create_time,
u.dept_id,
d.dept_name,
d.leader as dept_leader,
r.id as role_id,
r.role_name as role_role_name,
r.role_key as role_role_key,
r.role_sort as role_role_sort,
r.status as role_status,
r.create_time as role_create_time
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id AND d.deleted = 0
LEFT JOIN sys_user_role ur ON u.id = ur.user_id
LEFT JOIN sys_role r ON ur.role_id = r.id AND r.deleted = 0
WHERE u.id = #{userId} AND u.deleted = 0
ORDER BY r.role_sort
</select>
<!-- 3. 多表关联分页查询 -->
<select id="selectUserPageWithMultipleAssociations" resultMap="UserDetailVOResultMap">
SELECT
u.id,
u.user_name,
u.real_name,
u.age,
u.email,
u.phone_number,
u.dept_id,
u.status,
u.create_time,
d.dept_name,
d.leader as dept_leader,
(SELECT COUNT(*) FROM sys_order o WHERE o.user_id = u.id AND o.deleted = 0) as order_count,
(SELECT MAX(login_time) FROM sys_login_log l WHERE l.user_id = u.id) as last_login_time
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id AND d.deleted = 0
WHERE u.deleted = 0
<if test="query.userName != null and query.userName != ''">
AND u.user_name LIKE CONCAT('%', #{query.userName}, '%')
</if>
<if test="query.realName != null and query.realName != ''">
AND u.real_name LIKE CONCAT('%', #{query.realName}, '%')
</if>
<if test="query.deptId != null">
AND u.dept_id = #{query.deptId}
</if>
<if test="query.status != null">
AND u.status = #{query.status}
</if>
<if test="query.startTime != null">
AND u.create_time >= #{query.startTime}
</if>
<if test="query.endTime != null">
AND u.create_time <= #{query.endTime}
</if>
ORDER BY u.create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
<!-- 4. 动态关联查询 -->
<select id="selectUserWithDynamicAssociations" resultMap="UserDetailVOResultMap">
SELECT
u.id,
u.user_name,
u.real_name,
u.age,
u.email,
u.phone_number,
u.dept_id,
u.status,
u.create_time
<if test="includeDept != null and includeDept">
,d.dept_name, d.leader as dept_leader
</if>
<if test="includeStats != null and includeStats">
,(SELECT COUNT(*) FROM sys_order o WHERE o.user_id = u.id AND o.deleted = 0) as order_count
,(SELECT MAX(login_time) FROM sys_login_log l WHERE l.user_id = u.id) as last_login_time
</if>
FROM sys_user u
<if test="includeDept != null and includeDept">
LEFT JOIN sys_dept d ON u.dept_id = d.id AND d.deleted = 0
</if>
WHERE u.deleted = 0
<if test="userId != null">
AND u.id = #{userId}
</if>
ORDER BY u.create_time DESC
</select>
<!-- 5. 嵌套查询:分步加载关联数据 -->
<select id="selectUserWithStepByStep" resultMap="UserDetailVOResultMap">
SELECT
id,
user_name,
real_name,
age,
email,
phone_number,
dept_id,
status,
create_time
FROM sys_user
WHERE deleted = 0
<if test="userId != null">
AND id = #{userId}
</if>
ORDER BY create_time DESC
</select>
</mapper>
7.5 关联查询业务服务实现
/**
* 关联查询业务服务详细实现
* 重点:各种关联查询场景的业务逻辑处理
*/
@Service
@Slf4j
@Transactional(readOnly = true)
public class UserAssociationService {
@Autowired
private UserMapper userMapper;
@Autowired
private DeptMapper deptMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
/**
* 1. 获取用户详情(包含部门信息)
*/
public UserDetailVO getUserDetailWithDept(Long userId) {
log.info("获取用户详情,用户ID: {}", userId);
// 方式1:使用注解方式查询
// UserDetailVO userDetail = userMapper.selectUserWithDept(userId);
// 方式2:使用XML方式查询
UserDetailVO userDetail = userMapper.selectUserWithDeptByXml(userId);
if (userDetail == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
return userDetail;
}
/**
* 2. 获取用户完整信息(包含所有关联数据)
*/
public UserDetailVO getUserFullInfo(Long userId) {
log.info("获取用户完整信息,用户ID: {}", userId);
// 第一步:查询用户基础信息
UserDetailVO userDetail = userMapper.selectUserWithStepByStep(userId);
if (userDetail == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
// 第二步:查询部门信息
if (userDetail.getDeptId() != null) {
DeptVO dept = deptMapper.selectById(userDetail.getDeptId());
userDetail.setDept(dept);
userDetail.setDeptName(dept.getDeptName());
userDetail.setDeptLeader(dept.getLeader());
}
// 第三步:查询角色信息
List<RoleVO> roles = roleMapper.selectRolesByUserId(userId);
userDetail.setRoles(roles);
// 设置角色名称和Key的字符串形式
if (!roles.isEmpty()) {
String roleNames = roles.stream()
.map(RoleVO::getRoleName)
.collect(Collectors.joining(","));
String roleKeys = roles.stream()
.map(RoleVO::getRoleKey)
.collect(Collectors.joining(","));
userDetail.setRoleNames(roleNames);
userDetail.setRoleKeys(roleKeys);
}
// 第四步:查询权限信息
List<PermissionVO> permissions = permissionMapper.selectPermissionsByUserId(userId);
userDetail.setPermissions(permissions);
// 设置权限Key列表
if (!permissions.isEmpty()) {
List<String> permissionKeys = permissions.stream()
.map(PermissionVO::getPermissionKey)
.collect(Collectors.toList());
userDetail.setPermissionKeys(permissionKeys);
}
log.info("用户完整信息查询完成,用户: {}", userDetail.getUserName());
return userDetail;
}
/**
* 3. 分页查询用户列表(包含关联信息)
*/
public PageResult<UserDetailVO> getUserPageWithAssociations(UserQueryDTO query) {
log.info("分页查询用户列表,查询条件: {}", query);
// 创建分页对象
Page<UserDetailVO> page = new Page<>(query.getPageNum(), query.getPageSize());
// 执行分页查询
List<UserDetailVO> userList = userMapper.selectUserPageWithAssociations(page, query);
// 如果需要,可以在这里补充其他关联数据
if (query.isIncludeRoles()) {
userList.forEach(user -> {
List<RoleVO> roles = roleMapper.selectRolesByUserId(user.getId());
user.setRoles(roles);
});
}
// 构建分页结果
PageResult<UserDetailVO> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setRecords(userList);
log.info("用户分页查询完成,总数: {}, 当前页记录数: {}", page.getTotal(), userList.size());
return pageResult;
}
/**
* 4. 根据部门查询用户列表
*/
public List<UserDetailVO> getUsersByDept(Long deptId, boolean includeChildren) {
log.info("根据部门查询用户列表,部门ID: {}, 包含子部门: {}", deptId, includeChildren);
List<Long> deptIds = new ArrayList<>();
deptIds.add(deptId);
// 如果包含子部门,查询所有子部门ID
if (includeChildren) {
List<Long> childDeptIds = deptMapper.selectChildrenDeptIds(deptId);
deptIds.addAll(childDeptIds);
}
// 构建查询条件
UserQueryDTO query = new UserQueryDTO();
query.setDeptIds(deptIds);
// 执行查询
List<UserDetailVO> users = userMapper.selectUsersByDeptIds(query);
log.info("部门用户查询完成,部门ID: {}, 用户数量: {}", deptId, users.size());
return users;
}
/**
* 5. 根据角色查询用户列表
*/
public List<UserDetailVO> getUsersByRole(String roleKey) {
log.info("根据角色查询用户列表,角色Key: {}", roleKey);
// 执行查询
List<UserDetailVO> users = userMapper.selectUsersByRoleKey(roleKey);
log.info("角色用户查询完成,角色Key: {}, 用户数量: {}", roleKey, users.size());
return users;
}
/**
* 6. 动态关联查询(按需加载关联数据)
*/
public UserDetailVO getUserWithDynamicAssociations(Long userId, boolean includeDept,
boolean includeRoles, boolean includePermissions) {
log.info("动态关联查询用户,用户ID: {}, 包含部门: {}, 包含角色: {}, 包含权限: {}",
userId, includeDept, includeRoles, includePermissions);
// 执行动态查询
UserDetailVO userDetail = userMapper.selectUserWithDynamicAssociations(
userId, includeDept, includeDept); // includeDept同时用于统计信息
if (userDetail == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
// 按需加载角色信息
if (includeRoles) {
List<RoleVO> roles = roleMapper.selectRolesByUserId(userId);
userDetail.setRoles(roles);
}
// 按需加载权限信息
if (includePermissions) {
List<PermissionVO> permissions = permissionMapper.selectPermissionsByUserId(userId);
userDetail.setPermissions(permissions);
}
return userDetail;
}
/**
* 7. 批量查询用户详情(优化N+1查询)
*/
public Map<Long, UserDetailVO> batchGetUserDetails(List<Long> userIds) {
log.info("批量查询用户详情,用户ID数量: {}", userIds.size());
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
// 批量查询用户基础信息
List<UserDetailVO> userList = userMapper.selectUserListByIds(userIds);
// 批量查询部门信息
List<Long> deptIds = userList.stream()
.map(UserDetailVO::getDeptId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, DeptVO> deptMap = Collections.emptyMap();
if (!deptIds.isEmpty()) {
List<DeptVO> depts = deptMapper.selectBatchIds(deptIds);
deptMap = depts.stream()
.collect(Collectors.toMap(DeptVO::getId, Function.identity()));
}
// 批量查询角色信息
Map<Long, List<RoleVO>> userRolesMap = roleMapper.selectRolesByUserIds(userIds);
// 组装数据
Map<Long, UserDetailVO> resultMap = new HashMap<>();
for (UserDetailVO user : userList) {
// 设置部门信息
if (user.getDeptId() != null && deptMap.containsKey(user.getDeptId())) {
DeptVO dept = deptMap.get(user.getDeptId());
user.setDept(dept);
user.setDeptName(dept.getDeptName());
user.setDeptLeader(dept.getLeader());
}
// 设置角色信息
if (userRolesMap.containsKey(user.getId())) {
List<RoleVO> roles = userRolesMap.get(user.getId());
user.setRoles(roles);
// 设置角色字符串
String roleNames = roles.stream()
.map(RoleVO::getRoleName)
.collect(Collectors.joining(","));
user.setRoleNames(roleNames);
}
resultMap.put(user.getId(), user);
}
log.info("批量用户详情查询完成,用户数量: {}", resultMap.size());
return resultMap;
}
/**
* 8. 用户权限验证
*/
public boolean checkUserPermission(Long userId, String permissionKey) {
log.debug("验证用户权限,用户ID: {}, 权限Key: {}", userId, permissionKey);
List<PermissionVO> permissions = permissionMapper.selectPermissionsByUserId(userId);
boolean hasPermission = permissions.stream()
.anyMatch(permission -> permissionKey.equals(permission.getPermissionKey()));
log.debug("用户权限验证结果: {} -> {}", permissionKey, hasPermission);
return hasPermission;
}
/**
* 9. 用户菜单树查询
*/
public List<MenuTreeNodeVO> getUserMenuTree(Long userId) {
log.info("查询用户菜单树,用户ID: {}", userId);
// 查询用户所有权限
List<PermissionVO> permissions = permissionMapper.selectPermissionsByUserId(userId);
// 过滤出菜单权限
List<PermissionVO> menuPermissions = permissions.stream()
.filter(p -> "M".equals(p.getPermissionType())) // M表示菜单
.sorted(Comparator.comparing(PermissionVO::getOrderNum))
.collect(Collectors.toList());
// 构建菜单树
List<MenuTreeNodeVO> menuTree = buildMenuTree(menuPermissions, 0L);
log.info("用户菜单树查询完成,菜单数量: {}", menuTree.size());
return menuTree;
}
private List<MenuTreeNodeVO> buildMenuTree(List<PermissionVO> permissions, Long parentId) {
return permissions.stream()
.filter(p -> Objects.equals(p.getParentId(), parentId))
.map(permission -> {
MenuTreeNodeVO node = new MenuTreeNodeVO();
node.setId(permission.getId());
node.setName(permission.getPermissionName());
node.setKey(permission.getPermissionKey());
node.setOrderNum(permission.getOrderNum());
// 递归构建子节点
List<MenuTreeNodeVO> children = buildMenuTree(permissions, permission.getId());
node.setChildren(children);
return node;
})
.sorted(Comparator.comparing(MenuTreeNodeVO::getOrderNum))
.collect(Collectors.toList());
}
}
/**
* 菜单树节点VO
*/
@Data
class MenuTreeNodeVO {
private Long id;
private String name;
private String key;
private Integer orderNum;
private List<MenuTreeNodeVO> children;
}
7.6 关联查询性能优化方案
/**
* 关联查询性能优化方案
* 重点:解决N+1查询问题,提升关联查询性能
*/
@Service
@Slf4j
public class AssociationQueryOptimizationService {
@Autowired
private UserMapper userMapper;
@Autowired
private DeptMapper deptMapper;
@Autowired
private RoleMapper roleMapper;
/**
* 1. 解决N+1查询问题 - JOIN方式
* 重点:使用JOIN一次性查询所有数据,避免多次查询
*/
public List<UserDetailVO> getUserListWithJoin() {
log.info("使用JOIN方式查询用户列表(解决N+1问题)");
// 一次性JOIN查询所有需要的数据
List<UserDetailVO> users = userMapper.selectUserListWithJoin();
log.info("JOIN查询完成,用户数量: {}", users.size());
return users;
}
/**
* 2. 解决N+1查询问题 - 批量查询方式
* 重点:先查询主表,再批量查询关联表
*/
public List<UserDetailVO> getUserListWithBatchQuery() {
log.info("使用批量查询方式查询用户列表(解决N+1问题)");
// 第一步:查询所有用户
List<UserDetailVO> users = userMapper.selectUserListWithStepByStep(null);
// 第二步:批量查询部门信息
List<Long> deptIds = users.stream()
.map(UserDetailVO::getDeptId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, DeptVO> deptMap = Collections.emptyMap();
if (!deptIds.isEmpty()) {
List<DeptVO> depts = deptMapper.selectBatchIds(deptIds);
deptMap = depts.stream()
.collect(Collectors.toMap(DeptVO::getId, Function.identity()));
}
// 第三步:批量查询角色信息
List<Long> userIds = users.stream()
.map(UserDetailVO::getId)
.collect(Collectors.toList());
Map<Long, List<RoleVO>> userRolesMap = roleMapper.selectRolesByUserIds(userIds);
// 第四步:组装数据
for (UserDetailVO user : users) {
// 设置部门信息
if (user.getDeptId() != null && deptMap.containsKey(user.getDeptId())) {
DeptVO dept = deptMap.get(user.getDeptId());
user.setDept(dept);
user.setDeptName(dept.getDeptName());
}
// 设置角色信息
if (userRolesMap.containsKey(user.getId())) {
user.setRoles(userRolesMap.get(user.getId()));
}
}
log.info("批量查询完成,用户数量: {}", users.size());
return users;
}
/**
* 3. 延迟加载优化
* 重点:按需加载关联数据,减少不必要的查询
*/
public UserDetailVO getUserWithLazyLoading(Long userId, Set<String> includeFields) {
log.info("使用延迟加载查询用户,用户ID: {}, 包含字段: {}", userId, includeFields);
UserDetailVO userDetail = new UserDetailVO();
// 总是加载基础信息
UserDetailVO baseInfo = userMapper.selectUserBaseInfo(userId);
if (baseInfo == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
BeanUtils.copyProperties(baseInfo, userDetail);
// 按需加载部门信息
if (includeFields.contains("dept")) {
DeptVO dept = deptMapper.selectById(userDetail.getDeptId());
userDetail.setDept(dept);
}
// 按需加载角色信息
if (includeFields.contains("roles")) {
List<RoleVO> roles = roleMapper.selectRolesByUserId(userId);
userDetail.setRoles(roles);
}
// 按需加载权限信息
if (includeFields.contains("permissions")) {
List<PermissionVO> permissions = permissionMapper.selectPermissionsByUserId(userId);
userDetail.setPermissions(permissions);
}
return userDetail;
}
/**
* 4. 分页查询优化 - 延迟关联
* 重点:先分页查询ID,再关联查询详细数据
*/
public PageResult<UserDetailVO> getUserPageWithDeferredJoin(UserQueryDTO query) {
log.info("使用延迟关联分页查询用户列表");
// 第一步:分页查询用户ID
Page<User> idPage = new Page<>(query.getPageNum(), query.getPageSize());
QueryWrapper<User> idWrapper = new QueryWrapper<>();
idWrapper.select("id")
.eq("deleted", 0);
// 添加查询条件
if (StringUtils.isNotBlank(query.getUserName())) {
idWrapper.like("user_name", query.getUserName());
}
if (query.getDeptId() != null) {
idWrapper.eq("dept_id", query.getDeptId());
}
Page<User> idResult = userMapper.selectPage(idPage, idWrapper);
List<Long> userIds = idResult.getRecords().stream()
.map(User::getId)
.collect(Collectors.toList());
if (userIds.isEmpty()) {
return new PageResult<>(query.getPageNum(), query.getPageSize());
}
// 第二步:根据ID列表查询完整数据(包含关联信息)
List<UserDetailVO> userDetails = userMapper.selectUserListWithAssociationsByIds(userIds);
// 第三步:组装分页结果
PageResult<UserDetailVO> pageResult = new PageResult<>();
pageResult.setCurrent(idResult.getCurrent());
pageResult.setSize(idResult.getSize());
pageResult.setTotal(idResult.getTotal());
pageResult.setRecords(userDetails);
log.info("延迟关联分页查询完成,总数: {}, 当前页: {}", pageResult.getTotal(), userDetails.size());
return pageResult;
}
/**
* 5. 缓存优化
* 重点:使用缓存减少数据库查询
*/
@Cacheable(value = "user:detail", key = "#userId")
public UserDetailVO getUserDetailWithCache(Long userId) {
log.info("查询用户详情(使用缓存),用户ID: {}", userId);
return getUserFullInfo(userId);
}
/**
* 6. 索引优化建议
*/
public void suggestIndexes() {
log.info("""
==================== 关联查询索引优化建议 ====================
1. 用户表索引:
- PRIMARY KEY (id)
- INDEX idx_user_dept (dept_id, deleted)
- INDEX idx_user_status (status, deleted)
- INDEX idx_user_create_time (create_time)
2. 部门表索引:
- PRIMARY KEY (id)
- INDEX idx_dept_parent (parent_id)
- INDEX idx_dept_status (status, deleted)
3. 用户角色关联表索引:
- UNIQUE KEY uk_user_role (user_id, role_id)
- INDEX idx_role_user (role_id, user_id)
4. 角色权限关联表索引:
- UNIQUE KEY uk_role_permission (role_id, permission_id)
- INDEX idx_permission_role (permission_id, role_id)
""");
}
/**
* 7. 查询执行计划分析
*/
public void analyzeQueryExecutionPlan(String sql) {
log.info("分析查询执行计划,SQL: {}", sql);
// 在实际项目中,这里可以:
// 1. 使用EXPLAIN分析SQL执行计划
// 2. 检查是否使用了正确的索引
// 3. 识别全表扫描和性能瓶颈
// 4. 提供优化建议
log.info("""
执行计划分析要点:
1. 检查type列:应该避免ALL(全表扫描)
2. 检查key列:确保使用了正确的索引
3. 检查rows列:预估扫描行数应该尽可能少
4. 检查Extra列:避免Using filesort和Using temporary
""");
}
}
企业级MyBatis-Plus实战教学(续)
模块八:事务管理与批量操作优化
8.1 Spring事务管理深度解析
8.1.1 事务配置完整实现
/**
* 事务管理完整配置详解
* 重点:Spring事务的完整配置和最佳实践
*/
@Configuration
@EnableTransactionManagement // 开启注解事务管理
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象,解决内部调用事务失效问题
@Slf4j
public class TransactionConfig {
/**
* 事务管理器配置
* 重点:配置DataSourceTransactionManager,这是Spring事务管理的核心
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
// 设置数据源
transactionManager.setDataSource(dataSource);
// 设置全局超时时间(单位:秒)
transactionManager.setDefaultTimeout(30);
// 设置是否允许嵌套事务
transactionManager.setNestedTransactionAllowed(true);
// 设置是否在回滚时设置全局回滚标记
transactionManager.setGlobalRollbackOnParticipationFailure(true);
log.info("数据源事务管理器配置完成 - 超时时间: {}秒, 允许嵌套事务: {}",
30, true);
return transactionManager;
}
/**
* 事务模板配置
* 重点:用于编程式事务管理
*/
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate();
// 设置事务管理器
transactionTemplate.setTransactionManager(transactionManager);
// 设置传播行为
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 设置隔离级别
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 设置超时时间
transactionTemplate.setTimeout(30);
// 设置是否只读
transactionTemplate.setReadOnly(false);
log.info("事务模板配置完成 - 传播行为: REQUIRED, 隔离级别: READ_COMMITTED");
return transactionTemplate;
}
/**
* 事务拦截器配置
* 重点:自定义事务拦截逻辑
*/
@Bean
public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
// 事务属性配置
NameMatchTransactionAttributeSource attributeSource = new NameMatchTransactionAttributeSource();
Map<String, TransactionAttribute> txMap = new HashMap<>();
// ==================== 只读事务配置 ====================
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// ==================== 写事务配置 ====================
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
requiredTx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
requiredTx.setTimeout(30);
// 设置回滚规则
List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
rollbackRules.add(new RollbackRuleAttribute(Exception.class));
requiredTx.setRollbackRules(rollbackRules);
// ==================== 新事务配置 ====================
RuleBasedTransactionAttribute requiresNewTx = new RuleBasedTransactionAttribute();
requiresNewTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
requiresNewTx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
requiresNewTx.setTimeout(30);
requiresNewTx.setRollbackRules(rollbackRules);
// ==================== 方法匹配规则 ====================
// 查询方法 - 只读事务
txMap.put("get*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("select*", readOnlyTx);
txMap.put("count*", readOnlyTx);
txMap.put("list*", readOnlyTx);
txMap.put("search*", readOnlyTx);
// 新增方法 - 写事务
txMap.put("save*", requiredTx);
txMap.put("add*", requiredTx);
txMap.put("create*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("register*", requiredTx);
// 更新方法 - 写事务
txMap.put("update*", requiredTx);
txMap.put("modify*", requiredTx);
txMap.put("change*", requiredTx);
txMap.put("set*", requiredTx);
// 删除方法 - 写事务
txMap.put("delete*", requiredTx);
txMap.put("remove*", requiredTx);
txMap.put("cancel*", requiredTx);
// 批量操作 - 写事务
txMap.put("batch*", requiredTx);
txMap.put("process*", requiredTx);
// 特殊方法 - 新事务
txMap.put("async*", requiresNewTx);
txMap.put("background*", requiresNewTx);
attributeSource.setNameMap(txMap);
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionManager(transactionManager);
interceptor.setTransactionAttributeSource(attributeSource);
log.info("事务拦截器配置完成 - 配置了 {} 个方法匹配规则", txMap.size());
return interceptor;
}
}
/**
* 事务监控配置
* 重点:监控事务执行情况,用于性能分析和问题排查
*/
@Configuration
@Slf4j
public class TransactionMonitorConfig {
/**
* 事务监控切面
*/
@Bean
public TransactionMonitorAspect transactionMonitorAspect() {
return new TransactionMonitorAspect();
}
/**
* 事务监控切面实现
*/
@Aspect
@Component
@Slf4j
public static class TransactionMonitorAspect {
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* 监控所有@Transactional注解的方法
*/
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
startTime.set(System.currentTimeMillis());
log.debug("事务开始 - 方法: {}", methodName);
try {
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime.get();
log.debug("事务提交成功 - 方法: {}, 耗时: {}ms", methodName, costTime);
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime.get();
log.error("事务回滚 - 方法: {}, 耗时: {}ms, 异常: {}",
methodName, costTime, e.getMessage(), e);
throw e;
} finally {
startTime.remove();
}
}
}
}
8.1.2 事务传播行为深度解析
/**
* 事务传播行为深度解析
* 重点:七种传播行为的详细说明和实战应用
*/
@Service
@Slf4j
@Transactional
public class TransactionPropagationService {
@Autowired
private UserService userService;
@Autowired
private DeptService deptService;
@Autowired
private TransactionPropagationService selfProxy; // 用于解决内部调用事务失效问题
/**
* 1. REQUIRED(默认)- 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
* 使用场景:大多数业务方法
*/
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample() {
log.info("REQUIRED传播行为 - 当前方法将在事务中执行");
// 业务操作1
userService.updateUserStatus(1L, 1);
// 业务操作2 - 在同一个事务中
deptService.updateDeptInfo(1L, "技术部");
// 如果这里抛出异常,两个操作都会回滚
// throw new RuntimeException("测试异常");
}
/**
* 2. REQUIRES_NEW - 创建一个新的事务,如果当前存在事务,则把当前事务挂起
* 使用场景:需要独立事务的操作,如日志记录、消息发送
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample() {
log.info("REQUIRES_NEW传播行为 - 创建独立事务执行");
// 这个方法的操作在独立事务中执行
userService.recordOperationLog("REQUIRES_NEW示例");
// 即使外部事务回滚,这个方法的操作也不会回滚
}
/**
* 3. NESTED - 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行
* 使用场景:可独立回滚的子操作
*/
@Transactional(propagation = Propagation.NESTED)
public void nestedExample() {
log.info("NESTED传播行为 - 在嵌套事务中执行");
// 嵌套事务可以独立回滚,不影响外部事务
userService.updateUserProfile(1L, "新的个人资料");
// 如果这里抛出异常,只会回滚这个嵌套事务,外部事务继续执行
// throw new RuntimeException("嵌套事务异常");
}
/**
* 4. SUPPORTS - 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
* 使用场景:查询操作,可以适应调用方的事务状态
*/
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public User supportsExample(Long userId) {
log.info("SUPPORTS传播行为 - 有事务则加入,没有则非事务运行");
// 这个方法可以适应调用方的事务状态
return userService.getUserById(userId);
}
/**
* 5. NOT_SUPPORTED - 以非事务方式运行,如果当前存在事务,则把当前事务挂起
* 使用场景:不需要事务的操作,如数据导出、文件操作
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample() {
log.info("NOT_SUPPORTED传播行为 - 非事务方式运行");
// 这个方法总是在非事务环境下执行
userService.exportUserData();
}
/**
* 6. MANDATORY - 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
* 使用场景:必须在事务中调用的方法
*/
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample() {
log.info("MANDATORY传播行为 - 必须在事务中运行");
// 如果调用方没有事务,会抛出IllegalTransactionStateException
userService.criticalBusinessOperation();
}
/**
* 7. NEVER - 以非事务方式运行,如果当前存在事务,则抛出异常
* 使用场景:绝对不能在有事务的情况下调用的方法
*/
@Transactional(propagation = Propagation.NEVER)
public void neverExample() {
log.info("NEVER传播行为 - 必须在非事务中运行");
// 如果调用方有事务,会抛出IllegalTransactionStateException
userService.nonTransactionalOperation();
}
/**
* 复杂事务场景示例:混合使用不同传播行为
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void complexTransactionScenario() {
log.info("复杂事务场景 - 开始执行");
// 1. 主业务操作(在外部事务中)
userService.updateUserStatus(1L, 1);
try {
// 2. 独立事务操作:即使这里失败,也不影响外部事务
selfProxy.independentOperation();
} catch (Exception e) {
log.error("独立操作失败,但外部事务继续", e);
// 这里捕获异常,防止影响外部事务
}
try {
// 3. 嵌套事务操作:可以独立回滚
selfProxy.nestedOperation();
} catch (Exception e) {
log.error("嵌套操作失败,但外部事务继续", e);
// 嵌套事务独立回滚,不影响外部事务
}
// 4. 非事务操作
selfProxy.nonTransactionalOperation();
// 如果这里抛出异常,只有主业务操作会回滚
log.info("复杂事务场景 - 执行完成");
}
/**
* 独立事务操作
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void independentOperation() {
log.info("独立事务操作 - 开始");
deptService.updateDeptInfo(1L, "独立事务更新的部门");
// 模拟可能失败的独立操作
if (System.currentTimeMillis() % 2 == 0) {
throw new RuntimeException("独立事务操作失败");
}
log.info("独立事务操作 - 完成");
}
/**
* 嵌套事务操作
*/
@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
log.info("嵌套事务操作 - 开始");
userService.updateUserProfile(1L, "嵌套事务更新的资料");
// 模拟可能失败的嵌套操作
if (System.currentTimeMillis() % 3 == 0) {
throw new RuntimeException("嵌套事务操作失败");
}
log.info("嵌套事务操作 - 完成");
}
/**
* 非事务操作
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void nonTransactionalOperation() {
log.info("非事务操作 - 开始");
// 执行不需要事务的操作
userService.recordAccessLog();
log.info("非事务操作 - 完成");
}
/**
* 事务传播行为总结表
*/
public void printPropagationSummary() {
log.info("""
==================== 事务传播行为总结 ====================
传播行为 当前有事务 当前无事务
REQUIRED(默认) 加入当前事务 创建新事务
REQUIRES_NEW 挂起当前事务 创建新事务
NESTED 嵌套事务执行 创建新事务
SUPPORTS 加入当前事务 非事务执行
NOT_SUPPORTED 挂起当前事务 非事务执行
MANDATORY 加入当前事务 抛出异常
NEVER 抛出异常 非事务执行
======================================================
""");
}
}
8.1.3 事务隔离级别深度解析
/**
* 事务隔离级别深度解析
* 重点:四种隔离级别解决的数据并发问题
*/
@Service
@Slf4j
@Transactional
public class TransactionIsolationService {
@Autowired
private UserService userService;
@Autowired
private AccountService accountService;
/**
* 1. READ_UNCOMMITTED(读未提交)
* 问题:可能读取到其他事务未提交的数据(脏读)
* 使用场景:对数据一致性要求不高的场景,如统计报表
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
log.info("READ_UNCOMMITTED隔离级别 - 开始事务");
// 第一次读取
User user1 = userService.getUserById(1L);
log.info("第一次读取用户: {}", user1);
// 模拟其他事务修改了数据但未提交
simulateConcurrentUncommittedUpdate();
// 第二次读取(可能读取到未提交的数据 - 脏读)
User user2 = userService.getUserById(1L);
log.info("第二次读取用户: {}", user2);
log.info("READ_UNCOMMITTED隔离级别 - 可能发生脏读");
}
/**
* 2. READ_COMMITTED(读已提交)- Oracle、SQL Server默认级别
* 解决:脏读问题
* 问题:不可重复读(同一事务中多次读取同一数据可能结果不同)
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
log.info("READ_COMMITTED隔离级别 - 开始事务");
// 第一次读取
User user1 = userService.getUserById(1L);
log.info("第一次读取用户: {}", user1);
// 模拟其他事务提交了数据修改
simulateConcurrentCommittedUpdate();
// 第二次读取(可能得到不同的结果 - 不可重复读)
User user2 = userService.getUserById(1L);
log.info("第二次读取用户: {}", user2);
log.info("READ_COMMITTED隔离级别 - 解决脏读,可能发生不可重复读");
}
/**
* 3. REPEATABLE_READ(可重复读)- MySQL默认级别
* 解决:脏读、不可重复读问题
* 问题:幻读(同一事务中多次查询的结果集数量可能不同)
*/
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() {
log.info("REPEATABLE_READ隔离级别 - 开始事务");
// 第一次查询用户列表
List<User> users1 = userService.getUserList();
log.info("第一次查询用户数量: {}", users1.size());
// 模拟其他事务插入了新用户
simulateConcurrentInsert();
// 第二次查询用户列表(可能得到不同的结果集数量 - 幻读)
List<User> users2 = userService.getUserList();
log.info("第二次查询用户数量: {}", users2.size());
log.info("REPEATABLE_READ隔离级别 - 解决脏读、不可重复读,可能发生幻读");
}
/**
* 4. SERIALIZABLE(串行化)
* 解决:所有并发问题(脏读、不可重复读、幻读)
* 问题:性能最低,完全串行执行
*/
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
log.info("SERIALIZABLE隔离级别 - 开始事务");
// 这个事务会串行执行,不会受到其他事务的干扰
List<User> users = userService.getUserList();
log.info("查询用户数量: {}", users.size());
// 执行更新操作
userService.updateUserStatus(1L, 1);
log.info("SERIALIZABLE隔离级别 - 解决所有并发问题,性能最低");
}
/**
* 5. 默认隔离级别(使用数据库默认设置)
*/
@Transactional(isolation = Isolation.DEFAULT)
public void defaultIsolationExample() {
log.info("DEFAULT隔离级别 - 使用数据库默认隔离级别");
User user = userService.getUserById(1L);
log.info("读取用户: {}", user);
// 更新用户信息
userService.updateUserProfile(1L, "默认隔离级别测试");
log.info("DEFAULT隔离级别 - 操作完成");
}
/**
* 转账业务示例 - 使用合适的隔离级别
*/
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public boolean transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
log.info("开始转账业务 - 从账户 {} 向账户 {} 转账 {}", fromAccountId, toAccountId, amount);
// 1. 检查转出账户余额(需要读取最新提交的数据)
Account fromAccount = accountService.getAccountById(fromAccountId);
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("账户余额不足");
}
// 2. 检查转入账户状态
Account toAccount = accountService.getAccountById(toAccountId);
if (toAccount.getStatus() != 1) {
throw new RuntimeException("转入账户状态异常");
}
// 3. 执行转账操作
boolean deductSuccess = accountService.deductBalance(fromAccountId, amount);
boolean addSuccess = accountService.addBalance(toAccountId, amount);
if (!deductSuccess || !addSuccess) {
throw new RuntimeException("转账操作失败");
}
// 4. 记录转账流水
accountService.recordTransferLog(fromAccountId, toAccountId, amount);
log.info("转账业务完成");
return true;
}
/**
* 模拟并发未提交更新
*/
private void simulateConcurrentUncommittedUpdate() {
// 在实际项目中,这里会有另一个事务执行更新但未提交
new Thread(() -> {
log.info("并发线程 - 开始未提交更新");
// 这里应该启动一个新事务执行更新但不提交
}).start();
try {
Thread.sleep(100); // 给并发线程一些时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 模拟并发已提交更新
*/
private void simulateConcurrentCommittedUpdate() {
new Thread(() -> {
log.info("并发线程 - 开始已提交更新");
// 这里应该启动一个新事务执行更新并提交
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 模拟并发插入
*/
private void simulateConcurrentInsert() {
new Thread(() -> {
log.info("并发线程 - 开始插入新数据");
// 这里应该启动一个新事务执行插入
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 隔离级别与并发问题总结
*/
public void printIsolationSummary() {
log.info("""
==================== 事务隔离级别总结 ====================
隔离级别 脏读 不可重复读 幻读 性能
READ_UNCOMMITTED 可能 可能 可能 最高
READ_COMMITTED 不会 可能 可能 较高
REPEATABLE_READ 不会 不会 可能 中等
SERIALIZABLE 不会 不会 不会 最低
======================================================
选择建议:
1. 数据一致性要求高:REPEATABLE_READ 或 SERIALIZABLE
2. 高并发查询场景:READ_COMMITTED
3. 统计报表场景:READ_UNCOMMITTED
4. 默认选择:数据库默认级别
""");
}
}
8.2 声明式事务企业级实践
/**
* 声明式事务企业级实践
* 重点:@Transactional注解的各种用法和最佳实践
*/
@Service
@Slf4j
@Transactional(readOnly = true) // 类级别默认只读事务
public class DeclarativeTransactionService {
@Autowired
private UserMapper userMapper;
@Autowired
private DeptMapper deptMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private OperationLogMapper logMapper;
/**
* 1. 只读事务 - 用于查询操作
* 重点:readOnly = true 可以优化查询性能,避免不必要的写锁
*/
@Transactional(readOnly = true)
public User getUserById(Long userId) {
log.debug("只读事务 - 查询用户,用户ID: {}", userId);
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
return user;
}
/**
* 2. 写事务 - 用于数据修改操作
* 重点:readOnly = false(默认),遇到异常自动回滚
*/
@Transactional(readOnly = false, rollbackFor = Exception.class)
public boolean updateUser(User user) {
log.info("写事务 - 更新用户,用户ID: {}", user.getId());
// 检查用户是否存在
User existingUser = userMapper.selectById(user.getId());
if (existingUser == null) {
throw new RuntimeException("用户不存在,ID: " + user.getId());
}
// 执行更新
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
throw new RuntimeException("更新用户失败");
}
// 记录操作日志
recordOperationLog("UPDATE_USER", "更新用户信息", user.getId());
log.info("用户更新成功,用户ID: {}", user.getId());
return true;
}
/**
* 3. 自定义回滚规则
* 重点:指定哪些异常回滚,哪些异常不回滚
*/
@Transactional(rollbackFor = {RuntimeException.class, Exception.class},
noRollbackFor = {BusinessException.class})
public boolean updateUserWithCustomRollback(User user) {
log.info("自定义回滚规则 - 更新用户,用户ID: {}", user.getId());
try {
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
throw new BusinessException("业务异常,用户可能不存在"); // 不会触发回滚
}
// 模拟可能抛出系统异常的情况
if (user.getUserName().contains("admin")) {
throw new RuntimeException("系统异常,触发回滚"); // 会触发回滚
}
return true;
} catch (BusinessException e) {
log.warn("业务异常,继续执行: {}", e.getMessage());
return false;
}
}
/**
* 4. 超时设置 - 防止长时间事务
*/
@Transactional(timeout = 10, rollbackFor = Exception.class) // 10秒超时
public boolean updateUserWithTimeout(User user) {
log.info("超时事务 - 更新用户,用户ID: {}", user.getId());
// 模拟耗时操作
try {
Thread.sleep(5000); // 5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("操作被中断", e);
}
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
throw new RuntimeException("更新用户失败");
}
// 如果总操作时间超过10秒,会抛出TransactionTimedOutException
return true;
}
/**
* 5. 复杂业务事务 - 多表操作
*/
@Transactional(rollbackFor = Exception.class)
public boolean createUserWithRoles(User user, List<Long> roleIds) {
log.info("复杂业务事务 - 创建用户并分配角色,用户名: {}", user.getUserName());
// 1. 验证用户名唯一性
if (isUserNameExists(user.getUserName())) {
throw new BusinessException("用户名已存在: " + user.getUserName());
}
// 2. 创建用户
int userResult = userMapper.insert(user);
if (userResult == 0) {
throw new RuntimeException("创建用户失败");
}
log.info("用户创建成功,用户ID: {}", user.getId());
// 3. 分配角色
if (roleIds != null && !roleIds.isEmpty()) {
for (Long roleId : roleIds) {
UserRole userRole = new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(roleId);
int roleResult = userRoleMapper.insert(userRole);
if (roleResult == 0) {
throw new RuntimeException("分配角色失败,角色ID: " + roleId);
}
}
log.info("角色分配成功,分配 {} 个角色", roleIds.size());
}
// 4. 记录操作日志
recordOperationLog("CREATE_USER", "创建用户并分配角色", user.getId());
log.info("创建用户并分配角色成功,用户ID: {}", user.getId());
return true;
}
/**
* 6. 嵌套事务示例
*/
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public boolean nestedTransactionOperation(User user) {
log.info("嵌套事务操作 - 更新用户,用户ID: {}", user.getId());
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
// 嵌套事务可以单独回滚,不影响外部事务
throw new RuntimeException("嵌套事务操作失败");
}
log.info("嵌套事务操作成功");
return true;
}
/**
* 7. 手动回滚示例
*/
@Transactional(rollbackFor = Exception.class)
public boolean manualRollbackExample(User user) {
log.info("手动回滚示例 - 更新用户,用户ID: {}", user.getId());
try {
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
// 手动设置回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.warn("更新失败,手动设置回滚");
return false;
}
// 业务逻辑检查
if (!isBusinessValid(user)) {
// 手动设置回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.warn("业务验证失败,手动设置回滚");
return false;
}
return true;
} catch (Exception e) {
// 异常时会自动回滚
log.error("操作异常", e);
throw e;
}
}
/**
* 8. 编程式事务示例
*/
@Autowired
private TransactionTemplate transactionTemplate;
public boolean programmaticTransaction(User user) {
log.info("编程式事务 - 更新用户,用户ID: {}", user.getId());
return transactionTemplate.execute(status -> {
try {
// 业务操作1
int affectedRows = userMapper.updateById(user);
if (affectedRows == 0) {
status.setRollbackOnly(); // 手动回滚
return false;
}
// 业务操作2
recordOperationLog("UPDATE_USER", "编程式事务更新用户", user.getId());
return true;
} catch (Exception e) {
status.setRollbackOnly(); // 手动回滚
log.error("编程式事务执行失败", e);
throw new RuntimeException("事务执行失败", e);
}
});
}
/**
* 9. 事务方法调用注意事项
* 重点:在同一个类中调用事务方法,事务不会生效(代理问题)
*/
public void transactionCallWarning() {
log.info("事务方法调用警告示例");
// 错误调用:在同一个类中调用事务方法,事务不会生效
User user = new User();
user.setId(1L);
user.setUserName("test");
// 这种方式事务不会生效!
this.updateUser(user);
log.warn("注意:在同一个类中调用事务方法,事务不会生效!");
// 正确做法:
// 1. 将事务方法放在另一个Service中调用
// 2. 使用AopContext.currentProxy()获取代理对象
// 3. 使用@Autowired注入自身代理(需要@EnableAspectJAutoProxy(exposeProxy = true))
}
// ==================== 辅助方法 ====================
/**
* 检查用户名是否存在
*/
private boolean isUserNameExists(String userName) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", userName)
.eq("deleted", 0);
Long count = userMapper.selectCount(queryWrapper);
return count > 0;
}
/**
* 业务验证
*/
private boolean isBusinessValid(User user) {
// 实际业务验证逻辑
return user.getUserName() != null && !user.getUserName().trim().isEmpty();
}
/**
* 记录操作日志
*/
private void recordOperationLog(String operationType, String description, Long targetId) {
OperationLog log = new OperationLog();
log.setOperationType(operationType);
log.setDescription(description);
log.setTargetId(targetId);
log.setOperator(getCurrentUser());
log.setOperationTime(LocalDateTime.now());
logMapper.insert(log);
}
/**
* 获取当前用户
*/
private String getCurrentUser() {
// 实际项目中从SecurityContext获取
return "system";
}
}
/**
* 自定义业务异常(不回滚)
*/
class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 操作日志实体
*/
@Data
@TableName("sys_operation_log")
class OperationLog {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String operationType;
private String description;
private Long targetId;
private String operator;
private LocalDateTime operationTime;
}
8.3 批量操作优化企业级实践
8.3.1 批量插入优化
/**
* 批量插入优化企业级实践
* 重点:各种批量插入方案的性能对比和最佳实践
*/
@Service
@Slf4j
@Transactional
public class BatchInsertService {
@Autowired
private UserMapper userMapper;
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 1. 使用MyBatis-Plus的saveBatch方法
* 重点:适合小批量数据插入(1000条以内),内部自动分批
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchInsertWithSaveBatch(List<User> users) {
log.info("使用saveBatch批量插入,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
// MyBatis-Plus的saveBatch方法内部会分批处理
// 第三个参数batchSize表示每批的数量,默认1000
boolean success = userService.saveBatch(users, 1000);
long costTime = System.currentTimeMillis() - startTime;
log.info("saveBatch批量插入完成,数据量: {}, 耗时: {} ms, 性能: {}/s",
users.size(), costTime, calculatePerformance(users.size(), costTime));
return success;
}
/**
* 2. 使用MyBatis-Plus的saveBatch方法(自定义批次大小)
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchInsertWithCustomBatchSize(List<User> users, int batchSize) {
log.info("使用saveBatch批量插入(自定义批次大小),数据量: {}, 批次大小: {}",
users.size(), batchSize);
long startTime = System.currentTimeMillis();
boolean success = userService.saveBatch(users, batchSize);
long costTime = System.currentTimeMillis() - startTime;
log.info("saveBatch批量插入完成,数据量: {}, 批次: {}, 耗时: {} ms",
users.size(), batchSize, costTime);
return success;
}
/**
* 3. 使用MyBatis的BatchExecutor(最高性能)
* 重点:适合大批量数据插入(万级以上)
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchInsertWithBatchExecutor(List<User> users) {
log.info("使用BatchExecutor批量插入,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
// 获取批量模式的SqlSession
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
.openSession(ExecutorType.BATCH);
try {
UserMapper batchMapper = sqlSession.getMapper(UserMapper.class);
int count = 0;
for (User user : users) {
batchMapper.insert(user);
count++;
// 每1000条提交一次,防止内存溢出
if (count % 1000 == 0) {
sqlSession.commit();
sqlSession.clearCache(); // 清理缓存,防止内存溢出
log.debug("已提交 {} 条数据", count);
}
}
// 提交剩余数据
sqlSession.commit();
long costTime = System.currentTimeMillis() - startTime;
log.info("BatchExecutor批量插入完成,总数据量: {}, 耗时: {} ms, 性能: {}/s",
users.size(), costTime, calculatePerformance(users.size(), costTime));
return true;
} catch (Exception e) {
sqlSession.rollback();
log.error("BatchExecutor批量插入失败", e);
throw new RuntimeException("批量插入失败", e);
} finally {
sqlSession.close();
}
}
/**
* 4. 使用SQL的foreach语句(单条INSERT多值)
* 重点:MySQL有SQL长度限制(max_allowed_packet),需要分批处理
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchInsertWithForeachSql(List<User> users) {
log.info("使用foreach SQL批量插入,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
// 分批处理,每批500条(避免SQL过长)
int batchSize = 500;
int total = users.size();
int batchCount = (total + batchSize - 1) / batchSize;
int totalInserted = 0;
for (int i = 0; i < batchCount; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, total);
List<User> batchList = users.subList(fromIndex, toIndex);
// 执行批量插入
int affectedRows = userMapper.batchInsert(batchList);
totalInserted += affectedRows;
log.debug("第 {} 批插入完成,插入 {} 条数据", i + 1, affectedRows);
}
long costTime = System.currentTimeMillis() - startTime;
log.info("foreach SQL批量插入完成,总数据量: {}, 成功插入: {}, 耗时: {} ms",
users.size(), totalInserted, costTime);
return totalInserted == users.size();
}
/**
* 5. 使用编程式事务的批量插入
* 重点:更灵活的事务控制
*/
public boolean batchInsertWithProgrammaticTransaction(List<User> users) {
log.info("使用编程式事务批量插入,数据量: {}", users.size());
return transactionTemplate.execute(status -> {
try {
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
.openSession(ExecutorType.BATCH);
try {
UserMapper batchMapper = sqlSession.getMapper(UserMapper.class);
int count = 0;
for (User user : users) {
batchMapper.insert(user);
count++;
if (count % 1000 == 0) {
sqlSession.commit();
sqlSession.clearCache();
}
}
sqlSession.commit();
log.info("编程式事务批量插入成功,数据量: {}", users.size());
return true;
} catch (Exception e) {
sqlSession.rollback();
status.setRollbackOnly(); // 标记事务回滚
throw new RuntimeException("批量插入失败", e);
} finally {
sqlSession.close();
}
} catch (Exception e) {
log.error("编程式事务批量插入失败", e);
throw e;
}
});
}
/**
* 6. 批量插入性能测试
*/
public void performanceTest() {
log.info("开始批量插入性能测试");
// 生成测试数据
List<User> testData = generateTestData(10000);
// 测试1:MyBatis-Plus saveBatch
long start1 = System.currentTimeMillis();
batchInsertWithSaveBatch(new ArrayList<>(testData));
long time1 = System.currentTimeMillis() - start1;
// 测试2:BatchExecutor
long start2 = System.currentTimeMillis();
batchInsertWithBatchExecutor(new ArrayList<>(testData));
long time2 = System.currentTimeMillis() - start2;
// 测试3:foreach SQL
long start3 = System.currentTimeMillis();
batchInsertWithForeachSql(new ArrayList<>(testData));
long time3 = System.currentTimeMillis() - start3;
log.info("""
==================== 批量插入性能测试结果 ====================
测试数据量: {}
MyBatis-Plus saveBatch: {} ms
BatchExecutor: {} ms
foreach SQL: {} ms
=========================================================
""", testData.size(), time1, time2, time3);
}
/**
* 7. 内存优化批量插入(适合超大数据量)
*/
public boolean memoryOptimizedBatchInsert(List<User> users, int batchSize) {
log.info("内存优化批量插入,总数据量: {}, 批次大小: {}", users.size(), batchSize);
long startTime = System.currentTimeMillis();
int totalBatches = (users.size() + batchSize - 1) / batchSize;
for (int i = 0; i < totalBatches; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, users.size());
List<User> batch = users.subList(fromIndex, toIndex);
// 每批使用独立事务
transactionTemplate.execute(status -> {
userService.saveBatch(batch, batch.size());
return null;
});
// 手动触发GC(谨慎使用)
if (i % 10 == 0) {
System.gc();
}
log.debug("已完成第 {}/{} 批插入", i + 1, totalBatches);
}
long costTime = System.currentTimeMillis() - startTime;
log.info("内存优化批量插入完成,总数据量: {}, 耗时: {} ms", users.size(), costTime);
return true;
}
// ==================== 辅助方法 ====================
/**
* 计算性能(条/秒)
*/
private String calculatePerformance(int dataSize, long costTime) {
if (costTime == 0) {
return "N/A";
}
double performance = (double) dataSize / costTime * 1000;
return String.format("%.2f", performance);
}
/**
* 生成测试数据
*/
private List<User> generateTestData(int count) {
List<User> users = new ArrayList<>();
for (int i = 0; i < count; i++) {
User user = new User();
user.setUserName("testuser" + i);
user.setRealName("测试用户" + i);
user.setAge(20 + i % 30);
user.setEmail("test" + i + "@example.com");
user.setPhoneNumber("13800138000");
user.setDeptId((long) (i % 5 + 1));
user.setStatus(0);
users.add(user);
}
return users;
}
}
8.3.2 批量更新优化
/**
* 批量更新优化企业级实践
* 重点:各种批量更新方案的性能对比和最佳实践
*/
@Service
@Slf4j
@Transactional
public class BatchUpdateService {
@Autowired
private UserMapper userMapper;
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
/**
* 1. 使用MyBatis-Plus的updateBatchById方法
* 重点:适合小批量数据更新
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateWithUpdateBatchById(List<User> users) {
log.info("使用updateBatchById批量更新,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
boolean success = userService.updateBatchById(users, 1000); // 每批1000条
long costTime = System.currentTimeMillis() - startTime;
log.info("updateBatchById批量更新完成,数据量: {}, 耗时: {} ms",
users.size(), costTime);
return success;
}
/**
* 2. 使用MyBatis的BatchExecutor批量更新
* 重点:适合大批量数据更新
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateWithBatchExecutor(List<User> users) {
log.info("使用BatchExecutor批量更新,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
.openSession(ExecutorType.BATCH);
try {
UserMapper batchMapper = sqlSession.getMapper(UserMapper.class);
int count = 0;
for (User user : users) {
batchMapper.updateById(user);
count++;
// 每1000条提交一次
if (count % 1000 == 0) {
sqlSession.commit();
sqlSession.clearCache();
log.debug("已提交 {} 条数据", count);
}
}
sqlSession.commit();
long costTime = System.currentTimeMillis() - startTime;
log.info("BatchExecutor批量更新完成,总数据量: {}, 耗时: {} ms",
users.size(), costTime);
return true;
} catch (Exception e) {
sqlSession.rollback();
log.error("BatchExecutor批量更新失败", e);
throw new RuntimeException("批量更新失败", e);
} finally {
sqlSession.close();
}
}
/**
* 3. 使用CASE WHEN语句进行批量更新
* 重点:单条SQL更新多条数据,性能最好
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateWithCaseWhen(List<User> users) {
log.info("使用CASE WHEN批量更新,数据量: {}", users.size());
long startTime = System.currentTimeMillis();
// 分批处理,避免SQL过长
int batchSize = 500; // CASE WHEN语句建议批次更小
int total = users.size();
int batchCount = (total + batchSize - 1) / batchSize;
int totalUpdated = 0;
for (int i = 0; i < batchCount; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, total);
List<User> batchList = users.subList(fromIndex, toIndex);
int affectedRows = userMapper.batchUpdateWithCaseWhen(batchList);
totalUpdated += affectedRows;
log.debug("第 {} 批更新完成,影响 {} 条数据", i + 1, affectedRows);
}
long costTime = System.currentTimeMillis() - startTime;
log.info("CASE WHEN批量更新完成,总数据量: {}, 成功更新: {}, 耗时: {} ms",
users.size(), totalUpdated, costTime);
return totalUpdated > 0;
}
/**
* 4. 批量更新特定字段
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateSpecificField(List<Long> userIds, String fieldName, Object value) {
log.info("批量更新特定字段,用户数量: {}, 字段: {}, 值: {}",
userIds.size(), fieldName, value);
long startTime = System.currentTimeMillis();
// 构建更新条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", userIds)
.eq("deleted", 0) // 只更新未删除的数据
.set(fieldName, value);
int affectedRows = userMapper.update(null, updateWrapper);
long costTime = System.currentTimeMillis() - startTime;
log.info("批量更新特定字段完成,影响 {} 条数据,耗时: {} ms",
affectedRows, costTime);
return affectedRows > 0;
}
/**
* 5. 批量更新状态
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateStatus(List<Long> userIds, Integer status) {
log.info("批量更新用户状态,用户数量: {}, 状态: {}", userIds.size(), status);
if (userIds == null || userIds.isEmpty()) {
log.warn("用户ID列表为空");
return true;
}
// 分批处理
int batchSize = 1000;
int total = userIds.size();
int batchCount = (total + batchSize - 1) / batchSize;
int totalUpdated = 0;
for (int i = 0; i < batchCount; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, total);
List<Long> batchIds = userIds.subList(fromIndex, toIndex);
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", batchIds)
.eq("deleted", 0)
.set("status", status)
.set("update_time", LocalDateTime.now());
int affectedRows = userMapper.update(null, updateWrapper);
totalUpdated += affectedRows;
log.debug("第 {} 批状态更新完成,更新 {} 条数据", i + 1, affectedRows);
}
log.info("批量更新用户状态完成,总更新 {} 条数据", totalUpdated);
return totalUpdated > 0;
}
/**
* 6. 条件批量更新
*/
@Transactional(rollbackFor = Exception.class)
public boolean conditionalBatchUpdate(UserQueryDTO query, User updateUser) {
log.info("条件批量更新,查询条件: {}", query);
long startTime = System.currentTimeMillis();
// 构建查询条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 动态添加条件
if (StringUtils.isNotBlank(query.getUserName())) {
updateWrapper.like("user_name", query.getUserName());
}
if (query.getDeptId() != null) {
updateWrapper.eq("dept_id", query.getDeptId());
}
if (query.getStatus() != null) {
updateWrapper.eq("status", query.getStatus());
}
if (query.getMinAge() != null) {
updateWrapper.ge("age", query.getMinAge());
}
if (query.getMaxAge() != null) {
updateWrapper.le("age", query.getMaxAge());
}
updateWrapper.eq("deleted", 0);
// 设置更新字段
if (updateUser.getUserName() != null) {
updateWrapper.set("user_name", updateUser.getUserName());
}
if (updateUser.getRealName() != null) {
updateWrapper.set("real_name", updateUser.getRealName());
}
if (updateUser.getStatus() != null) {
updateWrapper.set("status", updateUser.getStatus());
}
updateWrapper.set("update_time", LocalDateTime.now());
int affectedRows = userMapper.update(null, updateWrapper);
long costTime = System.currentTimeMillis() - startTime;
log.info("条件批量更新完成,影响 {} 条数据,耗时: {} ms", affectedRows, costTime);
return affectedRows > 0;
}
}
8.3.3 批量删除优化
/**
* 批量删除优化企业级实践
* 重点:逻辑删除和物理删除的最佳实践
*/
@Service
@Slf4j
@Transactional
public class BatchDeleteService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private OperationLogMapper logMapper;
/**
* 1. 使用MyBatis-Plus的removeByIds方法(逻辑删除)
* 重点:默认执行逻辑删除,实际是UPDATE操作
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchLogicDeleteWithRemoveByIds(List<Long> userIds) {
log.info("使用removeByIds批量逻辑删除,数据量: {}", userIds.size());
long startTime = System.currentTimeMillis();
boolean success = userService.removeByIds(userIds);
long costTime = System.currentTimeMillis() - startTime;
log.info("removeByIds批量逻辑删除完成,耗时: {} ms", costTime);
return success;
}
/**
* 2. 使用UpdateWrapper进行批量逻辑删除
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchLogicDeleteWithWrapper(List<Long> userIds, String operator) {
log.info("使用UpdateWrapper批量逻辑删除,数据量: {}, 操作人: {}", userIds.size(), operator);
long startTime = System.currentTimeMillis();
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", userIds)
.eq("deleted", 0) // 只删除未删除的数据
.set("deleted", 1)
.set("delete_time", LocalDateTime.now())
.set("delete_by", operator);
int affectedRows = userMapper.update(null, updateWrapper);
long costTime = System.currentTimeMillis() - startTime;
log.info("UpdateWrapper批量逻辑删除完成,影响 {} 条数据,耗时: {} ms",
affectedRows, costTime);
// 记录操作日志
recordBatchDeleteLog(userIds, operator);
return affectedRows > 0;
}
/**
* 3. 条件批量逻辑删除
*/
@Transactional(rollbackFor = Exception.class)
public boolean conditionalBatchLogicDelete(UserQueryDTO query, String operator) {
log.info("条件批量逻辑删除,查询条件: {}, 操作人: {}", query, operator);
long startTime = System.currentTimeMillis();
// 构建查询条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 动态添加条件
if (StringUtils.isNotBlank(query.getUserName())) {
updateWrapper.like("user_name", query.getUserName());
}
if (query.getDeptId() != null) {
updateWrapper.eq("dept_id", query.getDeptId());
}
if (query.getStatus() != null) {
updateWrapper.eq("status", query.getStatus());
}
if (query.getCreateTimeStart() != null) {
updateWrapper.ge("create_time", query.getCreateTimeStart());
}
if (query.getCreateTimeEnd() != null) {
updateWrapper.le("create_time", query.getCreateTimeEnd());
}
updateWrapper.eq("deleted", 0)
.set("deleted", 1)
.set("delete_time", LocalDateTime.now())
.set("delete_by", operator);
int affectedRows = userMapper.update(null, updateWrapper);
long costTime = System.currentTimeMillis() - startTime;
log.info("条件批量逻辑删除完成,影响 {} 条数据,耗时: {} ms", affectedRows, costTime);
return affectedRows > 0;
}
/**
* 4. 物理批量删除(谨慎使用)
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchPhysicalDelete(List<Long> userIds, String operator) {
log.warn("批量物理删除,数据量: {}, 操作人: {}", userIds.size(), operator);
// 权限验证
if (!hasPhysicalDeletePermission(operator)) {
throw new RuntimeException("没有物理删除权限");
}
long startTime = System.currentTimeMillis();
// 先备份重要数据
backupUsersBeforeDelete(userIds, operator);
// 删除用户角色关联
deleteUserRoles(userIds);
// 执行物理删除
int affectedRows = userMapper.deleteBatchIds(userIds);
long costTime = System.currentTimeMillis() - startTime;
log.warn("批量物理删除完成,删除 {} 条数据,耗时: {} ms", affectedRows, costTime);
// 记录物理删除日志
recordPhysicalDeleteLog(userIds, operator);
return affectedRows > 0;
}
/**
* 5. 分批物理删除(避免SQL过长)
*/
@Transactional(rollbackFor = Exception.class)
public boolean batchPhysicalDeleteWithChunk(List<Long> userIds, int chunkSize, String operator) {
log.warn("分批物理删除,总数据量: {}, 每批大小: {}, 操作人: {}",
userIds.size(), chunkSize, operator);
// 权限验证
if (!hasPhysicalDeletePermission(operator)) {
throw new RuntimeException("没有物理删除权限");
}
long startTime = System.currentTimeMillis();
int total = userIds.size();
int chunkCount = (total + chunkSize - 1) / chunkSize;
int totalDeleted = 0;
for (int i = 0; i < chunkCount; i++) {
int fromIndex = i * chunkSize;
int toIndex = Math.min(fromIndex + chunkSize, total);
List<Long> chunkIds = userIds.subList(fromIndex, toIndex);
// 备份当前批次数据
backupUsersBeforeDelete(chunkIds, operator);
// 删除用户角色关联
deleteUserRoles(chunkIds);
// 删除当前批次
int deleted = userMapper.deleteBatchIds(chunkIds);
totalDeleted += deleted;
log.debug("第 {} 批删除完成,删除 {} 条数据", i + 1, deleted);
}
long costTime = System.currentTimeMillis() - startTime;
log.warn("分批物理删除完成,总删除 {} 条数据,耗时: {} ms", totalDeleted, costTime);
recordPhysicalDeleteLog(userIds, operator);
return totalDeleted > 0;
}
/**
* 6. 定时清理已逻辑删除的数据
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
@Transactional(rollbackFor = Exception.class)
public void cleanDeletedUsers() {
log.info("开始清理长时间已逻辑删除的用户");
// 清理30天前删除的用户
LocalDateTime threshold = LocalDateTime.now().minusDays(30);
// 查询需要清理的用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", 1)
.le("delete_time", threshold)
.last("LIMIT 1000"); // 每次最多清理1000条
List<User> deletedUsers = userMapper.selectList(queryWrapper);
if (deletedUsers.isEmpty()) {
log.info("没有需要清理的已删除用户");
return;
}
List<Long> userIds = deletedUsers.stream()
.map(User::getId)
.collect(Collectors.toList());
// 备份数据
backupUsersBeforeDelete(userIds, "system_cleanup");
// 执行物理删除
int deletedCount = userMapper.deleteBatchIds(userIds);
log.info("清理已删除用户完成,共清理 {} 条记录", deletedCount);
// 记录清理日志
recordCleanupLog(userIds, "system_cleanup");
}
// ==================== 辅助方法 ====================
/**
* 备份删除前的数据
*/
private void backupUsersBeforeDelete(List<Long> userIds, String operator) {
log.debug("备份用户数据,用户数量: {}", userIds.size());
try {
List<User> users = userMapper.selectBatchIds(userIds);
// 在实际项目中,这里应该将用户数据备份到历史表
// userHistoryMapper.batchInsert(users);
log.info("用户数据备份完成,数量: {}", users.size());
} catch (Exception e) {
log.error("备份用户数据失败", e);
throw new RuntimeException("备份用户数据失败", e);
}
}
/**
* 删除用户角色关联
*/
private void deleteUserRoles(List<Long> userIds) {
QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIds);
int deleted = userRoleMapper.delete(queryWrapper);
log.debug("删除用户角色关联,删除 {} 条记录", deleted);
}
/**
* 记录批量删除日志
*/
private void recordBatchDeleteLog(List<Long> userIds, String operator) {
OperationLog log = new OperationLog();
log.setOperationType("BATCH_DELETE_USER");
log.setDescription("批量删除用户,数量: " + userIds.size());
log.setOperator(operator);
log.setOperationTime(LocalDateTime.now());
logMapper.insert(log);
}
/**
* 记录物理删除日志
*/
private void recordPhysicalDeleteLog(List<Long> userIds, String operator) {
OperationLog log = new OperationLog();
log.setOperationType("PHYSICAL_DELETE_USER");
log.setDescription("物理删除用户,数量: " + userIds.size());
log.setOperator(operator);
log.setOperationTime(LocalDateTime.now());
logMapper.insert(log);
}
/**
* 记录清理日志
*/
private void recordCleanupLog(List<Long> userIds, String operator) {
OperationLog log = new OperationLog();
log.setOperationType("CLEANUP_DELETED_USERS");
log.setDescription("清理已删除用户,数量: " + userIds.size());
log.setOperator(operator);
log.setOperationTime(LocalDateTime.now());
logMapper.insert(log);
}
/**
* 检查物理删除权限
*/
private boolean hasPhysicalDeletePermission(String operator) {
// 实际项目中检查用户权限
return "admin".equals(operator) || "system".equals(operator);
}
}
8.4 批量操作性能监控和优化
/**
* 批量操作性能监控和优化
* 重点:监控批量操作性能,提供优化建议
*/
@Service
@Slf4j
public class BatchOperationMonitor {
@Autowired
private BatchInsertService batchInsertService;
@Autowired
private BatchUpdateService batchUpdateService;
@Autowired
private BatchDeleteService batchDeleteService;
/**
* 批量操作性能测试套件
*/
public void runPerformanceTestSuite() {
log.info("开始批量操作性能测试套件");
// 生成测试数据
List<User> testData = generateTestData(10000);
List<Long> testIds = testData.stream()
.map(User::getId)
.collect(Collectors.toList());
// 1. 批量插入性能测试
testBatchInsertPerformance(testData);
// 2. 批量更新性能测试
testBatchUpdatePerformance(testData);
// 3. 批量删除性能测试
testBatchDeletePerformance(testIds);
// 4. 内存使用监控
monitorMemoryUsage();
// 5. 输出优化建议
printOptimizationSuggestions();
log.info("批量操作性能测试套件完成");
}
/**
* 批量插入性能测试
*/
public void testBatchInsertPerformance(List<User> testData) {
log.info("开始批量插入性能测试");
// 准备测试数据副本
List<User> data1 = new ArrayList<>(testData);
List<User> data2 = new ArrayList<>(testData);
List<User> data3 = new ArrayList<>(testData);
// 测试1:MyBatis-Plus saveBatch
long start1 = System.currentTimeMillis();
batchInsertService.batchInsertWithSaveBatch(data1);
long time1 = System.currentTimeMillis() - start1;
// 测试2:BatchExecutor
long start2 = System.currentTimeMillis();
batchInsertService.batchInsertWithBatchExecutor(data2);
long time2 = System.currentTimeMillis() - start2;
// 测试3:foreach SQL
long start3 = System.currentTimeMillis();
batchInsertService.batchInsertWithForeachSql(data3);
long time3 = System.currentTimeMillis() - start3;
log.info("""
==================== 批量插入性能测试结果 ====================
测试数据量: {}
MyBatis-Plus saveBatch: {} ms
BatchExecutor: {} ms
foreach SQL: {} ms
=========================================================
""", testData.size(), time1, time2, time3);
}
/**
* 批量更新性能测试
*/
public void testBatchUpdatePerformance(List<User> testData) {
log.info("开始批量更新性能测试");
// 修改测试数据
testData.forEach(user -> user.setRealName("更新测试"));
List<User> data1 = new ArrayList<>(testData);
List<User> data2 = new ArrayList<>(testData);
List<User> data3 = new ArrayList<>(testData);
// 测试1:MyBatis-Plus updateBatchById
long start1 = System.currentTimeMillis();
batchUpdateService.batchUpdateWithUpdateBatchById(data1);
long time1 = System.currentTimeMillis() - start1;
// 测试2:BatchExecutor
long start2 = System.currentTimeMillis();
batchUpdateService.batchUpdateWithBatchExecutor(data2);
long time2 = System.currentTimeMillis() - start2;
// 测试3:CASE WHEN
long start3 = System.currentTimeMillis();
batchUpdateService.batchUpdateWithCaseWhen(data3);
long time3 = System.currentTimeMillis() - start3;
log.info("""
==================== 批量更新性能测试结果 ====================
测试数据量: {}
MyBatis-Plus updateBatchById: {} ms
BatchExecutor: {} ms
CASE WHEN: {} ms
=========================================================
""", testData.size(), time1, time2, time3);
}
/**
* 批量删除性能测试
*/
public void testBatchDeletePerformance(List<Long> testIds) {
log.info("开始批量删除性能测试");
List<Long> ids1 = new ArrayList<>(testIds);
List<Long> ids2 = new ArrayList<>(testIds);
// 测试1:逻辑删除
long start1 = System.currentTimeMillis();
batchDeleteService.batchLogicDeleteWithWrapper(ids1, "test");
long time1 = System.currentTimeMillis() - start1;
// 测试2:物理删除
long start2 = System.currentTimeMillis();
batchDeleteService.batchPhysicalDeleteWithChunk(ids2, 1000, "test");
long time2 = System.currentTimeMillis() - start2;
log.info("""
==================== 批量删除性能测试结果 ====================
测试数据量: {}
逻辑删除: {} ms
物理删除: {} ms
=========================================================
""", testIds.size(), time1, time2);
}
/**
* 内存使用监控
*/
public void monitorMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
// 运行GC,获取更准确的内存信息
System.gc();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
long maxMemory = runtime.maxMemory();
log.info("""
==================== 内存使用情况 ====================
总内存: {} MB
已使用: {} MB
剩余内存: {} MB
最大内存: {} MB
内存使用率: {}%
===================================================
""",
bytesToMB(totalMemory),
bytesToMB(usedMemory),
bytesToMB(freeMemory),
bytesToMB(maxMemory),
String.format("%.2f", (double) usedMemory / totalMemory * 100));
}
/**
* 生成测试数据
*/
private List<User> generateTestData(int count) {
List<User> users = new ArrayList<>();
for (int i = 0; i < count; i++) {
User user = new User();
user.setUserName("testuser" + i);
user.setRealName("测试用户" + i);
user.setAge(20 + i % 30);
user.setEmail("test" + i + "@example.com");
user.setPhoneNumber("13800138000");
user.setDeptId((long) (i % 5 + 1));
user.setStatus(0);
users.add(user);
}
return users;
}
/**
* 字节转MB
*/
private long bytesToMB(long bytes) {
return bytes / 1024 / 1024;
}
/**
* 输出优化建议
*/
public void printOptimizationSuggestions() {
log.info("""
==================== 批量操作优化建议 ====================
一、批量插入优化建议:
1. 小批量数据(1000条以内):使用MyBatis-Plus的saveBatch
2. 大批量数据(万级以上):使用BatchExecutor
3. 超大批量数据(10万+):考虑使用数据库原生批量导入工具
4. 内存优化:合理设置批次大小,及时清理缓存
二、批量更新优化建议:
1. 小批量数据:使用MyBatis-Plus的updateBatchById
2. 大批量相同值更新:使用UpdateWrapper的set方法
3. 大批量不同值更新:使用BatchExecutor或CASE WHEN
4. 性能优先:CASE WHEN语句性能最好
三、批量删除优化建议:
1. 优先使用逻辑删除,保留数据恢复可能
2. 物理删除前一定要备份数据
3. 大批量删除使用分批处理,避免长事务
4. 定期清理已逻辑删除的数据
四、通用优化建议:
1. 合理设置批次大小(通常500-1000)
2. 监控内存使用,防止OOM
3. 使用事务保证数据一致性
4. 考虑使用异步处理超大批量操作
5. 做好异常处理和重试机制
6. 定期分析慢SQL,优化数据库索引
五、数据库配置优化:
1. 调整max_allowed_packet参数(MySQL)
2. 优化innodb_buffer_pool_size(MySQL)
3. 调整批量操作超时时间
4. 合理设置事务隔离级别
=====================================================
""");
}
/**
* 批次大小优化测试
*/
public void testBatchSizeOptimization() {
log.info("开始批次大小优化测试");
List<User> testData = generateTestData(5000);
int[] batchSizes = {100, 500, 1000, 2000, 5000};
for (int batchSize : batchSizes) {
List<User> data = new ArrayList<>(testData);
long startTime = System.currentTimeMillis();
batchInsertService.batchInsertWithCustomBatchSize(data, batchSize);
long costTime = System.currentTimeMillis() - startTime;
log.info("批次大小: {}, 耗时: {} ms", batchSize, costTime);
}
log.info("批次大小优化测试完成");
}
}
企业级MyBatis-Plus实战教学(完结篇)
课程总结与进阶指导
9.1 课程核心知识点回顾
/**
* MyBatis-Plus企业级实战核心知识点总结
* 重点:八大模块核心要点回顾和最佳实践总结
*/
@Slf4j
public class MyBatisPlusCourseSummary {
/**
* 模块一:环境搭建与配置最佳实践
*/
public void summaryModule1() {
log.info("""
==================== 模块一核心要点 ====================
1. 依赖管理:
- 使用mybatis-plus-boot-starter简化配置
- 统一管理版本号,避免冲突
2. 配置详解:
- 数据源配置:时区、字符集、SSL
- 连接池配置:HikariCP性能优化
- MyBatis-Plus全局配置:ID策略、逻辑删除
3. 配置类设计:
- 拦截器顺序:分页->乐观锁->防全表操作
- 自动填充:创建时间、更新时间等
4. 避坑指南:
- 生产环境关闭SQL日志
- 多数据源手动配置SqlSessionFactory
- 时区问题导致的时间差
=====================================================
""");
}
/**
* 模块二:CRUD接口封装与Lambda查询
*/
public void summaryModule2() {
log.info("""
==================== 模块二核心要点 ====================
1. 实体类设计:
- @TableName、@TableId、@TableField注解
- 逻辑删除@TableLogic、乐观锁@Version
- 自动填充字段设计
2. 三层架构规范:
- Mapper继承BaseMapper获得基础CRUD
- Service继承ServiceImpl获得增强方法
- Controller统一响应格式
3. Lambda查询:
- 类型安全,编译期检查
- 避免硬编码字段名
- 链式调用,代码简洁
4. 最佳实践:
- 使用VO、DTO进行数据传递
- Service层统一异常处理
- 完整的单元测试覆盖
=====================================================
""");
}
/**
* 模块三:条件构造器高级用法
*/
public void summaryModule3() {
log.info("""
==================== 模块三核心要点 ====================
1. QueryWrapper核心功能:
- 等值查询:eq、ne
- 范围查询:gt、ge、lt、le、between
- 模糊查询:like、likeLeft、likeRight
- 空值判断:isNull、isNotNull
- IN查询:in、notIn
2. 动态条件构建:
- 根据业务参数动态添加条件
- 链式调用构建复杂查询
3. LambdaQueryWrapper:
- 类型安全的条件构建
- 避免SQL注入风险
- IDE智能提示支持
4. 性能优化:
- 避免N+1查询问题
- 合理使用索引
- 分页查询优化
=====================================================
""");
}
/**
* 模块四:分页插件实现原理与性能优化
*/
public void summaryModule4() {
log.info("""
==================== 模块四核心要点 ====================
1. 分页插件配置:
- 必须第一个添加到拦截器链
- 设置数据库类型、最大单页限制
- 开启溢出处理和count优化
2. 分页查询方式:
- 基础分页:Page对象 + selectPage
- 不计数分页:提升性能
- 自定义count查询:复杂查询优化
3. 性能优化方案:
- 游标分页:基于ID的连续分页
- 延迟关联:先查ID再查详情
- 覆盖索引:只查询需要的字段
4. 企业级实践:
- 统一分页响应格式
- 参数安全验证
- 深度分页性能问题解决
=====================================================
""");
}
/**
* 模块五:代码生成器实战配置
*/
public void summaryModule5() {
log.info("""
==================== 模块五核心要点 ====================
1. 代码生成器配置:
- 全局配置:作者、输出目录、文件覆盖
- 包配置:分层结构、模块划分
- 策略配置:表过滤、字段策略、命名规则
2. 自定义模板:
- 实体类模板:Lombok、Swagger集成
- Service模板:业务方法规范
- Controller模板:统一响应格式
3. 高级功能:
- 多数据源代码生成
- 增量代码生成
- 自定义文件生成
4. 企业级实践:
- 模板版本管理
- 生成代码质量检查
- 版本控制集成
=====================================================
""");
}
/**
* 模块六:乐观锁/逻辑删除实现机制
*/
public void summaryModule6() {
log.info("""
==================== 模块六核心要点 ====================
1. 乐观锁实现:
- @Version注解标记版本号字段
- 更新时自动version=version+1
- 冲突时抛出OptimisticLockException
2. 乐观锁最佳实践:
- 重试机制解决冲突
- 合理的重试次数和间隔
- 业务层面的冲突处理
3. 逻辑删除实现:
- @TableLogic注解标记删除字段
- 查询自动添加deleted=0条件
- 删除操作变为更新操作
4. 逻辑删除扩展:
- 记录删除人和删除时间
- 定期清理已删除数据
- 数据恢复机制
=====================================================
""");
}
/**
* 模块七:多表关联查询解决方案
*/
public void summaryModule7() {
log.info("""
==================== 模块七核心要点 ====================
1. 关联查询方式:
- 注解方式:@One、@Many、@Results
- XML方式:<association>、<collection>
- JOIN查询:一次性获取所有数据
2. VO对象设计:
- 专门用于前端展示的对象
- 包含多个实体类的字段
- 避免污染实体类
3. 性能优化方案:
- 解决N+1查询问题
- 延迟加载关联数据
- 批量查询优化
4. 企业级实践:
- 按需加载关联数据
- 缓存优化查询性能
- 合理的索引设计
=====================================================
""");
}
/**
* 模块八:事务管理与批量操作优化
*/
public void summaryModule8() {
log.info("""
==================== 模块八核心要点 ====================
1. 事务管理:
- 声明式事务:@Transactional注解
- 编程式事务:TransactionTemplate
- 传播行为:7种传播行为适用场景
2. 隔离级别:
- 4种隔离级别解决不同并发问题
- 默认使用数据库隔离级别
- 根据业务需求选择合适的隔离级别
3. 批量操作优化:
- 批量插入:saveBatch、BatchExecutor
- 批量更新:updateBatchById、CASE WHEN
- 批量删除:逻辑删除优先
4. 性能监控:
- 事务执行时间监控
- 内存使用监控
- 批次大小优化测试
=====================================================
""");
}
/**
* 技术选型决策指南
*/
public void technologySelectionGuide() {
log.info("""
==================== 技术选型决策指南 ====================
场景一:简单CRUD操作
✅ 推荐:MyBatis-Plus基础CRUD + Lambda查询
❌ 避免:原生MyBatis手动编写SQL
场景二:复杂动态查询
✅ 推荐:QueryWrapper动态条件构建
❌ 避免:XML中大量<if>标签
场景三:多表关联查询
✅ 推荐:XML配置关联查询 + VO对象
❌ 避免:在代码中循环查询关联数据
场景四:高并发更新
✅ 推荐:乐观锁 + 重试机制
❌ 避免:悲观锁或没有并发控制
场景五:大批量数据处理
✅ 推荐:BatchExecutor + 分批处理
❌ 避免:单条记录循环处理
场景六:分页查询
✅ 推荐:MyBatis-Plus分页插件
❌ 避免:内存分页或手动分页
场景七:需要事务管理
✅ 推荐:Spring声明式事务
❌ 避免:手动控制事务
=====================================================
""");
}
}
9.2 企业级项目架构设计
/**
* 企业级项目架构设计指南
* 重点:基于MyBatis-Plus的大型项目架构设计
*/
@Slf4j
public class EnterpriseArchitectureDesign {
/**
* 分层架构设计
*/
public void layeredArchitecture() {
log.info("""
==================== 企业级分层架构设计 ====================
1. 表现层(Presentation Layer)
- Controller:RESTful API接口
- DTO:数据传输对象
- VO:视图展示对象
- 参数校验、统一响应格式
2. 业务层(Business Layer)
- Service:业务逻辑实现
- Manager:复杂业务协调
- 事务管理、业务校验
3. 持久层(Persistence Layer)
- Mapper:数据访问接口
- Entity:实体对象
- 数据库操作、缓存集成
4. 通用层(Common Layer)
- 配置类、工具类
- 常量定义、枚举类
- 异常处理、日志处理
5. 依赖关系:
表现层 → 业务层 → 持久层
↓
通用层
=====================================================
""");
}
/**
* 包结构设计
*/
public void packageStructureDesign() {
log.info("""
==================== 企业级包结构设计 ====================
com.enterprise
├── application # 应用层
│ ├── controller # 控制器
│ ├── dto # 数据传输对象
│ └── vo # 视图对象
├── domain # 领域层
│ ├── entity # 实体类
│ ├── repository # 仓储接口
│ ├── service # 服务接口
│ └── manager # 业务协调
├── infrastructure # 基础设施层
│ ├── mapper # 数据访问
│ ├── config # 配置类
│ └── util # 工具类
├── common # 通用层
│ ├── constant # 常量
│ ├── enums # 枚举
│ ├── exception # 异常
│ └── response # 统一响应
└── EnterpriseApplication.java # 启动类
=====================================================
""");
}
/**
* 数据库设计规范
*/
public void databaseDesignSpecification() {
log.info("""
==================== 数据库设计规范 ====================
1. 命名规范:
- 表名:小写+下划线,如:sys_user
- 字段名:小写+下划线,如:user_name
- 索引名:idx_表名_字段名,如:idx_user_name
2. 字段设计:
- 主键:BIGINT自增或雪花算法
- 时间:DATETIME或TIMESTAMP
- 金额:DECIMAL(precision, scale)
- 状态:TINYINT或VARCHAR
3. 必备字段:
- id:主键
- version:乐观锁版本号
- deleted:逻辑删除标志
- create_time:创建时间
- update_time:更新时间
- create_by:创建人
- update_by:更新人
4. 索引设计:
- 主键索引:id
- 唯一索引:业务唯一字段
- 普通索引:查询条件字段
- 联合索引:多字段查询条件
=====================================================
""");
}
/**
* 配置文件组织
*/
public void configurationOrganization() {
log.info("""
==================== 配置文件组织 ====================
resources/
├── application.yml # 主配置文件
├── application-dev.yml # 开发环境
├── application-test.yml # 测试环境
├── application-prod.yml # 生产环境
├── mapper/ # MyBatis映射文件
│ ├── UserMapper.xml
│ ├── DeptMapper.xml
│ └── ...
├── templates/ # 代码生成器模板
│ ├── entity.java.ftl
│ ├── mapper.java.ftl
│ └── ...
└── static/ # 静态资源
├── sql/ # SQL脚本
└── ...
配置分离原则:
- 环境相关配置:数据库、Redis、MQ等
- 应用配置:端口、上下文路径等
- 第三方配置:Swagger、监控等
=====================================================
""");
}
}
9.3 性能优化完整方案
/**
* MyBatis-Plus性能优化完整方案
* 重点:从数据库到应用层的全方位性能优化
*/
@Service
@Slf4j
public class ComprehensivePerformanceOptimization {
/**
* 1. 数据库层面优化
*/
public void databaseLevelOptimization() {
log.info("""
==================== 数据库层面优化 ====================
1. 索引优化:
- 为查询条件字段创建索引
- 使用覆盖索引减少回表
- 避免在索引列上使用函数
- 定期分析索引使用情况
2. SQL优化:
- 避免SELECT *,只查询需要的字段
- 使用EXPLAIN分析SQL执行计划
- 避免在WHERE子句中使用!=或<>
- 使用JOIN代替子查询
3. 表结构优化:
- 选择合适的数据类型
- 适当使用字段冗余
- 大字段分离到扩展表
- 分区表处理大数据量
4. 数据库参数优化:
- 调整连接池参数
- 优化缓冲区大小
- 设置合理的超时时间
=====================================================
""");
}
/**
* 2. MyBatis-Plus层面优化
*/
public void mybatisPlusLevelOptimization() {
log.info("""
==================== MyBatis-Plus层面优化 ====================
1. 查询优化:
- 使用select()指定查询字段
- 合理使用LambdaQueryWrapper
- 避免在循环中执行查询
- 使用批量查询代替循环单条查询
2. 分页优化:
- 深度分页使用游标分页
- 不需要总数时关闭count查询
- 复杂分页使用延迟关联
3. 批量操作优化:
- 使用BatchExecutor进行大批量操作
- 合理设置批次大小(500-1000)
- 分批提交避免大事务
4. 缓存优化:
- 合理使用二级缓存
- 业务缓存减少数据库访问
- 缓存穿透、击穿、雪崩防护
=====================================================
""");
}
/**
* 3. 应用层面优化
*/
public void applicationLevelOptimization() {
log.info("""
==================== 应用层面优化 ====================
1. 连接池优化:
- 使用HikariCP高性能连接池
- 合理设置最大最小连接数
- 设置合理的连接超时时间
2. 事务优化:
- 使用合适的事务隔离级别
- 避免长事务,设置超时时间
- 只读事务优化查询性能
3. 线程池优化:
- 合理设置线程池参数
- 使用异步处理耗时操作
- 避免线程阻塞
4. JVM优化:
- 合理设置堆内存大小
- 选择合适的垃圾回收器
- 监控GC情况
=====================================================
""");
}
/**
* 4. 架构层面优化
*/
public void architectureLevelOptimization() {
log.info("""
==================== 架构层面优化 ====================
1. 读写分离:
- 主库处理写操作
- 从库处理读操作
- 使用MyBatis-Plus多数据源支持
2. 分库分表:
- 水平分表解决单表数据过大
- 垂直分库按业务拆分
- 使用ShardingSphere等中间件
3. 缓存架构:
- 多级缓存:本地缓存 + Redis
- 缓存更新策略
- 缓存一致性保证
4. 异步处理:
- 消息队列解耦耗时操作
- 异步线程处理非实时任务
- 事件驱动架构
=====================================================
""");
}
/**
* 性能监控方案
*/
public void performanceMonitoringSolution() {
log.info("""
==================== 性能监控方案 ====================
1. SQL监控:
- 开启MyBatis SQL日志(开发环境)
- 使用Druid监控SQL执行
- 慢SQL查询和优化
2. 应用监控:
- Spring Boot Actuator健康检查
- Micrometer + Prometheus指标收集
- APM工具:SkyWalking、Pinpoint
3. 业务监控:
- 关键业务指标监控
- 自定义业务埋点
- 实时告警机制
4. 日志监控:
- 结构化日志输出
- 日志聚合分析
- 错误日志实时告警
=====================================================
""");
}
}
9.4 安全防护最佳实践
/**
* MyBatis-Plus安全防护最佳实践
* 重点:防止SQL注入、数据泄露等安全问题
*/
@Service
@Slf4j
public class SecurityBestPractices {
/**
* 1. SQL注入防护
*/
public void sqlInjectionPrevention() {
log.info("""
==================== SQL注入防护 ====================
1. 使用MyBatis-Plus内置方法:
- 优先使用eq、like等内置方法
- 避免直接拼接SQL参数
2. XML中参数化查询:
- 使用#{param}而不是${param}
- #{}会预编译,${}会直接拼接
3. 动态SQL安全:
- 使用<if>标签而不是字符串拼接
- 对用户输入进行严格校验
4. 自定义SQL安全:
- 使用@Param注解传递参数
- 避免在注解SQL中拼接字符串
危险示例:
❌ @Select("SELECT * FROM user WHERE name = '" + name + "'")
✅ @Select("SELECT * FROM user WHERE name = #{name}")
=====================================================
""");
}
/**
* 2. 数据权限控制
*/
public void dataPermissionControl() {
log.info("""
==================== 数据权限控制 ====================
1. 基于租户的数据隔离:
- 使用MyBatis-Plus多租户插件
- 自动添加tenant_id条件
2. 基于组织的数据权限:
- 查询时自动添加部门过滤条件
- 使用自定义拦截器实现
3. 基于用户的数据权限:
- 查询时添加创建人过滤
- 细粒度数据权限控制
4. 数据脱敏:
- 敏感字段在查询时脱敏
- 使用@JsonIgnore忽略敏感字段
- 自定义序列化器处理敏感数据
=====================================================
""");
}
/**
* 3. 防全表操作
*/
public void preventFullTableOperations() {
log.info("""
==================== 防全表操作 ====================
1. 使用BlockAttackInnerInterceptor:
- 防止全表更新和删除
- 必须包含WHERE条件
2. 分页查询限制:
- 设置最大分页大小
- 深度分页限制
3. 批量操作限制:
- 限制单次批量操作数量
- 重要操作需要审批流程
4. 操作日志记录:
- 记录所有数据修改操作
- 定期审计操作日志
=====================================================
""");
}
/**
* 4. 敏感数据处理
*/
public void sensitiveDataHandling() {
log.info("""
==================== 敏感数据处理 ====================
1. 数据加密:
- 数据库字段加密存储
- 使用MyBatis类型处理器加解密
2. 数据脱敏:
- 手机号:138****8888
- 身份证:110***********1234
- 邮箱:te***@example.com
3. 访问控制:
- 接口级别权限控制
- 数据级别权限控制
4. 审计日志:
- 敏感数据访问日志
- 数据导出操作日志
=====================================================
""");
}
}
9.5 扩展与集成方案
/**
* MyBatis-Plus扩展与集成方案
* 重点:与其他技术栈的集成和自定义扩展
*/
@Slf4j
public class ExtensionAndIntegrationSolutions {
/**
* 1. Spring Boot集成
*/
public void springBootIntegration() {
log.info("""
==================== Spring Boot集成 ====================
1. 自动配置:
- MyBatisPlusAutoConfiguration
- 自动配置SqlSessionFactory
- 自动配置Mapper扫描
2. 配置属性:
- mybatis-plus.configuration.*
- mybatis-plus.global-config.*
- 支持YAML和Properties配置
3. 健康检查:
- 数据库连接健康检查
- 自定义健康检查指标
4. 监控集成:
- Actuator端点监控
- Micrometer指标收集
=====================================================
""");
}
/**
* 2. Spring Cloud集成
*/
public void springCloudIntegration() {
log.info("""
==================== Spring Cloud集成 ====================
1. 配置中心:
- 从配置中心读取数据库配置
- 动态刷新数据源配置
2. 服务发现:
- 多数据源动态路由
- 读写分离数据源配置
3. 分布式事务:
- Seata分布式事务集成
- 多数据源事务管理
4. 链路追踪:
- SQL执行链路追踪
- 业务操作链路追踪
=====================================================
""");
}
/**
* 3. 多数据源集成
*/
public void multiDataSourceIntegration() {
log.info("""
==================== 多数据源集成 ====================
1. 动态数据源:
- 使用AbstractRoutingDataSource
- 基于注解的数据源切换
2. 读写分离:
- 主从数据源配置
- 读操作路由到从库
3. 分库分表:
- ShardingSphere集成
- 自定义分片策略
4. 多租户:
- 基于数据库的租户隔离
- 基于Schema的租户隔离
=====================================================
""");
}
/**
* 4. 缓存集成
*/
public void cacheIntegration() {
log.info("""
==================== 缓存集成 ====================
1. Redis缓存:
- MyBatis二级缓存Redis实现
- 业务数据缓存
2. 本地缓存:
- Caffeine本地缓存
- 多级缓存架构
3. 缓存策略:
- 读多写少:缓存优先
- 写多读少:数据库优先
4. 缓存一致性:
- 双写策略
- 失效策略
- 延迟双删
=====================================================
""");
}
/**
* 5. 自定义扩展
*/
public void customExtensions() {
log.info("""
==================== 自定义扩展 ====================
1. 自定义注入器:
- 扩展BaseMapper方法
- 自定义通用方法
2. 自定义拦截器:
- SQL执行时间监控
- 数据权限过滤
- 多租户数据隔离
3. 自定义类型处理器:
- 数据加密解密
- JSON字段处理
- 枚举类型处理
4. 自定义ID生成器:
- 扩展雪花算法
- 自定义业务ID生成
=====================================================
""");
}
}
9.6 学习路径和进阶方向
/**
* MyBatis-Plus学习路径和进阶方向
* 重点:从入门到专家的学习路线规划
*/
@Slf4j
public class LearningPathAndAdvancedDirections {
/**
* 初学者阶段(1-2个月)
*/
public void beginnerStage() {
log.info("""
==================== 初学者阶段 ====================
学习目标:
- 掌握MyBatis-Plus基础CRUD操作
- 理解实体类注解使用
- 学会简单的条件查询
学习内容:
1. 环境搭建和基础配置
2. 实体类设计和注解使用
3. BaseMapper基础方法使用
4. 简单的QueryWrapper查询
5. 分页查询基础使用
实践项目:
- 用户管理系统
- 商品管理系统
考核标准:
- 能够独立完成基础CRUD功能
- 理解MyBatis-Plus基本概念
- 能够解决简单的配置问题
=====================================================
""");
}
/**
* 进阶阶段(2-4个月)
*/
public void intermediateStage() {
log.info("""
==================== 进阶阶段 ====================
学习目标:
- 掌握复杂查询和关联查询
- 理解事务管理和性能优化
- 学会代码生成器使用
学习内容:
1. 复杂的QueryWrapper和LambdaQueryWrapper
2. 多表关联查询实现
3. 事务管理和传播行为
4. 乐观锁和逻辑删除
5. 代码生成器配置和使用
实践项目:
- 电商订单系统
- 内容管理系统
考核标准:
- 能够处理复杂业务查询
- 理解事务的传播行为和隔离级别
- 能够使用代码生成器提高开发效率
=====================================================
""");
}
/**
* 高级阶段(4-6个月)
*/
public void advancedStage() {
log.info("""
==================== 高级阶段 ====================
学习目标:
- 掌握性能优化和调优
- 理解源码实现原理
- 能够进行自定义扩展
学习内容:
1. SQL性能分析和优化
2. MyBatis-Plus源码分析
3. 自定义拦截器和注入器
4. 多数据源和分布式事务
5. 企业级架构设计
实践项目:
- 高并发秒杀系统
- 分布式电商平台
考核标准:
- 能够分析和解决性能瓶颈
- 理解框架底层实现原理
- 能够根据业务需求进行框架扩展
=====================================================
""");
}
/**
* 专家阶段(6个月以上)
*/
public void expertStage() {
log.info("""
==================== 专家阶段 ====================
学习目标:
- 框架源码深度理解
- 性能极致优化
- 架构设计能力
学习内容:
1. MyBatis和MyBatis-Plus源码深度分析
2. 数据库深度优化和调优
3. 分布式系统架构设计
4. 高并发系统设计
5. 技术团队管理和架构决策
实践项目:
- 自研ORM框架
- 大型分布式系统架构
考核标准:
- 能够参与开源项目贡献
- 具备大型系统架构设计能力
- 能够指导团队技术方向
=====================================================
""");
}
/**
* 持续学习资源
*/
public void continuousLearningResources() {
log.info("""
==================== 持续学习资源 ====================
官方资源:
- MyBatis-Plus官网:https://baomidou.com/
- GitHub仓库:https://github.com/baomidou/mybatis-plus
- 官方文档:https://baomidou.com/guide/
社区资源:
- MyBatis-Plus社区
- 技术博客和文章
- 开源项目源码学习
实践资源:
- 参与开源项目
- 技术分享和演讲
- 个人技术博客写作
进阶方向:
- 数据库深度优化
- 分布式系统架构
- 云原生技术栈
- 大数据处理技术
=====================================================
""");
}
}
9.7 企业级项目 Checklist
/**
* 企业级MyBatis-Plus项目Checklist
* 重点:项目开发和上线的检查清单
*/
@Slf4j
public class EnterpriseProjectChecklist {
/**
* 开发阶段检查项
*/
public void developmentChecklist() {
log.info("""
==================== 开发阶段检查项 ====================
✅ 1. 项目结构规范
- 分层架构清晰
- 包结构合理
- 命名规范统一
✅ 2. 实体类设计
- 正确使用注解
- 包含必要的基础字段
- 数据类型选择合理
✅ 3. Mapper设计
- 继承BaseMapper
- 自定义方法命名规范
- XML文件位置正确
✅ 4. Service设计
- 业务逻辑清晰
- 事务管理正确
- 异常处理完善
✅ 5. 查询优化
- 避免N+1查询
- 合理使用索引
- 分页查询优化
✅ 6. 代码质量
- 单元测试覆盖
- 代码规范检查
- 性能测试通过
=====================================================
""");
}
/**
* 测试阶段检查项
*/
public void testingChecklist() {
log.info("""
==================== 测试阶段检查项 ====================
✅ 1. 单元测试
- 业务逻辑测试
- 数据库操作测试
- 边界条件测试
✅ 2. 集成测试
- 多表关联测试
- 事务管理测试
- 并发操作测试
✅ 3. 性能测试
- 响应时间测试
- 并发用户测试
- 大数据量测试
✅ 4. 安全测试
- SQL注入测试
- 数据权限测试
- 敏感数据测试
✅ 5. 兼容性测试
- 数据库版本兼容
- 不同环境测试
- 浏览器兼容性
=====================================================
""");
}
/**
* 上线前检查项
*/
public void preProductionChecklist() {
log.info("""
==================== 上线前检查项 ====================
✅ 1. 配置检查
- 生产环境配置正确
- 数据库连接配置
- 缓存配置正确
✅ 2. 数据库检查
- 表结构正确
- 索引创建完整
- 初始数据准备
✅ 3. 性能检查
- 慢SQL优化
- 连接池配置优化
- JVM参数优化
✅ 4. 安全检查
- 敏感信息加密
- 权限控制完善
- 防攻击措施
✅ 5. 监控检查
- 日志配置正确
- 监控告警设置
- 健康检查通过
=====================================================
""");
}
/**
* 运维阶段检查项
*/
public void operationChecklist() {
log.info("""
==================== 运维阶段检查项 ====================
🔄 1. 日常监控
- 数据库连接数监控
- 慢SQL监控和优化
- 系统资源监控
🔄 2. 性能优化
- 定期分析执行计划
- 索引优化和重建
- 数据库参数调优
🔄 3. 数据管理
- 数据备份和恢复测试
- 数据清理和归档
- 数据一致性检查
🔄 4. 版本管理
- 数据库脚本版本管理
- 应用版本回滚计划
- 依赖版本管理
🔄 5. 故障处理
- 故障应急预案
- 数据恢复流程
- 问题排查手册
=====================================================
""");
}
}
课程总结
通过这八个模块的系统学习,您已经掌握了MyBatis-Plus在企业级开发中的完整知识体系:
🎯 核心收获
- 完整的知识体系:从基础配置到高级特性,从单表操作到复杂关联查询
- 企业级最佳实践:基于真实项目经验的配置和优化方案
- 性能优化能力:全方位的性能监控和优化策略
- 架构设计思维:大型项目的架构设计和扩展方案
🚀 下一步建议
- 实践项目:选择真实业务场景进行项目实践
- 源码学习:深入理解MyBatis-Plus的底层实现原理
- 技术拓展:学习相关的分布式、云原生技术
- 社区参与:参与开源项目,分享技术经验
📚 持续学习
技术永远在进步,建议您:
- 关注MyBatis-Plus官方更新
- 参与技术社区讨论
- 定期复盘和总结项目经验
- 分享和学习最佳实践
感谢您完成本课程的学习!希望这套企业级MyBatis-Plus实战教学能够帮助您在实际工作中更加游刃有余,构建高性能、可维护的企业级应用。
浙公网安备 33010602011771号