
结合实际开发中的用户管理系统场景,分享 MyBatis-Plus(MP)的落地实践,包含核心功能落地、问题踩坑、优化技巧,让你快速将 MP 应用到真实项目中。
搭建一个「企业级用户管理系统」核心模块,支持:
技术栈:Spring Boot 3.x + MyBatis-Plus 3.5.5 + MySQL 8.0 + Lombok
先设计 t_user 表,贴合企业规范:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名(唯一)',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0-未删,1-已删',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';com.example.user
├── UserApplication.java(启动类)
├── config/(配置类)
│ ├── MyMetaObjectHandler.java(自动填充配置)
│ └── MyBatisPlusConfig.java(乐观锁等插件配置)
├── entity/(实体类)
│ └── User.java
├── mapper/(Mapper接口)
│ └── UserMapper.java
├── service/(服务层)
│ ├── UserService.java(接口)
│ └── impl/
│ └── UserServiceImpl.java(实现类)
├── controller/(控制层)
│ └── UserController.java
├── dto/(入参DTO)
│ ├── UserQueryDTO.java(查询入参)
│ └── UserAddDTO.java(新增入参)
└── vo/(出参VO)
└── UserVO.java(返回结果)import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user") // 表名映射(与类名一致可省略)
public class User {
// 主键:MySQL自增(如果是分布式场景,改为 IdType.ASSIGN_ID 用雪花算法)
@TableId(type = IdType.AUTO)
private Long id;
// 用户名(唯一索引)
@TableField("username") // 字段名与属性名一致可省略,这里显式指定更清晰
private String username;
private String password;
private String realName;
private String phone;
private Long deptId;
// 状态:0-禁用,1-正常
private Integer status;
// 乐观锁版本号(必加 @Version 注解)
@Version
private Integer version;
// 逻辑删除(配合全局配置)
@TableLogic
private Integer isDeleted;
// 自动填充:创建时间(仅插入时填充)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
// 自动填充:更新时间(插入+更新时填充)
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入时填充
@Override
public void insertFill(MetaObject metaObject) {
// 严格填充:只有字段为 null 时才填充(避免覆盖手动设置的值)
strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
// 更新时填充
@Override
public void updateFill(MetaObject metaObject) {
strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}继承 BaseMapper 获得基础CRUD,自定义复杂SQL:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.user.entity.User;
import com.example.user.vo.UserVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserMapper extends BaseMapper<User> {
// 1. 自定义分页:关联部门表查询用户详情(多表联查示例)
IPage<UserVO> selectUserPage(Page<User> page, @Param("query") UserQueryDTO query);
// 2. 批量插入(MP自带 saveBatch 基于JDBC批处理,也可自定义XML优化)
int batchInsert(@Param("list") List<User> userList);
}复杂SQL写在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.example.user.mapper.UserMapper">
<!-- 多表联查分页:用户+部门名称 -->
<select id="selectUserPage" resultType="com.example.user.vo.UserVO">
SELECT
u.id, u.username, u.real_name, u.phone, u.status,
d.dept_name, u.create_time
FROM t_user u
LEFT JOIN t_dept d ON u.dept_id = d.id
WHERE u.is_deleted = 0
<!-- 动态条件:用户名模糊查询 -->
<if test="query.username != null and query.username != ''">
AND u.username LIKE CONCAT('%', #{query.username}, '%')
</if>
<!-- 动态条件:部门ID筛选 -->
<if test="query.deptId != null">
AND u.dept_id = #{query.deptId}
</if>
<!-- 动态条件:状态筛选 -->
<if test="query.status != null">
AND u.status = #{query.status}
</if>
<!-- 排序:按创建时间降序 -->
ORDER BY u.create_time DESC
</select>
<!-- 批量插入(自定义XML,比MP自带 saveBatch 更灵活,可添加自定义逻辑) -->
<insert id="batchInsert">
INSERT INTO t_user (username, password, real_name, phone, dept_id, status, version, create_time, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.username}, #{item.password}, #{item.realName}, #{item.phone},
#{item.deptId}, #{item.status}, #{item.version},
#{item.createTime}, #{item.updateTime}
)
</foreach>
</insert>
</mapper>Service 层统一封装CRUD,避免Controller直接操作Mapper:
// UserService.java(接口)
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.user.entity.User;
import com.example.user.dto.UserAddDTO;
import com.example.user.dto.UserQueryDTO;
import com.example.user.dto.UserUpdateDTO;
import com.example.user.vo.UserVO;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface UserService extends IService<User> {
// 新增用户(密码加密等业务逻辑)
boolean addUser(UserAddDTO dto);
// 分页查询用户(支持多条件筛选)
IPage<UserVO> queryUserPage(Integer pageNum, Integer pageSize, UserQueryDTO query);
// 批量更新用户状态
boolean batchUpdateStatus(List<Long> ids, Integer status);
// 乐观锁更新用户信息(解决并发冲突)
boolean updateUserWithLock(UserUpdateDTO dto);
}// UserServiceImpl.java(实现类)
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.user.entity.User;
import com.example.user.dto.UserAddDTO;
import com.example.user.dto.UserQueryDTO;
import com.example.user.dto.UserUpdateDTO;
import com.example.user.mapper.UserMapper;
import com.example.user.service.UserService;
import com.example.user.vo.UserVO;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private PasswordEncoder passwordEncoder; // Spring Security 密码加密
// 新增用户:密码加密、参数转换
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addUser(UserAddDTO dto) {
// 1. 校验用户名唯一性(MP条件构造器)
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>()
.eq(User::getUsername, dto.getUsername());
if (baseMapper.selectCount(queryWrapper) > 0) {
throw new RuntimeException("用户名已存在");
}
// 2. DTO 转 Entity(密码加密)
User user = new User();
BeanUtils.copyProperties(dto, user);
user.setPassword(passwordEncoder.encode(dto.getPassword())); // 加密密码
user.setVersion(0); // 初始版本号
user.setStatus(1); // 默认正常状态
// 3. 插入数据库(自动填充 createTime/updateTime)
return save(user);
}
// 分页查询:多条件筛选+多表联查
@Override
public IPage<UserVO> queryUserPage(Integer pageNum, Integer pageSize, UserQueryDTO query) {
// 1. 构建分页对象(pageNum从1开始,MP自动处理分页逻辑)
Page<User> page = new Page<>(pageNum, pageSize);
// 2. 调用自定义分页SQL(关联部门表)
return baseMapper.selectUserPage(page, query);
}
// 批量更新状态:支持批量启用/禁用
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateStatus(List<Long> ids, Integer status) {
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<User>()
.in(User::getId, ids) // 批量ID
.set(User::getStatus, status); // 更新状态
return baseMapper.update(null, updateWrapper) > 0;
}
// 乐观锁更新:解决并发更新冲突
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUserWithLock(UserUpdateDTO dto) {
// 1. 查询用户(获取当前版本号)
User user = baseMapper.selectById(dto.getId());
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 2. 校验版本号(DTO中传入前端获取的版本号,防止并发覆盖)
if (!user.getVersion().equals(dto.getVersion())) {
throw new RuntimeException("数据已被修改,请刷新后重试");
}
// 3. 更新数据(MP自动拼接 version = version + 1,并发时版本号不匹配则更新失败)
BeanUtils.copyProperties(dto, user);
return updateById(user);
}
}RESTful 风格接口,接收前端请求:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.user.dto.UserAddDTO;
import com.example.user.dto.UserQueryDTO;
import com.example.user.dto.UserUpdateDTO;
import com.example.user.service.UserService;
import com.example.user.vo.UserVO;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Resource
private UserService userService;
// 新增用户
@PostMapping
public String addUser(@RequestBody UserAddDTO dto) {
boolean success = userService.addUser(dto);
return success ? "新增成功" : "新增失败";
}
// 分页查询用户
@GetMapping
public IPage<UserVO> queryUserPage(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
UserQueryDTO query) {
return userService.queryUserPage(pageNum, pageSize, query);
}
// 批量更新状态
@PutMapping("/status")
public String batchUpdateStatus(
@RequestParam List<Long> ids,
@RequestParam Integer status) {
boolean success = userService.batchUpdateStatus(ids, status);
return success ? "更新成功" : "更新失败";
}
// 乐观锁更新用户
@PutMapping
public String updateUser(@RequestBody UserUpdateDTO dto) {
boolean success = userService.updateUserWithLock(dto);
return success ? "更新成功" : "更新失败(数据已被修改)";
}
// 逻辑删除用户(MP自动拼接 is_deleted = 1)
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
return userService.removeById(id) ? "删除成功" : "删除失败";
}
}spring:
datasource:
url: jdbc:mysql://localhost:3306/enterprise_user?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
security:
user:
name: admin
password: 123456
# MyBatis-Plus 核心配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml # XML文件路径
type-aliases-package: com.example.user.entity # 实体类别名包
configuration:
map-underscore-to-camel-case: true # 驼峰命名转换(必须开启)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境打印SQL
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除字段
logic-delete-value: 1 # 已删除
logic-not-delete-value: 0 # 未删除
id-type: AUTO # 主键自增(分布式场景改为 ASSIGN_ID)@Data 会自动生成,若手动写实体类需确保 getter 存在,否则 Lambda 表达式无法获取字段。// 错误:不管 query.getUsername() 是否为null,都会拼接 LIKE(导致 %null%)
queryWrapper.like(User::getUsername, query.getUsername());
// 正确:只有 username 不为空时才拼接
queryWrapper.like(query.getUsername() != null, User::getUsername, query.getUsername());ORDER BY 字段(如 create_time)、筛选条件字段(如 dept_id、status)必须建索引,否则全表扫描导致分页卡顿。selectPage 会因 COUNT(*) 性能下降,改用 MP 游标查询 selectCursor:// 游标查询(避免一次性加载大量数据到内存)
Cursor<User> cursor = userMapper.selectCursor(new LambdaQueryWrapper<User>().gt(User::getCreateTime, "2024-01-01"));
cursor.forEach(user -> {
// 处理单条数据
});saveBatch 最佳实践:默认批量大小是 1000,可手动指定(根据数据库连接池配置调整,避免超连接池限制):userService.saveBatch(userList, 500); // 每500条提交一次updateBatchById:比循环 updateById 效率高10倍以上(内部复用 SQLSession):userService.updateBatchById(userList, 300);@TableLogic 注解,或全局配置 logic-delete-field 字段名与实体类属性名不一致(如实体类是 isDeleted,数据库是 is_deleted,需开启驼峰转换)。AND is_deleted = 0。@Version 注解,或数据库 version 字段未设默认值(需默认 0)。map-underscore-to-camel-case 配置为 false,或实体类属性名与数据库字段名驼峰对应错误(如数据库 real_name,实体类 realname 无驼峰,导致映射失败)。Page 的 pageNum 传错(MP 页码从 1 开始,若传 0 会查询第 0 页,导致无数据),或查询条件过滤了所有数据(如 status 传了不存在的值)。如果是 SaaS 系统,需支持多租户隔离,可通过 MP 多租户插件实现:
// 多租户配置类
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件:自动拼接 tenant_id = 当前租户ID
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从上下文获取当前租户ID(如登录用户信息)
return new LongValue(TenantContext.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id"; // 数据库租户字段名
}
}));
return interceptor;
}
}结合 Spring Security,实现基于角色的数据权限(如部门管理员只能看本部门用户),可通过 MP 条件构造器动态拼接数据权限条件:
// 数据权限工具类
public class DataScopeUtil {
public static <T> void addDataScope(LambdaQueryWrapper<T> queryWrapper, User loginUser) {
// 超级管理员:无权限限制
if (loginUser.getRoleId() == 1) {
return;
}
// 部门管理员:只能看本部门用户
queryWrapper.eq(User::getDeptId, loginUser.getDeptId());
}
}
// Service 层使用
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>();
DataScopeUtil.addDataScope(queryWrapper, loginUser); // 动态添加数据权限
List<User> userList = baseMapper.selectList(queryWrapper);开发环境集成 MP 性能分析插件,监控 SQL 执行时间,及时优化慢查询:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 性能分析插件:SQL执行时间超过1秒报警
interceptor.addInnerInterceptor(new PerformanceInterceptor()
.setFormat(true) // 格式化SQL
.setMaxTime(1000)); // 最大执行时间(毫秒)
return interceptor;
}MyBatis-Plus 核心是「增强不改变」,实战中需把握3个核心:
saveBatch/updateBatchById。通过以上实战落地,可快速搭建稳定、高效的企业级CRUD应用,减少80%的重复编码,让开发精力聚焦在业务逻辑上。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。