基础 CRUD 操作
本章将详细介绍 MyBatis Plus 的基础 CRUD(增删改查)操作,这是使用 MyBatis Plus 最核心的功能。
BaseMapper 接口
BaseMapper 是 MyBatis Plus 最核心的接口,它封装了大部分常用的 CRUD 方法。这是 MyBatis Plus "只做增强不做改变" 理念的体现——你只需要让 Mapper 接口继承 BaseMapper,就能获得丰富的数据库操作能力,而无需编写任何 XML 或注解 SQL。
为什么需要 BaseMapper?
在原生 MyBatis 中,即使是最简单的单表 CRUD,我们也需要编写:
<!-- 传统 MyBatis 方式 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<insert id="insert">
INSERT INTO user(name, age, email) VALUES(#{name}, #{age}, #{email})
</insert>
这些代码结构相似、模式固定,但每次都要重复编写。MyBatis Plus 的 BaseMapper 通过泛型和反射机制,自动生成这些标准 SQL,让开发者专注于业务逻辑。
继承 BaseMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 标记这是一个 MyBatis Mapper 接口,Spring 会自动扫描并创建实现类
public interface UserMapper extends BaseMapper<User> {
// 泛型参数 User 表示该 Mapper 操作的实体类型
// 继承后自动拥有 CRUD 方法,无需编写任何代码
}
继承 BaseMapper 后,无需编写任何 XML 或注解,就自动拥有了以下方法:
插入方法
| 方法 | 说明 |
|---|---|
insert(T entity) | 插入一条记录 |
删除方法
| 方法 | 说明 |
|---|---|
deleteById(Serializable id) | 根据 ID 删除 |
deleteByMap(Map<String, Object> columnMap) | 根据 map 条件删除(map 中的 key 为数据库字段名) |
delete(Wrapper<T> wrapper) | 根据条件删除 |
deleteBatchIds(Collection<? extends Serializable> idList) | 根据 ID 批量删除 |
更新方法
| 方法 | 说明 |
|---|---|
updateById(T entity) | 根据 ID 更新(只更新非 null 字段) |
update(T entity, Wrapper<T> updateWrapper) | 根据条件更新 |
查询方法
| 方法 | 说明 |
|---|---|
selectById(Serializable id) | 根据 ID 查询 |
selectBatchIds(Collection<? extends Serializable> idList) | 根据 ID 批量查询 |
selectByMap(Map<String, Object> columnMap) | 根据 map 条件查询 |
selectOne(Wrapper<T> queryWrapper) | 根据条件查询一条(多条会抛异常) |
selectList(Wrapper<T> queryWrapper) | 根据条件查询列表 |
selectCount(Wrapper<T> queryWrapper) | 根据条件查询总数 |
selectMaps(Wrapper<T> queryWrapper) | 根据条件查询返回 Map 列表 |
selectObjs(Wrapper<T> queryWrapper) | 只返回第一个字段的值 |
selectPage(Page<T> page, Wrapper<T> queryWrapper) | 分页查询 |
selectMapsPage(Page<T> page, Wrapper<T> queryWrapper) | 分页查询返回 Map |
实体类定义
实体类是 MyBatis Plus 操作数据库的核心,它定义了 Java 对象与数据库表之间的映射关系。理解实体类的注解配置是使用 MyBatis Plus 的基础。
基本实体类
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user") // 指定表名,不配置则默认将类名转为下划线格式(User -> user)
public class User {
/**
* 主键字段
* type 指定主键生成策略
*/
@TableId(type = IdType.AUTO)
private Long id;
/** 普通字段,默认映射到同名列(驼峰转下划线) */
private String name;
private Integer age;
private String email;
/**
* 自动填充字段
* INSERT 表示插入时自动填充
* 需要配置 MetaObjectHandler 实现
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* INSERT_UPDATE 表示插入和更新时都自动填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除字段
* 删除操作会变成 UPDATE,查询时会自动过滤已删除数据
*/
@TableLogic
private Integer deleted;
/**
* 乐观锁字段
* 更新时版本号会自动 +1,用于并发控制
* 需要配置 OptimisticLockerInnerInterceptor 插件
*/
@Version
private Integer version;
}
常用注解详解
MyBatis Plus 通过注解来完成对象关系映射(ORM),以下是核心注解的详细说明:
| 注解 | 说明 | 常用属性 |
|---|---|---|
@TableName | 指定实体类对应的表名 | value:表名,schema:数据库名 |
@TableId | 指定主键字段 | value:字段名,type:主键策略 |
@TableField | 指定普通字段的映射关系 | value:字段名,exist:是否为表字段,fill:自动填充策略 |
@TableLogic | 标记逻辑删除字段 | value:已删除值,delval:未删除值 |
@Version | 标记乐观锁字段 | 无 |
主键策略详解
主键策略决定了如何生成主键值,不同场景需要选择不同的策略:
public enum IdType {
AUTO, // 数据库自增,需要数据库支持 AUTO_INCREMENT
NONE, // 无状态,跟随全局配置
INPUT, // 用户手动输入,插入前必须设置 ID 值
ASSIGN_ID, // 分配 ID(雪花算法),生成 19 位 Long 类型 ID
ASSIGN_UUID // 分配 UUID,生成 32 位字符串 ID
}
各策略使用场景:
| 策略 | 适用场景 | 示例 |
|---|---|---|
AUTO | MySQL 自增主键、SQL Server Identity | 单机小规模应用 |
ASSIGN_ID | 分布式系统、需要全局唯一 ID | 微服务架构、分库分表 |
ASSIGN_UUID | 需要字符串主键、无序 ID | 某些特殊业务场景 |
INPUT | 业务生成主键、使用序列 | Oracle 序列、自定义规则 |
配置示例:
// 数据库自增(MySQL)
@TableId(type = IdType.AUTO)
private Long id;
// 雪花算法(推荐用于分布式系统)
@TableId(type = IdType.ASSIGN_ID)
private Long id;
// UUID 字符串主键
@TableId(type = IdType.ASSIGN_UUID)
private String id;
// 手动输入(需在插入前设置)
@TableId(type = IdType.INPUT)
private Long id;
全局配置默认主键策略:
mybatis-plus:
global-config:
db-config:
id-type: assign_id # 全局默认使用雪花算法
插入操作
基本插入
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class InsertTest {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setName("张三");
user.setAge(25);
user.setEmail("[email protected]");
int result = userMapper.insert(user);
System.out.println("影响行数:" + result);
System.out.println("自动生成的 ID:" + user.getId());
}
}
批量插入
BaseMapper 的 insert 方法只能插入单条记录。当需要批量插入时,需要使用 Service 层提供的 saveBatch 方法。
为什么批量插入要用 Service 层?
批量插入涉及 SQL 语句的拼接优化、事务控制等复杂逻辑,Service 层封装了这些细节,提供了更高效的批量操作能力。底层实现会将数据分批次执行,避免一次性生成过长的 SQL 导致数据库拒绝执行。
首先定义 Service 接口和实现类:
import com.baomidou.mybatisplus.extension.service.IService;
// Service 接口继承 IService
public interface UserService extends IService<User> {
}
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
// Service 实现类继承 ServiceImpl
// ServiceImpl<Mapper类型, 实体类型>
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 继承 IService 后自动拥有批量操作方法
// saveBatch、saveOrUpdateBatch、updateBatchById 等
}
使用示例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class BatchInsertTest {
@Autowired
private UserService userService;
/**
* 基本批量插入
* 默认每批次执行 1000 条
*/
@Test
void testBatchInsert() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 100; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + i % 30);
user.setEmail("user" + i + "@example.com");
users.add(user);
}
boolean success = userService.saveBatch(users);
System.out.println("批量插入结果:" + success);
// 生成的 SQL(实际是分批执行):
// INSERT INTO t_user (name, age, email) VALUES ('用户0', 20, '[email protected]'), ...
}
/**
* 指定批次大小的批量插入
* 当数据量大时,建议指定较小的批次大小,避免内存溢出或 SQL 过长
*/
@Test
void testBatchInsertWithBatchSize() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + i % 30);
user.setEmail("user" + i + "@example.com");
users.add(user);
}
// 每批次执行 100 条,共执行 10 批次
boolean success = userService.saveBatch(users, 100);
System.out.println("批量插入结果:" + success);
}
}
批量插入性能优化建议:
- 合理设置批次大小:一般建议 100-1000 条/批次,过大可能导致内存压力或 SQL 长度超限
- 关闭事务自动提交:大批量插入时,在事务中执行可以显著提升性能
- 使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE:需要时可以通过自定义 SQL 实现
## 删除操作
### 根据 ID 删除
```java
@Test
void testDeleteById() {
int result = userMapper.deleteById(1L);
System.out.println("删除结果:" + result);
}
根据 ID 批量删除
@Test
void testDeleteBatchIds() {
List<Long> ids = Arrays.asList(1L, 2L, 3L);
int result = userMapper.deleteBatchIds(ids);
System.out.println("批量删除结果:" + result);
}
根据 Map 条件删除
@Test
void testDeleteByMap() {
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "张三");
columnMap.put("age", 25);
int result = userMapper.deleteByMap(columnMap);
System.out.println("根据条件删除结果:" + result);
}
根据 Wrapper 条件删除
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@Test
void testDeleteByWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.gt("age", 20);
int result = userMapper.delete(wrapper);
System.out.println("根据条件删除结果:" + result);
}
逻辑删除
逻辑删除是一种数据安全机制,它不真正删除数据,而是通过标记字段来表示数据是否"已删除"。这在需要数据追溯、审计或支持数据恢复的场景中非常有用。
逻辑删除 vs 物理删除:
| 对比项 | 物理删除 | 逻辑删除 |
|---|---|---|
| 数据存储 | 从数据库永久删除 | 保留在数据库中 |
| 可恢复性 | 不可恢复 | 可恢复 |
| 查询性能 | 数据量小,查询快 | 数据量大时需考虑清理策略 |
| 存储空间 | 节省空间 | 占用更多空间 |
| 审计追溯 | 无法追溯 | 完整保留历史 |
配置逻辑删除:
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
/**
* 逻辑删除字段
* 0 表示未删除,1 表示已删除(默认值)
*/
@TableLogic
private Integer deleted;
}
全局配置逻辑删除值:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 已删除值
logic-not-delete-value: 0 # 未删除值
逻辑删除的工作原理:
MyBatis Plus 通过 SQL 拦截器,自动为所有查询和删除操作添加逻辑删除条件:
// 测试逻辑删除
@Test
void testLogicDelete() {
// 执行删除操作
int result = userMapper.deleteById(1L);
System.out.println("逻辑删除结果:" + result);
// 尝试查询已删除的数据
User user = userMapper.selectById(1L);
System.out.println("查询结果:" + user); // 输出: null
}
实际执行的 SQL:
-- 删除操作变成了更新
UPDATE t_user SET deleted = 1 WHERE id = 1 AND deleted = 0
-- 查询操作自动添加了过滤条件
SELECT * FROM t_user WHERE id = 1 AND deleted = 0
逻辑删除的注意事项:
-
唯一索引问题:如果表有唯一索引,逻辑删除的数据会占用唯一值。解决方案是唯一索引包含删除标记字段:
CREATE UNIQUE INDEX uk_email ON t_user(email, deleted); -
查询全部数据:某些场景需要查询包含已删除的数据(如管理员后台),可以使用
@InterceptorIgnore注解:@InterceptorIgnore(tenantLine = "true")
@Select("SELECT * FROM t_user")
List<User> selectAllIncludeDeleted(); -
物理删除:如果确实需要物理删除,可以直接执行自定义 SQL:
@Delete("DELETE FROM t_user WHERE id = #{id}")
int physicalDeleteById(Long id);
更新操作
根据 ID 更新
@Test
void testUpdateById() {
User user = new User();
user.setId(1L);
user.setName("李四");
user.setAge(30);
int result = userMapper.updateById(user);
System.out.println("更新结果:" + result);
}
注意:updateById 只更新非 null 字段。如果需要将字段更新为 null,需要使用 UpdateWrapper 或添加 @TableField(updateStrategy = FieldStrategy.IGNORED)。
根据 Wrapper 更新
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@Test
void testUpdateByWrapper() {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("name", "张三")
.set("age", 26)
.set("email", "[email protected]");
int result = userMapper.update(null, wrapper);
System.out.println("更新结果:" + result);
}
更新为 NULL 值
@Test
void testUpdateToNull() {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L)
.set("email", null);
int result = userMapper.update(null, wrapper);
System.out.println("更新为 NULL 结果:" + result);
}
Lambda 方式更新
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@Test
void testLambdaUpdate() {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getName, "张三")
.set(User::getAge, 26)
.set(User::getEmail, "[email protected]");
int result = userMapper.update(null, wrapper);
System.out.println("Lambda 更新结果:" + result);
}
乐观锁更新
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@Version
private Integer version;
}
// 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
// 测试
@Test
void testOptimisticLock() {
User user = userMapper.selectById(1L);
user.setName("新名字");
int result = userMapper.updateById(user);
System.out.println("乐观锁更新结果:" + result);
}
执行 SQL:
UPDATE t_user SET name = '新名字', version = version + 1
WHERE id = 1 AND version = 旧版本号
查询操作
根据 ID 查询
@Test
void testSelectById() {
User user = userMapper.selectById(1L);
System.out.println(user);
}
根据 ID 批量查询
@Test
void testSelectBatchIds() {
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<User> users = userMapper.selectBatchIds(ids);
users.forEach(System.out::println);
}
根据 Map 条件查询
@Test
void testSelectByMap() {
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "张三");
columnMap.put("age", 25);
List<User> users = userMapper.selectByMap(columnMap);
users.forEach(System.out::println);
}
查询所有
@Test
void testSelectAll() {
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
条件查询
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@Test
void testSelectByCondition() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.gt("age", 20)
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
Lambda 条件查询
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@Test
void testLambdaSelect() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.gt(User::getAge, 20)
.orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
查询一条记录
@Test
void testSelectOne() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三");
User user = userMapper.selectOne(wrapper);
System.out.println(user);
}
注意:如果查询结果有多条,会抛出异常。
查询总数
@Test
void testSelectCount() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, 20);
Long count = userMapper.selectCount(wrapper);
System.out.println("总数:" + count);
}
查询返回 Map
@Test
void testSelectMaps() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getName, User::getAge)
.gt(User::getAge, 20);
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
指定查询字段
@Test
void testSelectSpecificFields() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getName)
.gt(User::getAge, 20);
List<User> users = userMapper.selectList(wrapper);
users.forEach(u -> System.out.println(u.getId() + ": " + u.getName()));
}
Service 层 CRUD
MyBatis Plus 提供了 IService 接口和 ServiceImpl 实现类,封装了更多业务方法。
定义 Service 接口
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
}
实现 Service 类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
Service 层常用方法
@SpringBootTest
class ServiceTest {
@Autowired
private UserService userService;
@Test
void testSave() {
User user = new User();
user.setName("王五");
user.setAge(28);
boolean success = userService.save(user);
System.out.println("保存结果:" + success);
}
@Test
void testSaveBatch() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + i);
users.add(user);
}
boolean success = userService.saveBatch(users);
System.out.println("批量保存结果:" + success);
}
@Test
void testSaveOrUpdate() {
User user = new User();
user.setId(1L);
user.setName("更新后的名字");
boolean success = userService.saveOrUpdate(user);
System.out.println("保存或更新结果:" + success);
}
@Test
void testRemoveById() {
boolean success = userService.removeById(1L);
System.out.println("删除结果:" + success);
}
@Test
void testUpdateById() {
User user = new User();
user.setId(1L);
user.setName("新名字");
boolean success = userService.updateById(user);
System.out.println("更新结果:" + success);
}
@Test
void testGetById() {
User user = userService.getById(1L);
System.out.println(user);
}
@Test
void testList() {
List<User> users = userService.list();
users.forEach(System.out::println);
}
@Test
void testListByCondition() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, 20);
List<User> users = userService.list(wrapper);
users.forEach(System.out::println);
}
@Test
void testCount() {
long count = userService.count();
System.out.println("总数:" + count);
}
}
自动填充
自动填充可以在插入或更新时自动设置字段值。
配置自动填充处理器
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUserId());
}
private String getCurrentUserId() {
return "system";
}
}
实体类配置
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
}
小结
本章我们学习了:
- BaseMapper:继承即可获得 CRUD 方法
- 实体类注解:
@TableName、@TableId、@TableField等 - 插入操作:单条插入、批量插入
- 删除操作:根据 ID、Map、Wrapper 删除,逻辑删除
- 更新操作:根据 ID、Wrapper 更新,乐观锁
- 查询操作:各种条件查询、指定字段查询
- Service 层:更丰富的业务方法
- 自动填充:自动设置创建时间、更新时间等