首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >MyBatis-Plus 实战分享:从 0 到 1 搭建企业级 CRUD 应用

MyBatis-Plus 实战分享:从 0 到 1 搭建企业级 CRUD 应用

原创
作者头像
小焱
发布2025-11-04 13:45:29
发布2025-11-04 13:45:29
950
举报
文章被收录于专栏:Java开发Java开发

MyBatis-Plus 实战分享:从0到1搭建企业级CRUD应用

结合实际开发中的用户管理系统场景,分享 MyBatis-Plus(MP)的落地实践,包含核心功能落地、问题踩坑、优化技巧,让你快速将 MP 应用到真实项目中。

一、实战场景背景

搭建一个「企业级用户管理系统」核心模块,支持:

  • 用户基础CRUD(新增、查询、更新、删除)
  • 复杂条件筛选(多字段组合查询、模糊搜索)
  • 分页查询(支持排序、总数统计)
  • 批量操作(批量导入用户、批量更新状态)
  • 逻辑删除(防止误删数据)
  • 自动填充(创建时间、更新时间)
  • 乐观锁(解决并发更新冲突)

技术栈:Spring Boot 3.x + MyBatis-Plus 3.5.5 + MySQL 8.0 + Lombok

二、分步实战落地

1. 数据库设计(核心表结构)

先设计 ​​t_user​​ 表,贴合企业规范:

代码语言:javascript
复制
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='用户表';
2. 项目结构搭建(分层设计)
代码语言:javascript
复制
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(返回结果)
3. 核心组件实现
(1)实体类 User.java(映射+注解配置)
代码语言:javascript
复制
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;
}
(2)自动填充配置(MyMetaObjectHandler.java)
代码语言:javascript
复制
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());
    }
}
(3)Mapper 接口(UserMapper.java)

继承 ​​BaseMapper​​ 获得基础CRUD,自定义复杂SQL:

代码语言:javascript
复制
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);
}
(4)Mapper XML(UserMapper.xml)

复杂SQL写在XML中,保持可读性:

代码语言:javascript
复制
<?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>
(5)Service 层封装(业务逻辑核心)

Service 层统一封装CRUD,避免Controller直接操作Mapper:

代码语言:javascript
复制
// 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);
}
代码语言:javascript
复制
// 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);
    }
}
(6)Controller 层(接口暴露)

RESTful 风格接口,接收前端请求:

代码语言:javascript
复制
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) ? "删除成功" : "删除失败";
    }
}
4. 关键配置补充(application.yml)
代码语言:javascript
复制
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)

三、实战关键技巧与踩坑记录

1. 核心技巧(提升开发效率)
(1)Lambda条件构造器避坑
  • 必须保证实体类有 getter 方法:Lombok ​​@Data​​ 会自动生成,若手动写实体类需确保 getter 存在,否则 Lambda 表达式无法获取字段。
  • 动态条件优先用带条件判断的方法
代码语言:javascript
复制
// 错误:不管 query.getUsername() 是否为null,都会拼接 LIKE(导致 %null%)
queryWrapper.like(User::getUsername, query.getUsername());

// 正确:只有 username 不为空时才拼接
queryWrapper.like(query.getUsername() != null, User::getUsername, query.getUsername());
(2)分页查询优化
  • 分页必加索引:​​ORDER BY​​ 字段(如 ​​create_time​​)、筛选条件字段(如 ​​dept_id​​、​​status​​)必须建索引,否则全表扫描导致分页卡顿。
  • 大数据量分页用游标查询:若数据量超10万条,​​selectPage​​ 会因 ​​COUNT(*)​​ 性能下降,改用 MP 游标查询 ​​selectCursor​​:
代码语言:javascript
复制
// 游标查询(避免一次性加载大量数据到内存)
Cursor<User> cursor = userMapper.selectCursor(new LambdaQueryWrapper<User>().gt(User::getCreateTime, "2024-01-01"));
cursor.forEach(user -> {
    // 处理单条数据
});
(3)批量操作优化
  • MP 自带 saveBatch​ 最佳实践:默认批量大小是 1000,可手动指定(根据数据库连接池配置调整,避免超连接池限制):
代码语言:javascript
复制
userService.saveBatch(userList, 500); // 每500条提交一次
  • 批量更新用 updateBatchById​​:比循环 ​​updateById​​ 效率高10倍以上(内部复用 SQLSession):
代码语言:javascript
复制
userService.updateBatchById(userList, 300);
2. 踩坑记录(真实项目问题)
(1)逻辑删除不生效
  • 原因1:实体类未加 ​​@TableLogic​​ 注解,或全局配置 ​​logic-delete-field​​ 字段名与实体类属性名不一致(如实体类是 ​​isDeleted​​,数据库是 ​​is_deleted​​,需开启驼峰转换)。
  • 原因2:自定义 XML 未手动过滤逻辑删除字段,MP 只对 BaseMapper 方法自动生效,自定义 SQL 需手动加 ​​AND is_deleted = 0​​。
(2)乐观锁更新失败
  • 原因1:实体类未加 ​​@Version​​ 注解,或数据库 ​​version​​ 字段未设默认值(需默认 0)。
  • 原因2:并发时版本号不匹配,MP 会返回更新行数 0,需在 Service 层捕获并提示用户“数据已被修改”。
(3)驼峰转换失效
  • 原因:​​map-underscore-to-camel-case​​ 配置为 ​​false​​,或实体类属性名与数据库字段名驼峰对应错误(如数据库 ​​real_name​​,实体类 ​​realname​​ 无驼峰,导致映射失败)。
(4)分页总条数为0
  • 原因:分页对象 ​​Page​​ 的 ​​pageNum​​ 传错(MP 页码从 1 开始,若传 0 会查询第 0 页,导致无数据),或查询条件过滤了所有数据(如 ​​status​​ 传了不存在的值)。

四、企业级扩展建议

1. 多租户场景适配

如果是 SaaS 系统,需支持多租户隔离,可通过 MP 多租户插件实现:

代码语言:javascript
复制
// 多租户配置类
@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;
    }
}
2. 数据权限控制

结合 Spring Security,实现基于角色的数据权限(如部门管理员只能看本部门用户),可通过 MP 条件构造器动态拼接数据权限条件:

代码语言:javascript
复制
// 数据权限工具类
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);
3. 性能监控

开发环境集成 MP 性能分析插件,监控 SQL 执行时间,及时优化慢查询:

代码语言:javascript
复制
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 性能分析插件:SQL执行时间超过1秒报警
    interceptor.addInnerInterceptor(new PerformanceInterceptor()
            .setFormat(true) // 格式化SQL
            .setMaxTime(1000)); // 最大执行时间(毫秒)
    return interceptor;
}

五、实战总结

MyBatis-Plus 核心是「增强不改变」,实战中需把握3个核心:

  1. 规范映射:数据库设计遵循下划线命名,实体类用驼峰+注解,开启驼峰转换,减少映射错误。
  2. 高效CRUD:优先用 BaseMapper + Lambda 条件构造器,复杂SQL抽离到XML,批量操作选 ​​saveBatch​​/​​updateBatchById​​。
  3. 避坑优化:逻辑删除、乐观锁、分页等功能需配置到位,大数据量场景注意索引和游标查询,并发场景用乐观锁解决冲突。

通过以上实战落地,可快速搭建稳定、高效的企业级CRUD应用,减少80%的重复编码,让开发精力聚焦在业务逻辑上。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MyBatis-Plus 实战分享:从0到1搭建企业级CRUD应用
    • 一、实战场景背景
    • 二、分步实战落地
      • 1. 数据库设计(核心表结构)
      • 2. 项目结构搭建(分层设计)
      • 3. 核心组件实现
      • 4. 关键配置补充(application.yml)
    • 三、实战关键技巧与踩坑记录
      • 1. 核心技巧(提升开发效率)
      • 2. 踩坑记录(真实项目问题)
    • 四、企业级扩展建议
      • 1. 多租户场景适配
      • 2. 数据权限控制
      • 3. 性能监控
    • 五、实战总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档