跳到主要内容

基础 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
}

各策略使用场景:

策略适用场景示例
AUTOMySQL 自增主键、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);
}
}

批量插入性能优化建议:

  1. 合理设置批次大小:一般建议 100-1000 条/批次,过大可能导致内存压力或 SQL 长度超限
  2. 关闭事务自动提交:大批量插入时,在事务中执行可以显著提升性能
  3. 使用 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

逻辑删除的注意事项:

  1. 唯一索引问题:如果表有唯一索引,逻辑删除的数据会占用唯一值。解决方案是唯一索引包含删除标记字段:

    CREATE UNIQUE INDEX uk_email ON t_user(email, deleted);
  2. 查询全部数据:某些场景需要查询包含已删除的数据(如管理员后台),可以使用 @InterceptorIgnore 注解:

    @InterceptorIgnore(tenantLine = "true")
    @Select("SELECT * FROM t_user")
    List<User> selectAllIncludeDeleted();
  3. 物理删除:如果确实需要物理删除,可以直接执行自定义 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;
}

小结

本章我们学习了:

  1. BaseMapper:继承即可获得 CRUD 方法
  2. 实体类注解@TableName@TableId@TableField
  3. 插入操作:单条插入、批量插入
  4. 删除操作:根据 ID、Map、Wrapper 删除,逻辑删除
  5. 更新操作:根据 ID、Wrapper 更新,乐观锁
  6. 查询操作:各种条件查询、指定字段查询
  7. Service 层:更丰富的业务方法
  8. 自动填充:自动设置创建时间、更新时间等

参考资源