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 时,我们经常需要进行以下重复工作:
- 编写大量的 XML 映射文件,即使是最简单的 CRUD 操作
- 在执行分页查询时,需要手写分页相关的 SQL 语句
- 没有统一的 Service 层封装,导致代码重复
- SQL 语句容易写错,且不同数据库的 SQL 语法存在差异
MyBatis-Plus 的优势
相比于原生 MyBatis,使用 MyBatis-Plus 可以带来以下优势:
- 减少代码量:内置通用 Mapper、Service,单表 CRUD 操作无需编写 SQL
- 提高开发效率:避免了手动编写基础 CRUD 操作,团队开发更加规范高效
- 增强功能特性:内置主键生成、分页、性能分析、全局拦截等功能
- 支持 Lambda 表达式:使用 Lambda 编写查询条件,类型安全,避免字段名称错误
- 多种主键策略:内置多种主键生成策略,支持自定义主键生成器
- 代码生成器:可快速生成 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 没有自动生成。
解决方案:
- 检查实体类中的 ID 字段是否正确配置了
@TableId
注解。 - 确保指定了正确的 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 字段名映射问题
问题:数据库字段名与实体类属性名不一致,导致查询结果映射错误。
解决方案:
- 启用驼峰命名转换:
mybatis-plus:configuration:map-underscore-to-camel-case: true
- 使用
@TableField
注解显式指定映射关系:
@TableField("user_name")
private String userName;
- 对于不需要映射的字段,可以使用
@TableField(exist = false)
排除。
11.3 分页问题
问题:分页查询不起作用或结果不正确。
解决方案:
- 确保正确配置了分页插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;
}
- 检查分页参数是否正确:
// 第一页,每页10条
Page<User> page = new Page<>(1, 10);
- 对于复杂的分页查询,可能需要自定义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 逻辑删除问题
问题:逻辑删除配置后,查询结果异常。
解决方案:
- 确保正确配置了逻辑删除字段:
mybatis-plus:global-config:db-config:logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0
- 实体类中添加对应注解:
@TableLogic
private Integer deleted;
- 如果需要查询被逻辑删除的数据,需要使用自定义SQL:
@Select("SELECT * FROM user WHERE id = #{id}")
User selectByIdIncludeLogicDeleted(Long id);
11.5 复杂查询问题
问题:需要进行复杂连接查询,如多表关联查询。
解决方案:
- 使用自定义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);
- 使用 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>
- 使用多次查询组合结果:
// 在 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 映射文件没有被识别。
解决方案:
- 确保 XML 文件放在正确的目录下,并在配置中指定:
mybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml
- 检查 XML 文件的命名空间是否与 Mapper 接口匹配:
<mapper namespace="com.example.mapper.UserMapper"><!-- 映射内容 -->
</mapper>
- 在使用 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 核心优势
- 简化开发:通过继承 BaseMapper 和 IService,减少大量重复的 CRUD 代码编写。
- 强大的条件构造器:提供了丰富的条件构造方式,支持 Lambda 表达式,类型安全。
- 丰富的功能:内置分页、逻辑删除、自动填充、乐观锁等功能,无需手动实现。
- 性能优化:提供了性能分析插件,帮助开发者优化 SQL。
- 代码生成:快速生成基础代码,提高开发效率。
13.2 最佳实践
13.2.1 Entity 设计
- 使用 Lombok:减少冗余代码,使用
@Data
、@Builder
等注解。 - 规范字段命名:实体类使用驼峰命名,数据库使用下划线命名。
- 基础字段抽象:将通用字段抽取到基类中,如
id
、createTime
、updateTime
等。 - 使用注解标注特殊字段:如
@TableId
、@TableLogic
、@Version
等。
13.2.2 Mapper 设计
- 继承 BaseMapper:优先使用 MyBatis-Plus 提供的通用方法。
- 自定义方法谨慎使用:仅在复杂场景下添加自定义方法,避免冗余。
- XML 与注解结合:简单查询使用注解,复杂查询使用 XML。
13.2.3 Service 设计
- 继承 ServiceImpl:获取丰富的批量操作和链式操作能力。
- 业务逻辑集中:复杂的业务逻辑应当集中在 Service 层,保持 Controller 的简洁。
- 事务管理:在 Service 层管理事务,确保数据一致性。
13.2.4 性能优化
- 避免全表扫描:合理使用索引,避免不必要的全表扫描。
- 减少不必要的字段查询:使用
select()
方法指定需要查询的字段。 - 批量操作:使用批量插入、更新和删除,减少数据库交互次数。
- 合理使用分页:避免一次性查询大量数据,影响性能。
13.2.5 安全性
- 防止SQL注入:使用参数绑定而非字符串拼接 SQL。
- 数据校验:在 DTO 层使用 JSR303 注解进行数据校验。
- 权限控制:实施细粒度的权限控制,保障数据安全。
13.3 进阶学习方向
- 深入理解 MyBatis 原理:MyBatis-Plus 建立在 MyBatis 之上,了解 MyBatis 的工作原理有助于更好地使用 MyBatis-Plus。
- 动态数据源:学习如何在同一应用中连接多个数据库。
- 分库分表:了解如何使用 MyBatis-Plus 配合 Sharding-JDBC 等中间件实现分库分表。
- SQL性能优化:学习如何分析和优化 SQL 性能。
- 缓存策略:学习如何合理使用缓存提高查询性能。
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 核心优势
- 简化开发:通过继承 BaseMapper 和 IService,减少大量重复的 CRUD 代码编写。
- 强大的条件构造器:提供了丰富的条件构造方式,支持 Lambda 表达式,类型安全。
- 丰富的功能:内置分页、逻辑删除、自动填充、乐观锁等功能,无需手动实现。
- 性能优化:提供了性能分析插件,帮助开发者优化 SQL。
- 代码生成:快速生成基础代码,提高开发效率。
13.2 最佳实践
13.2.1 Entity 设计
- 使用 Lombok:减少冗余代码,使用
@Data
、@Builder
等注解。 - 规范字段命名:实体类使用驼峰命名,数据库使用下划线命名。
- 基础字段抽象:将通用字段抽取到基类中,如
id
、createTime
、updateTime
等。 - 使用注解标注特殊字段:如
@TableId
、@TableLogic
、@Version
等。
13.2.2 Mapper 设计
- 继承 BaseMapper:优先使用 MyBatis-Plus 提供的通用方法。
- 自定义方法谨慎使用:仅在复杂场景下添加自定义方法,避免冗余。
- XML 与注解结合:简单查询使用注解,复杂查询使用 XML。
13.2.3 Service 设计
- 继承 ServiceImpl:获取丰富的批量操作和链式操作能力。
- 业务逻辑集中:复杂的业务逻辑应当集中在 Service 层,保持 Controller 的简洁。
- 事务管理:在 Service 层管理事务,确保数据一致性。
13.2.4 性能优化
- 避免全表扫描:合理使用索引,避免不必要的全表扫描。
- 减少不必要的字段查询:使用
select()
方法指定需要查询的字段。 - 批量操作:使用批量插入、更新和删除,减少数据库交互次数。
- 合理使用分页:避免一次性查询大量数据,影响性能。
13.2.5 安全性
- 防止SQL注入:使用参数绑定而非字符串拼接 SQL。
- 数据校验:在 DTO 层使用 JSR303 注解进行数据校验。
- 权限控制:实施细粒度的权限控制,保障数据安全。
13.3 进阶学习方向
- 深入理解 MyBatis 原理:MyBatis-Plus 建立在 MyBatis 之上,了解 MyBatis 的工作原理有助于更好地使用 MyBatis-Plus。
- 动态数据源:学习如何在同一应用中连接多个数据库。
- 分库分表:了解如何使用 MyBatis-Plus 配合 Sharding-JDBC 等中间件实现分库分表。
- SQL性能优化:学习如何分析和优化 SQL 性能。
- 缓存策略:学习如何合理使用缓存提高查询性能。
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?1.2 为什么要使用 MyBatis-Plus?传统 MyBatis 的痛点MyBatis-Plus 的优势 1.3 MyBatis-Plus 与 MyBatis 的关系 2. 快速开始2.1 环境要求2.2 依赖引入MavenGradle 2.3 数据库准备2.4 配置 Spring …...
Java设计模式之观察者模式:从入门到架构级实践
一、观察者模式的核心价值 观察者模式(Observer Pattern)是行为型设计模式中的经典之作,它建立了对象间的一对多依赖关系,让多个观察者对象能够自动感知被观察对象的状态变化。这种模式在事件驱动系统、实时数据推送、GUI事件处理…...
【双指针】专题:LeetCode 202题解——快乐数
快乐数 一、题目链接二、题目三、题目解析四、算法原理扩展 五、编写代码 一、题目链接 快乐数 二、题目 三、题目解析 快乐数的定义中第二点最重要,只有两种情况,分别拿示例1、示例2分析吧: 示例1中一旦出现1了,继续重复过程就…...
深度学习占用大量内存空间解决办法
应该是缓存的问题,关机重启内存多了10G,暂时没找到别的方法 重启前 关机重启后...
[LeetCode 1871] 跳跃游戏 7(Ⅶ)
题面: 数据范围: 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′ ,要么是 ′ 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:基于选择性记忆组合的低开销视觉语言导航
作者: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单位: 1 ^{1} 1同济大学计算机科学与技术学院, 2 ^{2} 2中科院自动化研究所࿰…...
【Ubuntu | 网络】Vmware虚拟机里的Ubuntu开机后没有网络接口、也没有网络图标
😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C、数据结构、音视频🍭 😎金句分享😎&a…...
第二十一讲 XGBoost 回归建模 + SHAP 可解释性分析(利用R语言内置数据集)
下面我将使用 R 语言内置的 mtcars 数据集,模拟一个完整的 XGBoost 回归建模 SHAP 可解释性分析 实战流程。我们将以预测汽车的油耗(mpg)为目标变量,构建 XGBoost 模型,并用 SHAP 来解释模型输出。 🚗 示例…...
HP惠普打印机:解决每次打印后额外产生@PJL SET USERNAME=文档的情况
情况描述 惠普商用打印机型号:Color LaserJet Managed MFP E78223 在每次打印文档后都会出现包含我个人电脑用户名的额外文档: 这不是我希望的,因此我联系了惠普官方客服,并得到了解决 解决方案 原因 具客服所说,这些是…...
MariaDB MaxScale 的用途与实现细节
MaxScale 主要用途 MariaDB MaxScale 是一个智能数据库代理(proxy),主要用于增强 MySQL/MariaDB 数据库的高可用性、可扩展性和安全性,同时简化应用程序与数据库基础设施之间的交互。它的核心功能包括: 负载均衡&…...
CTF--eval
一、原网页: 二、步骤: 1.代码分析: <?phpinclude "flag.php"; // 引入一个文件,该文件可能定义了一些变量(例如 $flag)$a $_REQUEST[hello]; // 从用户请求中获取参数 hello 的值&#x…...
Android学习总结之算法篇七(图和矩阵)
有向图的深度优先搜索(DFS)和广度优先搜索(BFS)的示例,以此来模拟遍历 GC Root 引用链这种有向图结构: 一、深度优先搜索(DFS) import java.util.*;public class GraphDFS {privat…...
vmcore分析锁问题实例(x86-64)
问题描述:系统出现panic,dmesg有如下打印: [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等资源文件的下载
文件资源的下载,是我们业务开发中常见的需求。作为前端开发,学习下如何自己使用node的express框架来实现资源的下载操作。 实现效果 代码实现 前端 1.封装的请求后端下载接口的方法,需求配置aixos的请求参数里面的返回数据类型为blob // 下载 export…...
【BUG】Redis RDB快照持久化及写操作禁止问题排查与解决
1 问题描述 在使用Redis 的过程中,遇到如下报错,错误信息是 “MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk...”,记录下问题排查过程。 2 问题排查与解决 该错误提示表明&#…...
【HD-RK3576-PI】定制用户升级固件
硬件:HD-RK3576-PI 软件:Linux6.1Ubuntu22.04 在进行 Rockchip 相关开发时,制作自定义的烧写固件是一项常见且重要的操作。这里主要介绍文件系统的修改以及打包成完整update包升级的过程。 一、修改文件系统镜像(Ubuntu环境操作&…...
【AI学习】李宏毅老师讲AI Agent摘要
在b站听了李宏毅2025最新的AI Agent教程,简单易懂,而且紧跟发展,有大量最新的研究进展。 教程中引用了大量论文,为了方便将来阅读相关论文,进一步深入理解,做了截屏纪录。 同时也做一下分享。 根据经验调整…...
狂神SQL学习笔记十:修改和删除数据表字段
1、修改与删除表 alter 修改表的名称: 增加表的字段: 修改表的字段(重命名,修改约束): 修改约束 重命名 删除表的字段 删除表...
OSPF综合实验
一、网络拓扑 二、实验要求 1,R5为ISP,其上只能配置IP地址;R4作为企业边界路由器; 2,整个0SPF环境IP基于172.16.0.8/16划分; 3,所有设备均可访问R5的环回; 4,减少LSA的更新量,加快收敛…...
2025 cs144 Lab Checkpoint 2 小白超详细版
文章目录 1 环形索引的实现1.1 wrap类wrapunwrap 2 实现tcp_receiver2.1 tcp_receiver的功能2.2 传输的报文格式TCPSenderMessageTCPReceiverMessage 2.3 如何实现函数receive()send() 1 环形索引的实现 范围是0~2^32-1 需要有SY…...
VMware虚拟机安装Ubuntu 22.04.2
一、我的虚拟机版本 二、浏览器搜索Ubuntu 三、下载Ubuntu桌面版 四、下这个 五、创建新的虚拟机 六、选择典型,然后下一步 七、选择稍后安装操作系统,然后下一步 八、选择Linux ,版本选择Ubuntu 64位 九、选择好安装位置 十、磁盘大小一般选20G就够用了…...
XSS漏洞及常见处理方案
文章背景: 在近期项目安全测试中,安全团队发现了一处潜在的 跨站脚本攻击(XSS)漏洞,该漏洞可能导致用户数据被篡改或会话劫持等安全风险。针对这一问题,项目组迅速响应,通过代码修复、输入过滤、…...
TCP标志位抓包
说明 TCP协议的Header信息,URG、ACK、PSH、RST、SYN、FIN这6个字段在14字节的位置,对应的是tcp[13],因为字节数是从[0]开始数的,14字节对应的就是tcp[13],因此在抓这几个标志位的数据包时就要明确范围在tcp[13] 示例1…...
C/C++条件判断
条件判断 if语句的三种形态 if(a<b){} 、 if(a<b){}else{} 、 if(a<b){}else if(a>b) else{} if语句的嵌套 嵌套的常见错误(配对错误),与前面最近的,而且还没有配对的if匹配 错误避免方法:严格使用 { }、先写&am…...
单位门户网站被攻击后的安全防护策略
政府网站安全现状与挑战 近年来,随着数字化进程的加速,政府门户网站已成为政务公开和服务公众的重要窗口。然而,网络安全形势却日益严峻。国家互联网应急中心的数据显示,政府网站已成为黑客攻击的重点目标,被篡改和被…...
# 工具记录
工具记录 键盘操作可视化工具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);返回值: str1 < str2时, 返回值< 0(有些编译器返回 -1) str1 > str2时…...
CloudWeGo 技术沙龙·深圳站回顾:云原生 × AI 时代的微服务架构与技术实践
2025 年 3 月 22 日,CloudWeGo “云原生 AI 时代的微服务架构与技术实践”主题沙龙在深圳圆满落幕。作为云原生与 AI 微服务融合领域的深度技术聚会,本次活动吸引了来自企业、开发者社区的百余位参与者,共同探讨如何通过开源技术应对智能时代…...
STM32移植文件系统FATFS——片外SPI FLASH
一、电路连接 主控芯片选型为:STM32F407ZGT6,SPI FLASH选型为:W25Q256JV。 采用了两片32MB的片外SPI FLASH,电路如图所示。 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 驱动开发中,container_of 宏常被用来实现一个驱动兼容多种不同设备的架构。这种设计模式在 Linux 内核中非常常见,特别 是在设备驱动模型中。linux内核的主要开发语言是C,但是现在内核的框架使用了非常多的面向…...
UE5 检测球形范围的所有Actor
和Untiiy不同,不需要复杂的调用 首选确保角色添加了Sphere Collision 然后直接把sphere拖入蓝图,调用GetOverlappingActors来获取碰撞范围内的所有Actor...
AI大模型学习十:Ubuntu 22.04.5 调整根目录大小,解决根目录磁盘不够问题
一、说明 由于默认安装时导致home和根目录大小一样,导致根目录不够,所以我们调整下 二、调整 # 确认/home和/是否为独立逻辑卷,并属于同一卷组(VG) rootnode1:~# lsblk NAME MAJ:MIN RM SIZE…...
在ros2上使用opencv显示一张图片
1.先将图片放到桌面上 2.打开终端ctrlaltT,查看自己是否已安装opencv 3.创建工作环境 4.进入工作目录并创建ROS2包添加OpenCV依赖项 5.进入/home/kong/opencv_ws/opencv_use/src目录创建.cpp文件并编辑 6.代码如下 my_opencv.cpp #include <cstdio> #include…...
训练神经网络的原理(前向传播、反向传播、优化、迭代)
训练神经网络的原理 通过前向传播计算预测值和损失,利用反向传播计算梯度,然后通过优化算法更新参数,最终使模型在给定任务上表现更好。 核心:通过计算损失函数(通常是模型预测与真实值之间的差距)对模型参…...
每日一题(小白)暴力娱乐篇30
顺时针旋转,从上图中不难看出行列进行了变换。因为这是一道暴力可以解决的问题,我们直接尝试使用行列转换看能不能得到想要的结果。 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 科技为世界开启了一个新的发展篇章。同时,网络攻击也呈现出愈发强势的发展势头:高级持续性威胁 (APT:Advanced Persistent Threat)组织采用新的战术、技术和程序 (TTP)、AI 驱动下攻击数量和速度的提高…...
决策树简介
【理解】决策树例子 决策树算法是一种监督学习算法,英文是Decision tree。 决策树思想的来源非常朴素,试想每个人的大脑都有类似于if-else这样的逻辑判断,这其中的if表示的是条件,if之后的else就是一种选择或决策。程序设计中的…...
ScrollView(滚动视图)详解和按钮点击事件
文章目录 **ScrollView(滚动视图)详解****1. 核心特性****2. 基本用法****XML 示例:简单滚动布局** **3. 水平滚动:HorizontalScrollView****4. 高级用法****(1) 嵌套滚动控件****(2) 动态添加内容****(3) 监听滚动事件** **5. 注…...
2025年3月,再上中科院1区TOP,“等级熵+状态识别、故障诊断”
引言 2025年3月,研究者在国际机械领域顶级期刊《Mechanical Systems and Signal Processing》(JCR 1区,中科院1区 Top,IF:7.9)上以“Rating entropy and its multivariate version”为题发表科学研究成果。…...
根据pdf文档生成问答并进行评估
目标是根据pdf文档生成问答,并进行评估。 首先,安装依赖 pip install PyPDF2 pandas tqdm openai -q 具体过程如下: 1、将pdf放在opeai_blog_pdfs目录下,引用依赖 2、上传pdf文件,创建向量库 3、单个提问的向量检索…...
计算机网络 - 四次挥手相关问题
通过一些问题来讨论 TCP 的四次挥手断开连接 说一下四次挥手的过程?为什么需要四次呢?time-wait干嘛的,close-wait干嘛的,在哪一个阶段?状态CLOSE_WAIT在什么时候转换成下一个状态呢?为什么 TIME-WAIT 状态…...
SLAM | 两组时间戳不同但同时开始的imu如何对齐
场景: 两个手机在支架上,同时开始采集数据 需求: 对齐两个数据集的imu数据 做到A图片 B imu 做法: 取出来两组imu数据到excel表中,画图 A组 B组: x轴 : 所有imu的时间戳减去第一个时间…...
code review时线程池的使用
一、多线程的作用 多个任务并行执行可以提升效率异步,让与主业务无关的逻辑异步执行,不阻塞主业务 二、问题描述 insertSelective()方法是一个并发度比较高的业务,主要是插入task到任务表里,新建task,并且insertSele…...
物流网络暗战升级DHL新布局将如何影响eBay卖家库存分布策略?
物流网络暗战升级:DHL新布局将如何影响eBay卖家库存分布策略? 跨境电商发展迅猛,卖家对物流的依赖程度不言而喻。尤其是平台型卖家,例如在eBay上经营多站点的卖家,物流成本和时效几乎直接决定了利润空间与客户满意度。…...
JAMA Netw. Open:机器学习解码大脑:精准预测PTSD症状新突破
创伤后应激障碍(PTSD)是一种常见的心理健康状况,它可以在人们经历或目睹创伤性事件(如战争、严重事故、自然灾害、暴力攻击等)后发展。PTSD的症状可能包括 flashbacks(闪回)、噩梦、严重的焦虑、…...
域控制器升级的先决条件验证失败,证书服务器已安装
出现“证书服务器已安装”导致域控制器升级失败时,核心解决方法是卸载已安装的证书服务。具体操作如下: 卸载证书服务 以管理员身份打开PowerShell,执行命令: Remove-WindowsFeature -Name AD-Certificate该命令会移除A…...
Node.js入门
Node.js入门 html,css,js 30年了 nodejs环境 09年出现 15年 nodejs为我们解决了2个方面的问题: 【锦上添花】让我们前端工程师拥有了后端开发能力(开接口,访问数据库) - 大公司BFF(50)【✔️】前端工程…...
使用CubeMX新建EXTI外部中断工程——不使用回调函数
具体的使用CubeMX新建工程的步骤看这里:STM32CubeMX学习笔记(3)——EXTI(外部中断)接口使用_cubemx exti-CSDN博客 之前一直都是在看野火的视频没有亲手使用CubeMX生成工程,而且野火给的例程代码框架和自动生成的框架也不一样&…...