当前位置: 首页 > news >正文

MyBatis-Plus 详解教程

文章目录

    • 1. MyBatis-Plus 简介
      • 1.1 什么是 MyBatis-Plus?
      • 1.2 为什么要使用 MyBatis-Plus?
        • 传统 MyBatis 的痛点
        • MyBatis-Plus 的优势
      • 1.3 MyBatis-Plus 与 MyBatis 的关系
    • 2. 快速开始
      • 2.1 环境要求
      • 2.2 依赖引入
        • Maven
        • Gradle
      • 2.3 数据库准备
      • 2.4 配置 Spring Boot
      • 2.5 创建实体类
      • 2.6 创建 Mapper 接口
      • 2.7 配置 Spring Boot 启动类
      • 2.8 编写简单测试类
    • 3. 核心功能
      • 3.1 通用 CRUD
        • 3.1.1 插入操作
        • 3.1.2 删除操作
        • 3.1.3 更新操作
        • 3.1.4 查询操作
      • 3.2 条件构造器
        • 3.2.1 QueryWrapper 示例
        • 3.2.2 UpdateWrapper 示例
        • 3.2.3 LambdaQueryWrapper 示例
        • 3.2.4 LambdaUpdateWrapper 示例
      • 3.3 分页查询
        • 3.3.1 配置分页插件
        • 3.3.2 使用分页查询
      • 3.4 自定义 SQL
        • 3.4.1 在 Mapper 接口中定义方法
        • 3.4.2 创建对应的 XML 映射文件
    • 4. 进阶特性
      • 4.1 ActiveRecord 模式
        • 4.1.1 启用 ActiveRecord
        • 4.1.2 使用 ActiveRecord 操作
      • 4.2 逻辑删除
        • 4.2.1 全局配置
        • 4.2.2 实体类配置
        • 4.2.3 使用逻辑删除
      • 4.3 字段自动填充
        • 4.3.1 实体类配置
        • 4.3.2 实现元数据处理器
        • 4.3.3 测试自动填充
      • 4.4 枚举类型处理
        • 4.4.1 定义枚举类
        • 4.4.2 配置枚举包扫描
        • 4.4.3 在实体类中使用枚举
        • 4.4.4 测试枚举类型
      • 4.5 乐观锁插件
        • 4.5.1 数据库添加 version 字段
        • 4.5.2 实体类添加 version 字段
        • 4.5.3 配置乐观锁插件
        • 4.5.4 测试乐观锁
      • 4.6 SQL 性能分析插件
        • 4.6.1 配置性能分析插件
        • 4.6.2 执行SQL查看分析结果
      • 4.7 多租户插件
        • 4.7.1 基于字段的多租户
          • 配置多租户插件
          • 修改实体类
          • 在请求中设置租户ID
    • 5. Service 层封装
      • 5.1 IService 接口和实现
        • 5.1.1 定义 Service 接口
        • 5.1.2 实现 Service 接口
      • 5.2 使用 Service 方法
      • 5.3 批量操作
    • 6. 代码生成器
      • 6.1 引入依赖
      • 6.2 编写代码生成器
      • 6.3 运行代码生成器
    • 7. 动态表名
      • 7.1 配置动态表名插件
      • 7.2 测试动态表名
    • 8. JSON 字段处理
      • 8.1 定义类型处理器
      • 8.2 在实体类中使用 JSON 处理器
      • 8.3 注册类型处理器
    • 9. 高级查询
      • 9.1 聚合查询
      • 9.2 子查询
      • 9.3 复杂条件查询
      • 9.4 动态条件查询
    • 10. 性能优化
      • 10.1 减少不必要的查询字段
      • 10.2 批量操作优化
      • 10.3 避免全表更新和删除
      • 10.4 使用索引
    • 11. 常见问题与解决方案
      • 11.1 ID 生成问题
      • 11.2 字段名映射问题
      • 11.3 分页问题
      • 11.4 逻辑删除问题
      • 11.5 复杂查询问题
      • 11.6 XML 映射文件无法加载
    • 12. 实战案例:构建完整的用户管理系统
      • 12.1 数据库设计
      • 12.2 实体类设计
        • 用户实体
        • 部门实体
        • 角色实体
        • 用户角色关联实体
        • 基础实体
      • 12.3 Mapper 接口
        • 用户 Mapper
        • 用户 Mapper XML
      • 12.4 Service 层
        • 用户 Service 接口
        • 用户 Service 实现
      • 12.5 Controller 层
      • 12.6 DTO 和查询对象
      • 12.7 统一返回结果
      • 12.8 配置文件
    • 13. 总结与最佳实践
      • 13.1 MyBatis-Plus 核心优势
      • 13.2 最佳实践
        • 13.2.1 Entity 设计
        • 13.2.2 Mapper 设计
        • 13.2.3 Service 设计
        • 13.2.4 性能优化
        • 13.2.5 安全性
      • 13.3 进阶学习方向
      • 13.4 总结
    • 附录:常用注解说明
      • @TableName
      • @TableId
      • @TableField
      • @TableLogic
      • @Version
      • @EnumValue
      • @KeySequence
      • @OrderBy
    • 13. 总结与最佳实践
      • 13.1 MyBatis-Plus 核心优势
      • 13.2 最佳实践
        • 13.2.1 Entity 设计
        • 13.2.2 Mapper 设计
        • 13.2.3 Service 设计
        • 13.2.4 性能优化
        • 13.2.5 安全性
      • 13.3 进阶学习方向
      • 13.4 总结
    • 附录:常用注解说明
      • @TableName
      • @TableId
      • @TableField
      • @TableLogic
      • @Version
      • @EnumValue
      • @KeySequence
      • @OrderBy

1. MyBatis-Plus 简介

1.1 什么是 MyBatis-Plus?

MyBatis-Plus(简称 MP)是一个基于 MyBatis 框架的增强工具,它在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

MyBatis-Plus 的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,齐力面对难题。它提供了诸多功能特性,如:

  • 无侵入:只做增强不做改变,引入它不会对现有项目产生影响
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便地编写各类查询条件,无需担心字段写错
  • 支持主键自动生成:支持多种主键策略,可自由配置,完美解决主键问题
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,写分页等同于写基本 List 查询
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

1.2 为什么要使用 MyBatis-Plus?

传统 MyBatis 的痛点

在使用原生 MyBatis 时,我们经常需要进行以下重复工作:

  1. 编写大量的 XML 映射文件,即使是最简单的 CRUD 操作
  2. 在执行分页查询时,需要手写分页相关的 SQL 语句
  3. 没有统一的 Service 层封装,导致代码重复
  4. SQL 语句容易写错,且不同数据库的 SQL 语法存在差异
MyBatis-Plus 的优势

相比于原生 MyBatis,使用 MyBatis-Plus 可以带来以下优势:

  1. 减少代码量:内置通用 Mapper、Service,单表 CRUD 操作无需编写 SQL
  2. 提高开发效率:避免了手动编写基础 CRUD 操作,团队开发更加规范高效
  3. 增强功能特性:内置主键生成、分页、性能分析、全局拦截等功能
  4. 支持 Lambda 表达式:使用 Lambda 编写查询条件,类型安全,避免字段名称错误
  5. 多种主键策略:内置多种主键生成策略,支持自定义主键生成器
  6. 代码生成器:可快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码

1.3 MyBatis-Plus 与 MyBatis 的关系

MyBatis-Plus 是在 MyBatis 的基础上构建的增强工具,其核心功能仍然依赖于 MyBatis。它通过自动注入的通用方法,减少了手动编写 SQL 的工作量,但同时也完全兼容 MyBatis 的所有功能。你可以:

  • 使用 MyBatis-Plus 的通用 Mapper 执行基础 CRUD
  • 使用原生 MyBatis 的方式编写复杂查询
  • 混合使用两者,优势互补

实际上,MyBatis-Plus 底层依然是调用 MyBatis 的API,它只是为 MyBatis 赋予了更多的能力而已。

2. 快速开始

2.1 环境要求

  • JDK 1.8 及以上
  • Maven 或 Gradle
  • Spring Boot 2.0 及以上(本教程以 Spring Boot 为例)

2.2 依赖引入

在 Spring Boot 项目中,我们只需要引入 MyBatis-Plus 的 starter 依赖即可。

Maven
<!-- Maven 项目 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency><!-- 数据库驱动(以 MySQL 为例) -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version>
</dependency>
Gradle
// Gradle 项目
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.3.1'
implementation 'mysql:mysql-connector-java:8.0.28'

2.3 数据库准备

创建一个简单的用户表作为示例:

CREATE TABLE `user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(30) DEFAULT NULL COMMENT '姓名',`age` int(11) DEFAULT NULL COMMENT '年龄',`email` varchar(50) DEFAULT NULL COMMENT '邮箱',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除:0-未删除,1-已删除',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 插入一些测试数据
INSERT INTO `user` (`name`, `age`, `email`) VALUES
('张三', 18, 'zhangsan@example.com'),
('李四', 20, 'lisi@example.com'),
('王五', 28, 'wangwu@example.com'),
('赵六', 21, 'zhaoliu@example.com'),
('孙七', 24, 'sunqi@example.com');

2.4 配置 Spring Boot

application.yml 中配置数据库连接信息:

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/your_database?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: your_password# MyBatis-Plus 配置
mybatis-plus:configuration:# 开启下划线转驼峰map-underscore-to-camel-case: true# 开启SQL语句打印(开发环境使用)log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 全局配置global-config:db-config:# 主键类型id-type: auto# 逻辑删除配置logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0# XML 映射文件位置mapper-locations: classpath*:/mapper/**/*.xml

2.5 创建实体类

创建对应用户表的实体类:

package com.example.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import lombok.Data;@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;private LocalDateTime createTime;private LocalDateTime updateTime;@TableLogicprivate Integer deleted;
}

2.6 创建 Mapper 接口

创建 UserMapper 接口,继承 BaseMapper:

package com.example.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper extends BaseMapper<User> {// 无需编写任何方法,即可获得 CRUD 功能
}

2.7 配置 Spring Boot 启动类

确保 Spring Boot 启动类中添加了 MapperScan 注解:

package com.example;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

2.8 编写简单测试类

使用 Spring Boot 的测试功能编写一个简单的测试类:

package com.example;import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
public class SampleTest {@Autowiredprivate UserMapper userMapper;@Testpublic void testSelect() {System.out.println("---------- 查询所有用户 ----------");List<User> userList = userMapper.selectList(null);userList.forEach(System.out::println);}@Testpublic void testInsert() {System.out.println("---------- 插入用户 ----------");User user = new User();user.setName("小明");user.setAge(22);user.setEmail("xiaoming@example.com");int result = userMapper.insert(user);System.out.println("插入结果:" + result);System.out.println("插入后,自动获取的ID:" + user.getId());}@Testpublic void testUpdate() {System.out.println("---------- 更新用户 ----------");User user = userMapper.selectById(1);if (user != null) {user.setAge(user.getAge() + 1);int result = userMapper.updateById(user);System.out.println("更新结果:" + result);}}@Testpublic void testDelete() {System.out.println("---------- 删除用户 ----------");int result = userMapper.deleteById(1);System.out.println("删除结果:" + result);}
}

3. 核心功能

3.1 通用 CRUD

通过继承 BaseMapper,你的 Mapper 接口将自动拥有通用的 CRUD 能力。以下是 BaseMapper 提供的常用方法:

3.1.1 插入操作
// 插入一条记录
int insert(T entity);

示例:

User user = new User();
user.setName("小红");
user.setAge(23);
user.setEmail("xiaohong@example.com");// 插入一条记录
int result = userMapper.insert(user);
3.1.2 删除操作
// 根据 ID 删除
int deleteById(Serializable id);// 根据实体(ID)删除
int deleteById(T entity);// 根据 columnMap 条件删除
int deleteByMap(@Param("cm") Map<String, Object> columnMap);// 根据 entity 条件删除
int delete(@Param("ew") Wrapper<T> queryWrapper);// 删除(根据ID 批量删除)
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);

示例:

// 根据ID删除
userMapper.deleteById(1);// 根据多个ID批量删除
List<Long> ids = Arrays.asList(1L, 2L, 3L);
userMapper.deleteBatchIds(ids);// 根据条件删除
Map<String, Object> map = new HashMap<>();
map.put("age", 18);
userMapper.deleteByMap(map);  // 删除年龄为18的用户// 使用条件构造器
userMapper.delete(new LambdaQueryWrapper<User>().eq(User::getAge, 18).like(User::getName, "张"));
3.1.3 更新操作
// 根据 ID 更新
int updateById(@Param("et") T entity);// 根据 whereEntity 条件,更新记录
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

示例:

// 根据ID更新
User user = userMapper.selectById(1);
user.setAge(28);
userMapper.updateById(user);// 根据条件更新
User updateUser = new User();
updateUser.setAge(30);// 将所有名字中包含"张"的用户年龄更新为30
userMapper.update(updateUser, new LambdaQueryWrapper<User>().like(User::getName, "张"));// 也可以不创建实体,直接使用UpdateWrapper
userMapper.update(null, new UpdateWrapper<User>().set("age", 35).like("name", "张"));
3.1.4 查询操作
// 根据 ID 查询
T selectById(Serializable id);// 根据 ID 批量查询
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);// 根据 columnMap 条件查询
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);// 根据 entity 条件,查询一条记录
T selectOne(@Param("ew") Wrapper<T> queryWrapper);// 根据 Wrapper 条件,查询总记录数
Long selectCount(@Param("ew") Wrapper<T> queryWrapper);// 根据 entity 条件,查询全部记录
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);

示例:

// 根据ID查询
User user = userMapper.selectById(1);// 根据多个ID批量查询
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<User> users = userMapper.selectBatchIds(ids);// 根据条件查询
Map<String, Object> map = new HashMap<>();
map.put("age", 18);
List<User> users = userMapper.selectByMap(map);  // 查询年龄为18的用户// 使用条件构造器查询单条记录
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getEmail, "zhangsan@example.com"));// 查询符合条件的记录数
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>().gt(User::getAge, 20));  // 查询年龄大于20的用户数量// 条件查询多条记录
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().like(User::getName, "张").ge(User::getAge, 20));  // 查询名字包含"张"且年龄大于等于20的用户

3.2 条件构造器

MyBatis-Plus 提供了强大的条件构造器,帮助你构建复杂的 SQL 查询条件。主要有以下几种实现:

  • QueryWrapper:普通查询条件构造器
  • UpdateWrapper:更新条件构造器
  • LambdaQueryWrapper:支持 Lambda 表达式的查询条件构造器
  • LambdaUpdateWrapper:支持 Lambda 表达式的更新条件构造器
3.2.1 QueryWrapper 示例
@Test
public void testQueryWrapper() {QueryWrapper<User> queryWrapper = new QueryWrapper<>();// WHERE name LIKE '%张%' AND age > 20queryWrapper.like("name", "张").gt("age", 20);// WHERE name LIKE '%张%' OR age > 20queryWrapper.like("name", "张").or().gt("age", 20);// WHERE (name LIKE '%张%' AND age < 40) OR email IS NOT NULLqueryWrapper.nested(w -> w.like("name", "张").lt("age", 40)).or().isNotNull("email");// ORDER BY age DESC, id ASCqueryWrapper.orderByDesc("age").orderByAsc("id");// SELECT id, name, age FROM user ...queryWrapper.select("id", "name", "age");List<User> users = userMapper.selectList(queryWrapper);users.forEach(System.out::println);
}
3.2.2 UpdateWrapper 示例
@Test
public void testUpdateWrapper() {UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();// SET name = '小红', age = 30 WHERE name LIKE '%张%'updateWrapper.set("name", "小红").set("age", 30).like("name", "张");userMapper.update(null, updateWrapper);
}
3.2.3 LambdaQueryWrapper 示例
@Test
public void testLambdaQueryWrapper() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();// WHERE name LIKE '%张%' AND age > 20wrapper.like(User::getName, "张").gt(User::getAge, 20);// 条件判断String name = "张";wrapper.like(StringUtils.isNotBlank(name), User::getName, name);List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}
3.2.4 LambdaUpdateWrapper 示例
@Test
public void testLambdaUpdateWrapper() {LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();// SET name = '小红', age = 30 WHERE name LIKE '%张%'wrapper.set(User::getName, "小红").set(User::getAge, 30).like(User::getName, "张");userMapper.update(null, wrapper);
}

3.3 分页查询

MyBatis-Plus 内置了分页插件,使用非常简单。

3.3.1 配置分页插件
package com.example.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}
3.3.2 使用分页查询
@Test
public void testPage() {// 创建分页对象,参数分别是:当前页,每页显示条数Page<User> page = new Page<>(1, 2);// 直接传入分页对象即可Page<User> userPage = userMapper.selectPage(page, null);System.out.println("总记录数:" + userPage.getTotal());System.out.println("总页数:" + userPage.getPages());System.out.println("当前页:" + userPage.getCurrent());System.out.println("每页显示条数:" + userPage.getSize());System.out.println("是否有上一页:" + userPage.hasPrevious());System.out.println("是否有下一页:" + userPage.hasNext());// 获取分页数据List<User> records = userPage.getRecords();records.forEach(System.out::println);
}@Test
public void testPageWithCondition() {// 创建分页对象Page<User> page = new Page<>(1, 2);// 构建查询条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.ge(User::getAge, 20);  // 年龄大于等于20// 执行带条件的分页查询Page<User> userPage = userMapper.selectPage(page, wrapper);userPage.getRecords().forEach(System.out::println);
}

3.4 自定义 SQL

虽然 MyBatis-Plus 提供了丰富的 CRUD 操作,但在复杂的业务场景下,我们仍然需要自定义 SQL。

3.4.1 在 Mapper 接口中定义方法
@Mapper
public interface UserMapper extends BaseMapper<User> {// 自定义方法@Select("SELECT * FROM user WHERE age > #{minAge}")List<User> selectOlderThan(@Param("minAge") Integer minAge);// 复杂查询可以使用XMLList<User> selectUserWithAddress(Long userId);
}
3.4.2 创建对应的 XML 映射文件

src/main/resources/mapper 目录下创建 UserMapper.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.mapper.UserMapper"><select id="selectUserWithAddress" resultType="com.example.entity.User">SELECT u.* FROM user uLEFT JOIN user_address ua ON u.id = ua.user_idWHERE u.id = #{userId}LIMIT 1</select></mapper>

4. 进阶特性

4.1 ActiveRecord 模式

MyBatis-Plus 支持 ActiveRecord 模式,让实体类具有数据库操作能力。

4.1.1 启用 ActiveRecord

让实体类继承 Model 类即可:

@Data
@TableName("user")
public class User extends Model<User> {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;@TableLogicprivate Integer deleted;
}
4.1.2 使用 ActiveRecord 操作
@Test
public void testActiveRecord() {User user = new User();user.setName("Active Record");user.setAge(25);user.setEmail("ar@example.com");// 插入boolean success = user.insert();System.out.println("插入结果:" + success);// 查询User found = new User().selectById(user.getId());System.out.println("查询结果:" + found);// 更新found.setAge(28);success = found.updateById();System.out.println("更新结果:" + success);// 删除success = found.deleteById();System.out.println("删除结果:" + success);// 查询所有List<User> users = new User().selectAll();users.forEach(System.out::println);
}

4.2 逻辑删除

逻辑删除是将数据标记为已删除,而不是物理删除。MyBatis-Plus 支持全局设置逻辑删除字段。

4.2.1 全局配置

application.yml 中配置:

mybatis-plus:global-config:db-config:# 逻辑删除字段logic-delete-field: deleted# 已删除值logic-delete-value: 1# 未删除值logic-not-delete-value: 0
4.2.2 实体类配置
@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;// 其他字段...@TableLogicprivate Integer deleted;
}
4.2.3 使用逻辑删除

配置完成后,所有的删除操作都会变成逻辑删除,所有的查询操作都会自动过滤已删除数据。

// 执行逻辑删除,实际上是执行UPDATE语句
userMapper.deleteById(1);// 查询会自动过滤已删除数据
List<User> users = userMapper.selectList(null);

如果想要查询包含已删除数据,可以使用自定义SQL:

@Select("SELECT * FROM user")
List<User> selectAllIncludeDeleted();

4.3 字段自动填充

在实际应用中,某些字段需要在插入或更新时自动填充值,如创建时间、更新时间等。MyBatis-Plus 提供了字段自动填充功能。

4.3.1 实体类配置
@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;// 创建时间,插入时自动填充@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;// 更新时间,插入和更新时自动填充@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;@TableLogicprivate Integer deleted;
}
4.3.2 实现元数据处理器
package com.example.config;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {log.info("start insert fill ...");// 起始版本 3.3.0(推荐)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 或者使用// this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);// this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {log.info("start update fill ...");// 起始版本 3.3.0(推荐)this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 或者使用// this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);}
}
4.3.3 测试自动填充
@Test
public void testAutoFill() {User user = new User();user.setName("测试自动填充");user.setAge(25);user.setEmail("test@example.com");// 插入,createTime 和 updateTime 会自动填充userMapper.insert(user);User found = userMapper.selectById(user.getId());System.out.println("插入后:" + found);// 休眠1秒,以观察时间差异try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 更新,updateTime 会自动更新found.setAge(26);userMapper.updateById(found);User updated = userMapper.selectById(user.getId());System.out.println("更新后:" + updated);
}

4.4 枚举类型处理

MyBatis-Plus 支持将数据库字段映射为 Java 枚举类型。

4.4.1 定义枚举类
package com.example.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(0, "正常"),DISABLED(1, "禁用"),DELETED(2, "已删除");@EnumValue // 标记数据库存的值是codeprivate final int code;private final String desc;UserStatus(int code, String desc) {this.code = code;this.desc = desc;}
}
4.4.2 配置枚举包扫描

application.yml 中配置:

mybatis-plus:# 枚举包扫描路径type-enums-package: com.example.enums
4.4.3 在实体类中使用枚举
@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;private UserStatus status; // 使用枚举类型// 其他字段...
}
4.4.4 测试枚举类型
@Test
public void testEnum() {User user = new User();user.setName("枚举测试");user.setAge(30);user.setEmail("enum@example.com");user.setStatus(UserStatus.NORMAL);userMapper.insert(user);User found = userMapper.selectById(user.getId());System.out.println("状态码:" + found.getStatus().getCode());System.out.println("状态描述:" + found.getStatus().getDesc());
}

4.5 乐观锁插件

乐观锁是一种并发控制方法,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。

4.5.1 数据库添加 version 字段
ALTER TABLE `user` ADD COLUMN `version` INT DEFAULT 1;
4.5.2 实体类添加 version 字段
@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;@Version // 乐观锁注解private Integer version;// 其他字段...
}
4.5.3 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 添加乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}
4.5.4 测试乐观锁
@Test
public void testOptimisticLocker() {// 查询User user = userMapper.selectById(1);System.out.println("查询到的数据:" + user);// 修改数据user.setAge(user.getAge() + 1);// 更新 (当更新成功, version 会自增)userMapper.updateById(user);// 查询更新后的结果User updated = userMapper.selectById(1);System.out.println("更新后的数据:" + updated);
}@Test
public void testOptimisticLockerFail() {// 线程1查询User user1 = userMapper.selectById(1);// 线程2查询并更新User user2 = userMapper.selectById(1);user2.setAge(user2.getAge() + 1);int result2 = userMapper.updateById(user2);System.out.println("线程2更新结果:" + result2);// 线程1更新,此时版本已变化,更新失败user1.setAge(100);int result1 = userMapper.updateById(user1);System.out.println("线程1更新结果:" + result1);
}

4.6 SQL 性能分析插件

在开发环境下,SQL 性能分析插件可以帮助我们分析 SQL 执行效率。

4.6.1 配置性能分析插件
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加其他插件...// 添加SQL性能分析插件 (开发环境使用)if (SpringProfileUtil.isDev()) {interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());}return interceptor;}
}// 判断环境的工具类
class SpringProfileUtil {public static boolean isDev() {return Arrays.asList(SpringContextHolder.getApplicationContext().getEnvironment().getActiveProfiles()).contains("dev");}
}
4.6.2 执行SQL查看分析结果

执行任何 SQL 操作后,控制台会输出 SQL 性能分析信息。

4.7 多租户插件

多租户是一种软件架构,多个租户共享相同的系统或软件实例。MyBatis-Plus 提供了多租户插件,支持多种策略。

4.7.1 基于字段的多租户
配置多租户插件
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加多租户插件interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic String getTenantIdColumn() {return "tenant_id";}@Overridepublic boolean ignoreTable(String tableName) {// 不需要租户控制的表return "sys_config".equalsIgnoreCase(tableName);}@Overridepublic Expression getTenantId() {// 获取当前租户IDreturn new StringValue(TenantContextHolder.getTenantId());}}));// 添加其他插件...return interceptor;}
}// 租户上下文,用于存储当前租户ID
class TenantContextHolder {private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();public static void setTenantId(String tenantId) {TENANT_ID.set(tenantId);}public static String getTenantId() {return TENANT_ID.get();}public static void clear() {TENANT_ID.remove();}
}
修改实体类
@Data
@TableName("user")
public class User {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;private String email;private String tenantId; // 租户ID// 其他字段...
}
在请求中设置租户ID
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserMapper userMapper;@GetMapping("/{tenantId}")public List<User> getUsersByTenant(@PathVariable String tenantId) {// 设置当前租户IDTenantContextHolder.setTenantId(tenantId);try {// 查询数据,会自动追加租户条件return userMapper.selectList(null);} finally {// 清除租户IDTenantContextHolder.clear();}}
}

5. Service 层封装

MyBatis-Plus 提供了 Service 层的封装,进一步简化业务逻辑的编写。

5.1 IService 接口和实现

5.1.1 定义 Service 接口
package com.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.User;public interface UserService extends IService<User> {// 自定义方法void updateAgeByName(String name, Integer age);
}
5.1.2 实现 Service 接口
package com.example.service.impl;import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic void updateAgeByName(String name, Integer age) {LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();wrapper.eq(User::getName, name).set(User::getAge, age);this.update(wrapper);}
}

5.2 使用 Service 方法

Service 接口继承了 IService,提供了丰富的操作方法:

@SpringBootTest
public class ServiceTest {@Autowiredprivate UserService userService;@Testpublic void testSaveBatch() {List<User> users = new ArrayList<>();for (int i = 0; i < 5; i++) {User user = new User();user.setName("批量用户" + i);user.setAge(20 + i);user.setEmail("batch" + i + "@example.com");users.add(user);}// 批量插入boolean success = userService.saveBatch(users);System.out.println("批量插入结果:" + success);}@Testpublic void testChain() {// 链式操作userService.lambdaQuery().eq(User::getAge, 20).like(User::getName, "张").list().forEach(System.out::println);// 链式更新boolean updated = userService.lambdaUpdate().eq(User::getName, "张三").set(User::getAge, 30).update();System.out.println("更新结果:" + updated);}@Testpublic void testCustomMethod() {// 调用自定义方法userService.updateAgeByName("李四", 25);// 查询验证User user = userService.lambdaQuery().eq(User::getName, "李四").one();System.out.println("更新后的用户:" + user);}@Testpublic void testTransaction() {// 测试事务try {userService.updateAgeByName("不存在的用户", 100);// 后续可能会有异常操作,触发事务回滚} catch (Exception e) {System.out.println("操作失败:" + e.getMessage());}}
}

5.3 批量操作

IService 提供了多种批量操作方法:

@Test
public void testBatchOperations() {// 批量查询List<Long> ids = Arrays.asList(1L, 2L, 3L);List<User> users = userService.listByIds(ids);// 批量更新users.forEach(user -> user.setAge(user.getAge() + 5));boolean updated = userService.updateBatchById(users);// 批量保存或更新List<User> mixedUsers = new ArrayList<>();for (int i = 0; i < 5; i++) {User user = new User();if (i < 2) {// 已存在的用户,将会更新user.setId((long) (i + 1));user.setAge(40);}user.setName("SaveOrUpdate" + i);user.setEmail("sou" + i + "@example.com");mixedUsers.add(user);}boolean result = userService.saveOrUpdateBatch(mixedUsers);System.out.println("批量保存或更新结果:" + result);
}

6. 代码生成器

MyBatis-Plus 代码生成器可以快速生成 Entity、Mapper、Service、Controller 等各个模块的代码。

6.1 引入依赖

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.3.1</version>
</dependency><!-- 模板引擎,选择一个 -->
<dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.3</version>
</dependency>

6.2 编写代码生成器

package com.example.generator;import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;import java.sql.Types;
import java.util.Collections;public class CodeGenerator {public static void main(String[] args) {FastAutoGenerator.create("jdbc:mysql://localhost:3306/your_database?serverTimezone=Asia/Shanghai", "root", "password").globalConfig(builder -> {builder.author("Your Name") // 设置作者.enableSwagger() // 开启 swagger 模式.fileOverride() // 覆盖已生成文件.outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录}).dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {int typeCode = metaInfo.getJdbcType().TYPE_CODE;if (typeCode == Types.SMALLINT) {// 将数据库中SMALLINT转换为Integerreturn DbColumnType.INTEGER;}return typeRegistry.getColumnType(metaInfo);})).packageConfig(builder -> {builder.parent("com.example") // 设置父包名.moduleName("system") // 设置父包模块名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); // 设置mapperXml生成路径}).strategyConfig(builder -> {builder.addInclude("user", "role") // 设置需要生成的表名.addTablePrefix("t_", "sys_") // 设置过滤表前缀// Entity 策略配置.entityBuilder().enableLombok() // 开启 Lombok.logicDeleteColumnName("deleted") // 逻辑删除字段.enableTableFieldAnnotation() // 开启生成实体时生成字段注解// Mapper 策略配置.mapperBuilder().enableMapperAnnotation() // 开启 @Mapper 注解.formatMapperFileName("%sMapper") // 格式化 mapper 文件名称.formatXmlFileName("%sMapper") // 格式化 xml 文件名称// Service 策略配置.serviceBuilder().formatServiceFileName("%sService") // 格式化 service 接口文件名称.formatServiceImplFileName("%sServiceImpl") // 格式化 service 实现类文件名称// Controller 策略配置.controllerBuilder().enableRestStyle() // 开启生成 @RestController 控制器.formatFileName("%sController"); // 格式化文件名称}).templateEngine(new VelocityTemplateEngine()) // 使用Velocity引擎模板.execute();}
}

6.3 运行代码生成器

直接运行 main 方法,生成代码。生成后你会得到:

  • Entity 类
  • Mapper 接口和 XML
  • Service 接口和实现类
  • Controller 类

7. 动态表名

在某些场景下,我们需要根据不同条件操作不同的表,如分表、租户表等。MyBatis-Plus 提供了动态表名功能。

7.1 配置动态表名插件

@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加动态表名插件DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {// 根据业务动态替换表名,例如表名后缀加上年月if ("user".equals(tableName)) {return tableName + "_" + DateUtil.format(new Date(), "yyyyMM");}return tableName;});interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);// 添加其他插件...return interceptor;}
}

7.2 测试动态表名

@Test
public void testDynamicTableName() {// 假设当前是2023年8月,表名会自动替换为 user_202308List<User> users = userMapper.selectList(null);users.forEach(System.out::println);
}

8. JSON 字段处理

在实际开发中,我们常常需要将 JSON 字段映射为 Java 对象。MyBatis-Plus 提供了自定义类型处理器功能。

8.1 定义类型处理器

package com.example.handler;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;/*** JSON 字段类型处理器* @param <T> 目标类型*/
@MappedTypes({Object.class})
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.LONGVARCHAR})
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {private static final ObjectMapper MAPPER = new ObjectMapper();private final Class<T> clazz;public JsonTypeHandler(Class<T> clazz) {if (clazz == null) {throw new IllegalArgumentException("Type argument cannot be null");}this.clazz = clazz;}@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {try {ps.setString(i, MAPPER.writeValueAsString(parameter));} catch (JsonProcessingException e) {throw new SQLException("Error converting JSON to String", e);}}@Overridepublic T getNullableResult(ResultSet rs, String columnName) throws SQLException {return parseJSON(rs.getString(columnName));}@Overridepublic T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return parseJSON(rs.getString(columnIndex));}@Overridepublic T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return parseJSON(cs.getString(columnIndex));}private T parseJSON(String json) {if (json == null) {return null;}try {return MAPPER.readValue(json, clazz);} catch (JsonProcessingException e) {throw new RuntimeException("Error parsing JSON", e);}}
}

8.2 在实体类中使用 JSON 处理器

@Data
@TableName("product")
public class Product {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private BigDecimal price;// 使用自定义类型处理器处理 JSON 字段@TableField(typeHandler = JsonTypeHandler.class)private Map<String, Object> attributes;// 或者映射为具体的类@TableField(typeHandler = JsonTypeHandler.class)private ProductSpec specification;
}@Data
public class ProductSpec {private String color;private String size;private Integer weight;private List<String> tags;
}

8.3 注册类型处理器

在 MyBatis 配置中注册类型处理器:

mybatis-plus:configuration:default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandlertype-handlers-package: com.example.handler

9. 高级查询

MyBatis-Plus 支持丰富的高级查询功能。

9.1 聚合查询

@Test
public void testAggregation() {// 查询用户的平均年龄LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.select(User::getAge);List<Object> ages = userMapper.selectObjs(wrapper);double avgAge = ages.stream().mapToInt(obj -> (Integer) obj).average().orElse(0);System.out.println("平均年龄:" + avgAge);// 分组统计各年龄段人数QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select("age, count(*) as count").groupBy("age");List<Map<String, Object>> result = userMapper.selectMaps(queryWrapper);result.forEach(System.out::println);
}

9.2 子查询

@Test
public void testSubQuery() {// 查询年龄大于平均年龄的用户QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.apply("age > (select avg(age) from user)");List<User> users = userMapper.selectList(queryWrapper);users.forEach(System.out::println);// 或者使用子查询queryWrapper = new QueryWrapper<>();queryWrapper.inSql("id", "select id from user where age > 25");users = userMapper.selectList(queryWrapper);users.forEach(System.out::println);
}

9.3 复杂条件查询

@Test
public void testComplexQuery() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();// 构造复杂查询条件wrapper.nested(w -> w.like(User::getName, "张").or().like(User::getName, "李")).and(w -> w.ge(User::getAge, 20).le(User::getAge, 30)).orderByDesc(User::getCreateTime);List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}

9.4 动态条件查询

@Test
public void testDynamicCondition() {// 模拟前端传入的查询参数String name = "张";Integer minAge = 20;Integer maxAge = null;String email = "example.com";// 构建动态条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();// 只有当参数不为空时才会加入条件wrapper.like(StringUtils.isNotBlank(name), User::getName, name).ge(minAge != null, User::getAge, minAge).le(maxAge != null, User::getAge, maxAge).like(StringUtils.isNotBlank(email), User::getEmail, email);List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}

10. 性能优化

使用 MyBatis-Plus 时,我们需要注意一些性能优化点。

10.1 减少不必要的查询字段

@Test
public void testSelectFields() {// 只查询需要的字段,减少数据传输量LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.select(User::getId, User::getName, User::getAge);List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}

10.2 批量操作优化

@Test
public void testBatchInsert() {// 批量插入优化List<User> users = new ArrayList<>();for (int i = 0; i < 1000; i++) {User user = new User();user.setName("批量用户" + i);user.setAge(20 + (i % 10));user.setEmail("batch" + i + "@example.com");users.add(user);}// 默认每次提交100条数据userService.saveBatch(users, 100);
}

10.3 避免全表更新和删除

MyBatis-Plus 默认会阻止全表更新和删除操作,但在某些特殊场景下,我们可能需要进行有意义的全表操作。

@Test
public void testSafeUpdate() {try {// 下面这行代码会被阻止执行userMapper.update(new User(), null);} catch (Exception e) {System.out.println("全表更新被阻止:" + e.getMessage());}// 如果确实需要全表更新,可以这样做User updateEntity = new User();updateEntity.setDeleted(0);UpdateWrapper<User> wrapper = new UpdateWrapper<>();wrapper.like("name", "test");userMapper.update(updateEntity, wrapper);
}

10.4 使用索引

确保在常用查询字段上创建适当的索引:

-- 为常用查询字段添加索引
ALTER TABLE `user` ADD INDEX `idx_name` (`name`);
ALTER TABLE `user` ADD INDEX `idx_age` (`age`);
ALTER TABLE `user` ADD INDEX `idx_email` (`email`);

11. 常见问题与解决方案

在使用 MyBatis-Plus 的过程中,可能会遇到各种问题。这里列出一些常见问题及其解决方案。

11.1 ID 生成问题

问题:插入数据时 ID 没有自动生成。

解决方案

  1. 检查实体类中的 ID 字段是否正确配置了 @TableId 注解。
  2. 确保指定了正确的 ID 生成策略。
// 自增策略(依赖数据库的自增功能)
@TableId(value = "id", type = IdType.AUTO)
private Long id;// 雪花算法(适用于分布式系统)
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;// UUID 策略
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;

11.2 字段名映射问题

问题:数据库字段名与实体类属性名不一致,导致查询结果映射错误。

解决方案

  1. 启用驼峰命名转换:
mybatis-plus:configuration:map-underscore-to-camel-case: true
  1. 使用 @TableField 注解显式指定映射关系:
@TableField("user_name")
private String userName;
  1. 对于不需要映射的字段,可以使用 @TableField(exist = false) 排除。

11.3 分页问题

问题:分页查询不起作用或结果不正确。

解决方案

  1. 确保正确配置了分页插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;
}
  1. 检查分页参数是否正确:
// 第一页,每页10条
Page<User> page = new Page<>(1, 10);
  1. 对于复杂的分页查询,可能需要自定义SQL:
public interface UserMapper extends BaseMapper<User> {@Select("SELECT u.*, r.name as role_name FROM user u LEFT JOIN role r ON u.id = r.id ${ew.customSqlSegment}")IPage<User> selectUserWithRolePage(Page<UserVO> page, @Param("ew") Wrapper<User> wrapper);
}

11.4 逻辑删除问题

问题:逻辑删除配置后,查询结果异常。

解决方案

  1. 确保正确配置了逻辑删除字段:
mybatis-plus:global-config:db-config:logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0
  1. 实体类中添加对应注解:
@TableLogic
private Integer deleted;
  1. 如果需要查询被逻辑删除的数据,需要使用自定义SQL:
@Select("SELECT * FROM user WHERE id = #{id}")
User selectByIdIncludeLogicDeleted(Long id);

11.5 复杂查询问题

问题:需要进行复杂连接查询,如多表关联查询。

解决方案

  1. 使用自定义SQL:
@Select("SELECT u.*, d.name as dept_name FROM user u LEFT JOIN department d ON u.id = d.id WHERE u.id = #{id}")
UserVO getUserWithDept(Long id);
  1. 使用 XML 映射文件:
<select id="getUserWithRoles" resultMap="userWithRolesMap">SELECT u.*, r.id as role_id, r.name as role_nameFROM user uLEFT JOIN user_role ur ON u.id = ur.user_idLEFT JOIN role r ON ur.role_id = r.idWHERE u.id = #{userId}AND u.deleted = 0AND r.deleted = 0
</select>
  1. 使用多次查询组合结果:
// 在 Service 层先查询用户,再查询关联数据
public UserVO getUserWithRoles(Long userId) {User user = this.getById(userId);if (user == null) {return null;}List<Role> roles = roleMapper.selectRolesByUserId(userId);UserVO userVO = new UserVO();BeanUtils.copyProperties(user, userVO);userVO.setRoles(roles);return userVO;
}

11.6 XML 映射文件无法加载

问题:自定义的 XML 映射文件没有被识别。

解决方案

  1. 确保 XML 文件放在正确的目录下,并在配置中指定:
mybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml
  1. 检查 XML 文件的命名空间是否与 Mapper 接口匹配:
<mapper namespace="com.example.mapper.UserMapper"><!-- 映射内容 -->
</mapper>
  1. 在使用 Maven 或 Gradle 时,确保 XML 文件会被打包到 classpath 中:
<!-- Maven -->
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource><resource><directory>src/main/resources</directory></resource></resources>
</build>

12. 实战案例:构建完整的用户管理系统

下面我们通过一个实际案例,来展示如何使用 MyBatis-Plus 构建一个完整的用户管理系统。

12.1 数据库设计

-- 用户表
CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '密码',`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`gender` tinyint(1) DEFAULT NULL COMMENT '性别:0-女,1-男',`avatar` varchar(255) DEFAULT NULL COMMENT '头像',`dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',`status` tinyint(1) DEFAULT '0' COMMENT '状态:0-正常,1-禁用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除:0-未删除,1-已删除',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`),INDEX `idx_dept_id` (`dept_id`),INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 部门表
CREATE TABLE `sys_dept` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '部门ID',`name` varchar(50) NOT NULL COMMENT '部门名称',`parent_id` bigint(20) DEFAULT '0' COMMENT '父部门ID',`ancestors` varchar(100) DEFAULT '' COMMENT '祖级列表',`order_num` int(11) DEFAULT '0' COMMENT '显示顺序',`leader` varchar(50) DEFAULT NULL COMMENT '负责人',`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`status` tinyint(1) DEFAULT '0' COMMENT '状态:0-正常,1-禁用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除:0-未删除,1-已删除',PRIMARY KEY (`id`),INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';-- 角色表
CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`name` varchar(50) NOT NULL COMMENT '角色名称',`code` varchar(50) NOT NULL COMMENT '角色编码',`description` varchar(255) DEFAULT NULL COMMENT '角色描述',`sort` int(11) DEFAULT '0' COMMENT '排序',`status` tinyint(1) DEFAULT '0' COMMENT '状态:0-正常,1-禁用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除:0-未删除,1-已删除',PRIMARY KEY (`id`),UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';-- 用户角色关联表
CREATE TABLE `sys_user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',`user_id` bigint(20) NOT NULL COMMENT '用户ID',`role_id` bigint(20) NOT NULL COMMENT '角色ID',PRIMARY KEY (`id`),UNIQUE KEY `uk_user_role` (`user_id`,`role_id`),INDEX `idx_user_id` (`user_id`),INDEX `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

12.2 实体类设计

用户实体
@Data
@TableName("sys_user")
public class User extends BaseEntity {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.ASSIGN_ID)private Long id;@NotBlank(message = "用户名不能为空")private String username;@JsonIgnore  // 密码不参与JSON序列化private String password;private String nickName;@Email(message = "邮箱格式不正确")private String email;private String phone;private Integer gender;private String avatar;private Long deptId;private Integer status;@TableField(exist = false)  // 非数据库字段private List<Role> roles;@TableField(exist = false)private Dept dept;
}
部门实体
@Data
@TableName("sys_dept")
public class Dept extends BaseEntity {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.ASSIGN_ID)private Long id;@NotBlank(message = "部门名称不能为空")private String name;private Long parentId;private String ancestors;private Integer orderNum;private String leader;private String phone;private String email;private Integer status;@TableField(exist = false)private List<Dept> children;
}
角色实体
@Data
@TableName("sys_role")
public class Role extends BaseEntity {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.ASSIGN_ID)private Long id;@NotBlank(message = "角色名称不能为空")private String name;@NotBlank(message = "角色编码不能为空")private String code;private String description;private Integer sort;private Integer status;
}
用户角色关联实体
@Data
@TableName("sys_user_role")
public class UserRole {@TableId(value = "id", type = IdType.ASSIGN_ID)private Long id;private Long userId;private Long roleId;
}
基础实体
@Data
public class BaseEntity implements Serializable {private static final long serialVersionUID = 1L;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;@TableLogicprivate Integer deleted;
}

12.3 Mapper 接口

用户 Mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {@Select("SELECT u.*, d.name as dept_name FROM sys_user u " +"LEFT JOIN sys_dept d ON u.dept_id = d.id " +"WHERE u.id = #{userId}")User getUserWithDept(Long userId);/*** 获取用户及其角色信息*/List<User> getUserWithRoles(@Param("userId") Long userId);
}
用户 Mapper 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.mapper.UserMapper"><resultMap id="userWithRolesMap" type="com.example.entity.User"><id property="id" column="id"/><result property="username" column="username"/><result property="nickName" column="nick_name"/><result property="email" column="email"/><result property="phone" column="phone"/><result property="gender" column="gender"/><result property="avatar" column="avatar"/><result property="deptId" column="dept_id"/><result property="status" column="status"/><result property="createTime" column="create_time"/><result property="updateTime" column="update_time"/><collection property="roles" ofType="com.example.entity.Role"><id property="id" column="role_id"/><result property="name" column="role_name"/><result property="code" column="role_code"/></collection></resultMap><select id="getUserWithRoles" resultMap="userWithRolesMap">SELECT u.*, r.id as role_id, r.name as role_name, r.code as role_codeFROM sys_user uLEFT JOIN sys_user_role ur ON u.id = ur.user_idLEFT JOIN sys_role r ON ur.role_id = r.idWHERE u.id = #{userId}AND u.deleted = 0AND r.deleted = 0</select></mapper>

12.4 Service 层

用户 Service 接口
public interface UserService extends IService<User> {/*** 获取用户详情,包括部门和角色信息*/User getUserDetail(Long userId);/*** 创建用户,同时关联角色*/boolean createUser(User user, List<Long> roleIds);/*** 更新用户,同时更新角色关联*/boolean updateUser(User user, List<Long> roleIds);/*** 根据部门ID获取用户列表*/List<User> getUsersByDeptId(Long deptId);/*** 根据角色ID获取用户列表*/List<User> getUsersByRoleId(Long roleId);/*** 重置用户密码*/boolean resetPassword(Long userId, String newPassword);/*** 修改用户状态*/boolean changeStatus(Long userId, Integer status);
}
用户 Service 实现
@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate UserRoleService userRoleService;@Autowiredprivate DeptService deptService;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic User getUserDetail(Long userId) {User user = this.baseMapper.getUserWithRoles(userId);if (user != null && user.getDeptId() != null) {user.setDept(deptService.getById(user.getDeptId()));}return user;}@Overridepublic boolean createUser(User user, List<Long> roleIds) {// 密码加密user.setPassword(passwordEncoder.encode(user.getPassword()));// 保存用户boolean result = this.save(user);// 保存用户角色关联if (result && CollectionUtil.isNotEmpty(roleIds)) {userRoleService.saveUserRoles(user.getId(), roleIds);}return result;}@Overridepublic boolean updateUser(User user, List<Long> roleIds) {// 更新用户基本信息boolean result = this.updateById(user);// 更新用户角色关联if (result && roleIds != null) {// 先删除原来的关联userRoleService.remove(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, user.getId()));// 重新建立关联if (CollectionUtil.isNotEmpty(roleIds)) {userRoleService.saveUserRoles(user.getId(), roleIds);}}return result;}@Overridepublic List<User> getUsersByDeptId(Long deptId) {if (deptId == null) {return Collections.emptyList();}return this.list(new LambdaQueryWrapper<User>().eq(User::getDeptId, deptId));}@Overridepublic List<User> getUsersByRoleId(Long roleId) {if (roleId == null) {return Collections.emptyList();}// 查询拥有该角色的用户IDList<UserRole> userRoles = userRoleService.list(new LambdaQueryWrapper<UserRole>().eq(UserRole::getRoleId, roleId));if (CollectionUtil.isEmpty(userRoles)) {return Collections.emptyList();}// 获取用户ID集合List<Long> userIds = userRoles.stream().map(UserRole::getUserId).collect(Collectors.toList());// 查询用户信息return this.listByIds(userIds);}@Overridepublic boolean resetPassword(Long userId, String newPassword) {User user = new User();user.setId(userId);user.setPassword(passwordEncoder.encode(newPassword));return this.updateById(user);}@Overridepublic boolean changeStatus(Long userId, Integer status) {User user = new User();user.setId(userId);user.setStatus(status);return this.updateById(user);}
}

12.5 Controller 层

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {private final UserService userService;/*** 获取用户列表*/@GetMappingpublic Result<Page<User>> list(UserQuery query, Page<User> page) {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();// 构建查询条件wrapper.like(StringUtils.isNotBlank(query.getUsername()), User::getUsername, query.getUsername()).like(StringUtils.isNotBlank(query.getNickName()), User::getNickName, query.getNickName()).eq(query.getStatus() != null, User::getStatus, query.getStatus()).eq(query.getDeptId() != null, User::getDeptId, query.getDeptId()).between(query.getBeginTime() != null && query.getEndTime() != null,User::getCreateTime,query.getBeginTime(),query.getEndTime()).orderByDesc(User::getCreateTime);Page<User> userPage = userService.page(page, wrapper);return Result.success(userPage);}/*** 获取用户详情*/@GetMapping("/{id}")public Result<User> detail(@PathVariable Long id) {User user = userService.getUserDetail(id);return Result.success(user);}/*** 创建用户*/@PostMapping@PreAuthorize("hasAuthority('sys:user:add')")public Result<?> create(@RequestBody @Validated UserDTO userDTO) {User user = BeanUtil.copyProperties(userDTO, User.class);boolean result = userService.createUser(user, userDTO.getRoleIds());return result ? Result.success() : Result.failure("创建用户失败");}/*** 更新用户*/@PutMapping("/{id}")@PreAuthorize("hasAuthority('sys:user:edit')")public Result<?> update(@PathVariable Long id, @RequestBody @Validated UserDTO userDTO) {User user = BeanUtil.copyProperties(userDTO, User.class);user.setId(id);boolean result = userService.updateUser(user, userDTO.getRoleIds());return result ? Result.success() : Result.failure("更新用户失败");}/*** 删除用户*/@DeleteMapping("/{id}")@PreAuthorize("hasAuthority('sys:user:delete')")public Result<?> delete(@PathVariable Long id) {if (id.equals(SecurityUtils.getCurrentUserId())) {return Result.failure("不能删除当前登录用户");}boolean result = userService.removeById(id);return result ? Result.success() : Result.failure("删除用户失败");}/*** 修改用户状态*/@PatchMapping("/{id}/status")@PreAuthorize("hasAuthority('sys:user:edit')")public Result<?> changeStatus(@PathVariable Long id, @RequestParam Integer status) {boolean result = userService.changeStatus(id, status);return result ? Result.success() : Result.failure("修改用户状态失败");}/*** 获取当前登录用户信息*/@GetMapping("/me")public Result<User> me() {Long userId = SecurityUtils.getCurrentUserId();User user = userService.getUserDetail(userId);return Result.success(user);}
}

12.6 DTO 和查询对象

@Data
public class UserDTO {@NotBlank(message = "用户名不能为空")private String username;private String password;private String nickName;@Email(message = "邮箱格式不正确")private String email;private String phone;private Integer gender;private String avatar;private Long deptId;private Integer status;private List<Long> roleIds;
}@Data
public class UserQuery extends BaseQuery {private String username;private String nickName;private Integer status;private Long deptId;
}@Data
public class BaseQuery {private LocalDateTime beginTime;private LocalDateTime endTime;
}

12.7 统一返回结果

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {private Integer code;private String message;private T data;public static <T> Result<T> success() {return new Result<>(200, "操作成功", null);}public static <T> Result<T> success(T data) {return new Result<>(200, "操作成功", data);}public static <T> Result<T> success(String message, T data) {return new Result<>(200, message, data);}public static <T> Result<T> failure(String message) {return new Result<>(500, message, null);}public static <T> Result<T> failure(Integer code, String message) {return new Result<>(code, message, null);}
}

12.8 配置文件

server:port: 8080spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/user_management?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: passwordjackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8# MyBatis-Plus 配置
mybatis-plus:configuration:# 开启下划线转驼峰map-underscore-to-camel-case: true# 开启SQL语句打印(开发环境使用)log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 全局配置global-config:db-config:# 主键类型id-type: assign_id# 表前缀table-prefix: sys_# 逻辑删除配置logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0# XML 映射文件位置mapper-locations: classpath*:/mapper/**/*.xml

13. 总结与最佳实践

13.1 MyBatis-Plus 核心优势

  1. 简化开发:通过继承 BaseMapper 和 IService,减少大量重复的 CRUD 代码编写。
  2. 强大的条件构造器:提供了丰富的条件构造方式,支持 Lambda 表达式,类型安全。
  3. 丰富的功能:内置分页、逻辑删除、自动填充、乐观锁等功能,无需手动实现。
  4. 性能优化:提供了性能分析插件,帮助开发者优化 SQL。
  5. 代码生成:快速生成基础代码,提高开发效率。

13.2 最佳实践

13.2.1 Entity 设计
  1. 使用 Lombok:减少冗余代码,使用 @Data@Builder 等注解。
  2. 规范字段命名:实体类使用驼峰命名,数据库使用下划线命名。
  3. 基础字段抽象:将通用字段抽取到基类中,如 idcreateTimeupdateTime 等。
  4. 使用注解标注特殊字段:如 @TableId@TableLogic@Version 等。
13.2.2 Mapper 设计
  1. 继承 BaseMapper:优先使用 MyBatis-Plus 提供的通用方法。
  2. 自定义方法谨慎使用:仅在复杂场景下添加自定义方法,避免冗余。
  3. XML 与注解结合:简单查询使用注解,复杂查询使用 XML。
13.2.3 Service 设计
  1. 继承 ServiceImpl:获取丰富的批量操作和链式操作能力。
  2. 业务逻辑集中:复杂的业务逻辑应当集中在 Service 层,保持 Controller 的简洁。
  3. 事务管理:在 Service 层管理事务,确保数据一致性。
13.2.4 性能优化
  1. 避免全表扫描:合理使用索引,避免不必要的全表扫描。
  2. 减少不必要的字段查询:使用 select() 方法指定需要查询的字段。
  3. 批量操作:使用批量插入、更新和删除,减少数据库交互次数。
  4. 合理使用分页:避免一次性查询大量数据,影响性能。
13.2.5 安全性
  1. 防止SQL注入:使用参数绑定而非字符串拼接 SQL。
  2. 数据校验:在 DTO 层使用 JSR303 注解进行数据校验。
  3. 权限控制:实施细粒度的权限控制,保障数据安全。

13.3 进阶学习方向

  1. 深入理解 MyBatis 原理:MyBatis-Plus 建立在 MyBatis 之上,了解 MyBatis 的工作原理有助于更好地使用 MyBatis-Plus。
  2. 动态数据源:学习如何在同一应用中连接多个数据库。
  3. 分库分表:了解如何使用 MyBatis-Plus 配合 Sharding-JDBC 等中间件实现分库分表。
  4. SQL性能优化:学习如何分析和优化 SQL 性能。
  5. 缓存策略:学习如何合理使用缓存提高查询性能。

13.4 总结

MyBatis-Plus 作为一款优秀的 ORM 框架,极大地简化了开发过程,提高了开发效率。通过本教程,你已经掌握了 MyBatis-Plus 的核心功能和使用方法,能够在实际项目中熟练应用。

在实际开发中,建议先从基础功能开始,逐步探索高级特性,根据业务需求选择合适的功能。同时,关注官方文档的更新,持续学习新的特性和优化方法。

MyBatis-Plus 的口号是"为简化开发而生",希望它能为你的开发工作带来便利和效率!


附录:常用注解说明

@TableName

指定实体类对应的数据库表名。

@TableName("sys_user")
public class User {// ...
}

@TableId

指定主键字段及生成策略。

@TableId(value = "id", type = IdType.AUTO)
private Long id;

@TableField

指定字段名和其他属性。

@TableField("user_name")
private String userName;

@TableLogic

标识逻辑删除字段。

@TableLogic
private Integer deleted;

@Version

标识乐观锁字段。

@Version
private Integer version;

@EnumValue

标识枚举字段的存储值。

public enum UserStatus {NORMAL(0, "正常"),DISABLED(1, "禁用");@EnumValueprivate final int value;private final String desc;// ...
}

@KeySequence

序列主键策略(Oracle、PostgreSQL等)。

@KeySequence(value = "seq_user", clazz = Long.class)
public class User {// ...
}

@OrderBy

指定默认排序字段。

@OrderBy(asc = false)
private LocalDateTime createTime;logic-not-delete-value: 0# XML 映射文件位置mapper-locations: classpath*:/mapper/**/*.xml

13. 总结与最佳实践

13.1 MyBatis-Plus 核心优势

  1. 简化开发:通过继承 BaseMapper 和 IService,减少大量重复的 CRUD 代码编写。
  2. 强大的条件构造器:提供了丰富的条件构造方式,支持 Lambda 表达式,类型安全。
  3. 丰富的功能:内置分页、逻辑删除、自动填充、乐观锁等功能,无需手动实现。
  4. 性能优化:提供了性能分析插件,帮助开发者优化 SQL。
  5. 代码生成:快速生成基础代码,提高开发效率。

13.2 最佳实践

13.2.1 Entity 设计
  1. 使用 Lombok:减少冗余代码,使用 @Data@Builder 等注解。
  2. 规范字段命名:实体类使用驼峰命名,数据库使用下划线命名。
  3. 基础字段抽象:将通用字段抽取到基类中,如 idcreateTimeupdateTime 等。
  4. 使用注解标注特殊字段:如 @TableId@TableLogic@Version 等。
13.2.2 Mapper 设计
  1. 继承 BaseMapper:优先使用 MyBatis-Plus 提供的通用方法。
  2. 自定义方法谨慎使用:仅在复杂场景下添加自定义方法,避免冗余。
  3. XML 与注解结合:简单查询使用注解,复杂查询使用 XML。
13.2.3 Service 设计
  1. 继承 ServiceImpl:获取丰富的批量操作和链式操作能力。
  2. 业务逻辑集中:复杂的业务逻辑应当集中在 Service 层,保持 Controller 的简洁。
  3. 事务管理:在 Service 层管理事务,确保数据一致性。
13.2.4 性能优化
  1. 避免全表扫描:合理使用索引,避免不必要的全表扫描。
  2. 减少不必要的字段查询:使用 select() 方法指定需要查询的字段。
  3. 批量操作:使用批量插入、更新和删除,减少数据库交互次数。
  4. 合理使用分页:避免一次性查询大量数据,影响性能。
13.2.5 安全性
  1. 防止SQL注入:使用参数绑定而非字符串拼接 SQL。
  2. 数据校验:在 DTO 层使用 JSR303 注解进行数据校验。
  3. 权限控制:实施细粒度的权限控制,保障数据安全。

13.3 进阶学习方向

  1. 深入理解 MyBatis 原理:MyBatis-Plus 建立在 MyBatis 之上,了解 MyBatis 的工作原理有助于更好地使用 MyBatis-Plus。
  2. 动态数据源:学习如何在同一应用中连接多个数据库。
  3. 分库分表:了解如何使用 MyBatis-Plus 配合 Sharding-JDBC 等中间件实现分库分表。
  4. SQL性能优化:学习如何分析和优化 SQL 性能。
  5. 缓存策略:学习如何合理使用缓存提高查询性能。

13.4 总结

MyBatis-Plus 作为一款优秀的 ORM 框架,极大地简化了开发过程,提高了开发效率。通过本教程,你已经掌握了 MyBatis-Plus 的核心功能和使用方法,能够在实际项目中熟练应用。

在实际开发中,建议先从基础功能开始,逐步探索高级特性,根据业务需求选择合适的功能。同时,关注官方文档的更新,持续学习新的特性和优化方法。

MyBatis-Plus 的口号是"为简化开发而生",希望它能为你的开发工作带来便利和效率!


附录:常用注解说明

@TableName

指定实体类对应的数据库表名。

@TableName("sys_user")
public class User {// ...
}

@TableId

指定主键字段及生成策略。

@TableId(value = "id", type = IdType.AUTO)
private Long id;

@TableField

指定字段名和其他属性。

@TableField("user_name")
private String userName;

@TableLogic

标识逻辑删除字段。

@TableLogic
private Integer deleted;

@Version

标识乐观锁字段。

@Version
private Integer version;

@EnumValue

标识枚举字段的存储值。

public enum UserStatus {NORMAL(0, "正常"),DISABLED(1, "禁用");@EnumValueprivate final int value;private final String desc;// ...
}

@KeySequence

序列主键策略(Oracle、PostgreSQL等)。

@KeySequence(value = "seq_user", clazz = Long.class)
public class User {// ...
}

@OrderBy

指定默认排序字段。

@OrderBy(asc = false)
private LocalDateTime createTime;

相关文章:

MyBatis-Plus 详解教程

文章目录 1. MyBatis-Plus 简介1.1 什么是 MyBatis-Plus&#xff1f;1.2 为什么要使用 MyBatis-Plus&#xff1f;传统 MyBatis 的痛点MyBatis-Plus 的优势 1.3 MyBatis-Plus 与 MyBatis 的关系 2. 快速开始2.1 环境要求2.2 依赖引入MavenGradle 2.3 数据库准备2.4 配置 Spring …...

Java设计模式之观察者模式:从入门到架构级实践

一、观察者模式的核心价值 观察者模式&#xff08;Observer Pattern&#xff09;是行为型设计模式中的经典之作&#xff0c;它建立了对象间的一对多依赖关系&#xff0c;让多个观察者对象能够自动感知被观察对象的状态变化。这种模式在事件驱动系统、实时数据推送、GUI事件处理…...

【双指针】专题:LeetCode 202题解——快乐数

快乐数 一、题目链接二、题目三、题目解析四、算法原理扩展 五、编写代码 一、题目链接 快乐数 二、题目 三、题目解析 快乐数的定义中第二点最重要&#xff0c;只有两种情况&#xff0c;分别拿示例1、示例2分析吧&#xff1a; 示例1中一旦出现1了&#xff0c;继续重复过程就…...

深度学习占用大量内存空间解决办法

应该是缓存的问题&#xff0c;关机重启内存多了10G&#xff0c;暂时没找到别的方法 重启前 关机重启后...

[LeetCode 1871] 跳跃游戏 7(Ⅶ)

题面&#xff1a; 数据范围&#xff1a; 2 ≤ s . l e n g t h ≤ 1 0 5 2 \le s.length \le 10^5 2≤s.length≤105 s [ i ] s[i] s[i] 要么是 ′ 0 ′ 0 ′0′ &#xff0c;要么是 ′ 1 ′ 1 ′1′ s [ 0 ] 0 s[0] 0 s[0]0 1 ≤ m i n J u m p ≤ m a x J u m p <…...

同济大学轻量化低成本具身导航!COSMO:基于选择性记忆组合的低开销视觉语言导航

作者&#xff1a;Siqi Zhang 1 ^{1} 1, Yanyuan Qiao 3 ^{3} 3, Qunbo Wang 2 ^{2} 2, Zike Yan 4 ^{4} 4, Qi Wu 3 ^{3} 3, Zhihua Wei 1 ^{1} 1, Jing Liu 1 ^{1} 1单位&#xff1a; 1 ^{1} 1同济大学计算机科学与技术学院&#xff0c; 2 ^{2} 2中科院自动化研究所&#xff0…...

【Ubuntu | 网络】Vmware虚拟机里的Ubuntu开机后没有网络接口、也没有网络图标

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f60e;金句分享&#x1f60e;&a…...

第二十一讲 XGBoost 回归建模 + SHAP 可解释性分析(利用R语言内置数据集)

下面我将使用 R 语言内置的 mtcars 数据集&#xff0c;模拟一个完整的 XGBoost 回归建模 SHAP 可解释性分析 实战流程。我们将以预测汽车的油耗&#xff08;mpg&#xff09;为目标变量&#xff0c;构建 XGBoost 模型&#xff0c;并用 SHAP 来解释模型输出。 &#x1f697; 示例…...

HP惠普打印机:解决每次打印后额外产生@PJL SET USERNAME=文档的情况

情况描述 惠普商用打印机型号&#xff1a;Color LaserJet Managed MFP E78223 在每次打印文档后都会出现包含我个人电脑用户名的额外文档&#xff1a; 这不是我希望的&#xff0c;因此我联系了惠普官方客服&#xff0c;并得到了解决 解决方案 原因 具客服所说&#xff0c;这些是…...

MariaDB MaxScale 的用途与实现细节

MaxScale 主要用途 MariaDB MaxScale 是一个智能数据库代理&#xff08;proxy&#xff09;&#xff0c;主要用于增强 MySQL/MariaDB 数据库的高可用性、可扩展性和安全性&#xff0c;同时简化应用程序与数据库基础设施之间的交互。它的核心功能包括&#xff1a; 负载均衡&…...

CTF--eval

一、原网页&#xff1a; 二、步骤&#xff1a; 1.代码分析&#xff1a; <?phpinclude "flag.php"; // 引入一个文件&#xff0c;该文件可能定义了一些变量&#xff08;例如 $flag&#xff09;$a $_REQUEST[hello]; // 从用户请求中获取参数 hello 的值&#x…...

Android学习总结之算法篇七(图和矩阵)

有向图的深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08;BFS&#xff09;的示例&#xff0c;以此来模拟遍历 GC Root 引用链这种有向图结构&#xff1a; 一、深度优先搜索&#xff08;DFS&#xff09; import java.util.*;public class GraphDFS {privat…...

vmcore分析锁问题实例(x86-64)

问题描述&#xff1a;系统出现panic&#xff0c;dmesg有如下打印&#xff1a; [122061.197311] task:irq/181-ice-enp state:D stack:0 pid:3134 ppid:2 flags:0x00004000 [122061.197315] Call Trace: [122061.197317] <TASK> [122061.197318] __schedule0…...

【vue3】vue3+express实现图片/pdf等资源文件的下载

文件资源的下载&#xff0c;是我们业务开发中常见的需求。作为前端开发&#xff0c;学习下如何自己使用node的express框架来实现资源的下载操作。 实现效果 代码实现 前端 1.封装的请求后端下载接口的方法,需求配置aixos的请求参数里面的返回数据类型为blob // 下载 export…...

【BUG】Redis RDB快照持久化及写操作禁止问题排查与解决

1 问题描述 在使用Redis 的过程中&#xff0c;遇到如下报错&#xff0c;错误信息是 “MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk...”&#xff0c;记录下问题排查过程。 2 问题排查与解决 该错误提示表明&#…...

【HD-RK3576-PI】定制用户升级固件

硬件&#xff1a;HD-RK3576-PI 软件&#xff1a;Linux6.1Ubuntu22.04 在进行 Rockchip 相关开发时&#xff0c;制作自定义的烧写固件是一项常见且重要的操作。这里主要介绍文件系统的修改以及打包成完整update包升级的过程。 一、修改文件系统镜像&#xff08;Ubuntu环境操作&…...

【AI学习】李宏毅老师讲AI Agent摘要

在b站听了李宏毅2025最新的AI Agent教程&#xff0c;简单易懂&#xff0c;而且紧跟发展&#xff0c;有大量最新的研究进展。 教程中引用了大量论文&#xff0c;为了方便将来阅读相关论文&#xff0c;进一步深入理解&#xff0c;做了截屏纪录。 同时也做一下分享。 根据经验调整…...

狂神SQL学习笔记十:修改和删除数据表字段

1、修改与删除表 alter 修改表的名称&#xff1a; 增加表的字段&#xff1a; 修改表的字段&#xff08;重命名&#xff0c;修改约束&#xff09;&#xff1a; 修改约束 重命名 删除表的字段 删除表...

OSPF综合实验

一、网络拓扑 二、实验要求 1&#xff0c;R5为ISP&#xff0c;其上只能配置IP地址;R4作为企业边界路由器&#xff1b; 2&#xff0c;整个0SPF环境IP基于172.16.0.8/16划分; 3&#xff0c;所有设备均可访问R5的环回; 4&#xff0c;减少LSA的更新量&#xff0c;加快收敛&#xf…...

2025 cs144 Lab Checkpoint 2 小白超详细版

文章目录 1 环形索引的实现1.1 wrap类wrapunwrap 2 实现tcp_receiver2.1 tcp_receiver的功能2.2 传输的报文格式TCPSenderMessageTCPReceiverMessage 2.3 如何实现函数receive&#xff08;&#xff09;send&#xff08;&#xff09; 1 环形索引的实现 范围是0~2^32-1 需要有SY…...

VMware虚拟机安装Ubuntu 22.04.2

一、我的虚拟机版本 二、浏览器搜索Ubuntu 三、下载Ubuntu桌面版 四、下这个 五、创建新的虚拟机 六、选择典型&#xff0c;然后下一步 七、选择稍后安装操作系统&#xff0c;然后下一步 八、选择Linux ,版本选择Ubuntu 64位 九、选择好安装位置 十、磁盘大小一般选20G就够用了…...

XSS漏洞及常见处理方案

文章背景&#xff1a; 在近期项目安全测试中&#xff0c;安全团队发现了一处潜在的 跨站脚本攻击&#xff08;XSS&#xff09;漏洞&#xff0c;该漏洞可能导致用户数据被篡改或会话劫持等安全风险。针对这一问题&#xff0c;项目组迅速响应&#xff0c;通过代码修复、输入过滤、…...

TCP标志位抓包

说明 TCP协议的Header信息&#xff0c;URG、ACK、PSH、RST、SYN、FIN这6个字段在14字节的位置&#xff0c;对应的是tcp[13]&#xff0c;因为字节数是从[0]开始数的&#xff0c;14字节对应的就是tcp[13]&#xff0c;因此在抓这几个标志位的数据包时就要明确范围在tcp[13] 示例1…...

C/C++条件判断

条件判断 if语句的三种形态 if(a<b){} 、 if(a<b){}else{} 、 if(a<b){}else if(a>b) else{} if语句的嵌套 嵌套的常见错误&#xff08;配对错误&#xff09;,与前面最近的&#xff0c;而且还没有配对的if匹配 错误避免方法&#xff1a;严格使用 { }、先写&am…...

单位门户网站被攻击后的安全防护策略

政府网站安全现状与挑战 近年来&#xff0c;随着数字化进程的加速&#xff0c;政府门户网站已成为政务公开和服务公众的重要窗口。然而&#xff0c;网络安全形势却日益严峻。国家互联网应急中心的数据显示&#xff0c;政府网站已成为黑客攻击的重点目标&#xff0c;被篡改和被…...

# 工具记录

工具记录 键盘操作可视化工具openark64系统工具dufs-webui文件共享zotero文献查看cff explorerNoFencesfreeplane开源思维导图...

C/C++运算

C语言字符串的比较 #include <string.h> int strcmp( const char *str1, const char *str2 );例如: int ret; ret strcmp(str1, str2);返回值&#xff1a; str1 < str2时&#xff0c; 返回值< 0&#xff08;有些编译器返回 -1&#xff09; str1 > str2时…...

CloudWeGo 技术沙龙·深圳站回顾:云原生 × AI 时代的微服务架构与技术实践

2025 年 3 月 22 日&#xff0c;CloudWeGo “云原生 AI 时代的微服务架构与技术实践”主题沙龙在深圳圆满落幕。作为云原生与 AI 微服务融合领域的深度技术聚会&#xff0c;本次活动吸引了来自企业、开发者社区的百余位参与者&#xff0c;共同探讨如何通过开源技术应对智能时代…...

STM32移植文件系统FATFS——片外SPI FLASH

一、电路连接 主控芯片选型为&#xff1a;STM32F407ZGT6&#xff0c;SPI FLASH选型为&#xff1a;W25Q256JV。 采用了两片32MB的片外SPI FLASH&#xff0c;电路如图所示。 SPI FLASH与主控芯片的连接方式如表所示。 STM32F407GT6W25Q256JVPB3SPI1_SCKPB4SPI1_MISOPB5SPI1_MOSI…...

华为HG8546M光猫宽带密码破解

首先进光猫管理界面 将password改成text就可以看到加密后的密码了 复制密码到下面代码里 import hashlibdef sha256(todo):return hashlib.sha256(str(todo).encode()).hexdigest()def md5(todo):return hashlib.md5(str(todo).encode()).hexdigest()def find_secret(secret,…...

驱动-兼容不同设备-container_of

驱动兼容不同类型设备 在 Linux 驱动开发中&#xff0c;container_of 宏常被用来实现一个驱动兼容多种不同设备的架构。这种设计模式在 Linux 内核中非常常见&#xff0c;特别 是在设备驱动模型中。linux内核的主要开发语言是C&#xff0c;但是现在内核的框架使用了非常多的面向…...

UE5 检测球形范围的所有Actor

和Untiiy不同&#xff0c;不需要复杂的调用 首选确保角色添加了Sphere Collision 然后直接把sphere拖入蓝图&#xff0c;调用GetOverlappingActors来获取碰撞范围内的所有Actor...

AI大模型学习十:‌Ubuntu 22.04.5 调整根目录大小,解决根目录磁盘不够问题

一、说明 由于默认安装时导致home和根目录大小一样&#xff0c;导致根目录不够&#xff0c;所以我们调整下 二、调整 # 确认/home和/是否为独立逻辑卷&#xff0c;并属于同一卷组&#xff08;VG&#xff09; rootnode1:~# lsblk NAME MAJ:MIN RM SIZE…...

在ros2上使用opencv显示一张图片

1.先将图片放到桌面上 2.打开终端ctrlaltT&#xff0c;查看自己是否已安装opencv 3.创建工作环境 4.进入工作目录并创建ROS2包添加OpenCV依赖项 5.进入/home/kong/opencv_ws/opencv_use/src目录创建.cpp文件并编辑 6.代码如下 my_opencv.cpp #include <cstdio> #include…...

训练神经网络的原理(前向传播、反向传播、优化、迭代)

训练神经网络的原理 通过前向传播计算预测值和损失&#xff0c;利用反向传播计算梯度&#xff0c;然后通过优化算法更新参数&#xff0c;最终使模型在给定任务上表现更好。 核心&#xff1a;通过计算损失函数&#xff08;通常是模型预测与真实值之间的差距&#xff09;对模型参…...

每日一题(小白)暴力娱乐篇30

顺时针旋转&#xff0c;从上图中不难看出行列进行了变换。因为这是一道暴力可以解决的问题&#xff0c;我们直接尝试使用行列转换看能不能得到想要的结果。 public static void main(String[] args) {Scanner scan new Scanner(System.in);int nscan.nextInt();int mscan.next…...

【HTTPS】免费SSL证书配置Let‘s Encrypt自动续期

【HTTPS】免费SSL证书配置Lets Encrypt自动续期 1. 安装Certbot1.1 snapd1.2 certbot2. 申请泛域名证书使用 DNS 验证申请泛域名证书3.配置nginx申请的 SSL 证书文件所在目录nginx配置证书示例查看证书信息和剩余时间4.自动续期手动自动5.不同服务器使用1. 安装Certbot 1.1 sn…...

企业应如何防范 AI 驱动的网络安全威胁?

互联网技术和 AI 科技为世界开启了一个新的发展篇章。同时&#xff0c;网络攻击也呈现出愈发强势的发展势头&#xff1a;高级持续性威胁 &#xff08;APT&#xff1a;Advanced Persistent Threat&#xff09;组织采用新的战术、技术和程序 (TTP)、AI 驱动下攻击数量和速度的提高…...

决策树简介

【理解】决策树例子 决策树算法是一种监督学习算法&#xff0c;英文是Decision tree。 决策树思想的来源非常朴素&#xff0c;试想每个人的大脑都有类似于if-else这样的逻辑判断&#xff0c;这其中的if表示的是条件&#xff0c;if之后的else就是一种选择或决策。程序设计中的…...

ScrollView(滚动视图)详解和按钮点击事件

文章目录 **ScrollView&#xff08;滚动视图&#xff09;详解****1. 核心特性****2. 基本用法****XML 示例&#xff1a;简单滚动布局** **3. 水平滚动&#xff1a;HorizontalScrollView****4. 高级用法****(1) 嵌套滚动控件****(2) 动态添加内容****(3) 监听滚动事件** **5. 注…...

2025年3月,再上中科院1区TOP,“等级熵+状态识别、故障诊断”

引言 2025年3月&#xff0c;研究者在国际机械领域顶级期刊《Mechanical Systems and Signal Processing》&#xff08;JCR 1区&#xff0c;中科院1区 Top&#xff0c;IF&#xff1a;7.9&#xff09;上以“Rating entropy and its multivariate version”为题发表科学研究成果。…...

根据pdf文档生成问答并进行评估

目标是根据pdf文档生成问答&#xff0c;并进行评估。 首先&#xff0c;安装依赖 pip install PyPDF2 pandas tqdm openai -q 具体过程如下&#xff1a; 1、将pdf放在opeai_blog_pdfs目录下&#xff0c;引用依赖 2、上传pdf文件&#xff0c;创建向量库 3、单个提问的向量检索…...

计算机网络 - 四次挥手相关问题

通过一些问题来讨论 TCP 的四次挥手断开连接 说一下四次挥手的过程&#xff1f;为什么需要四次呢&#xff1f;time-wait干嘛的&#xff0c;close-wait干嘛的&#xff0c;在哪一个阶段&#xff1f;状态CLOSE_WAIT在什么时候转换成下一个状态呢&#xff1f;为什么 TIME-WAIT 状态…...

SLAM | 两组时间戳不同但同时开始的imu如何对齐

场景&#xff1a; 两个手机在支架上&#xff0c;同时开始采集数据 需求&#xff1a; 对齐两个数据集的imu数据 做到A图片 B imu 做法&#xff1a; 取出来两组imu数据到excel表中&#xff0c;画图 A组 B组&#xff1a; x轴 &#xff1a; 所有imu的时间戳减去第一个时间…...

code review时线程池的使用

一、多线程的作用 多个任务并行执行可以提升效率异步&#xff0c;让与主业务无关的逻辑异步执行&#xff0c;不阻塞主业务 二、问题描述 insertSelective()方法是一个并发度比较高的业务&#xff0c;主要是插入task到任务表里&#xff0c;新建task&#xff0c;并且insertSele…...

物流网络暗战升级DHL新布局将如何影响eBay卖家库存分布策略?

物流网络暗战升级&#xff1a;DHL新布局将如何影响eBay卖家库存分布策略&#xff1f; 跨境电商发展迅猛&#xff0c;卖家对物流的依赖程度不言而喻。尤其是平台型卖家&#xff0c;例如在eBay上经营多站点的卖家&#xff0c;物流成本和时效几乎直接决定了利润空间与客户满意度。…...

JAMA Netw. Open:机器学习解码大脑:精准预测PTSD症状新突破

创伤后应激障碍&#xff08;PTSD&#xff09;是一种常见的心理健康状况&#xff0c;它可以在人们经历或目睹创伤性事件&#xff08;如战争、严重事故、自然灾害、暴力攻击等&#xff09;后发展。PTSD的症状可能包括 flashbacks&#xff08;闪回&#xff09;、噩梦、严重的焦虑、…...

域控制器升级的先决条件验证失败,证书服务器已安装

出现“证书服务器已安装”导致域控制器升级失败时&#xff0c;核心解决方法是卸载已安装的证书服务‌。具体操作如下&#xff1a;‌ ‌卸载证书服务‌ 以管理员身份打开PowerShell&#xff0c;执行命令&#xff1a; Remove-WindowsFeature -Name AD-Certificate该命令会移除A…...

Node.js入门

Node.js入门 html,css,js 30年了 nodejs环境 09年出现 15年 nodejs为我们解决了2个方面的问题&#xff1a; 【锦上添花】让我们前端工程师拥有了后端开发能力&#xff08;开接口&#xff0c;访问数据库&#xff09; - 大公司BFF&#xff08;50&#xff09;【✔️】前端工程…...

使用CubeMX新建EXTI外部中断工程——不使用回调函数

具体的使用CubeMX新建工程的步骤看这里&#xff1a;STM32CubeMX学习笔记&#xff08;3&#xff09;——EXTI(外部中断)接口使用_cubemx exti-CSDN博客 之前一直都是在看野火的视频没有亲手使用CubeMX生成工程&#xff0c;而且野火给的例程代码框架和自动生成的框架也不一样&…...