目标:为所有 Mapper 接口统一添加自定义通用方法(如批量软删除、按条件更新特定字段等),避免重复写 XML 或 Java 代码。
适用场景:企业级项目中,多个实体都需要相同的扩展操作(如逻辑删除增强、数据权限等)。
一、核心概念
1.1 为什么要自定义通用方法?
MyBatis-Plus 提供了 BaseMapper<T>
接口,包含常用的 CRUD 方法。但实际开发中,我们常需要:
- 批量逻辑删除
- 按条件更新某些字段(如状态)
- 多表关联通用查询
- 数据权限过滤
如果每个 Mapper 都写一遍,会造成代码重复。
1.2 解决方案:自定义 BaseMapper
通过 继承 BaseMapper<T>
并添加自定义方法 + 自定义 SQL 实现,实现“一次定义,全局使用”。
1.3 核心技术原理
技术 | 说明 |
---|---|
BaseMapper<T> |
MP 提供的基础接口 |
@Select , @Update 等注解 |
定义 SQL |
@Param |
参数绑定 |
Wrapper<T> |
构建动态条件(推荐) |
@Component + @Mapper 扫描 |
让自定义 Mapper 被识别 |
✅ 关键点:MyBatis-Plus 支持 自定义通用接口,通过 SQL 解析器 动态生成 SQL。
二、详细操作步骤(手把手教学)
步骤 1:添加依赖(Maven)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
步骤 2:配置 application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
步骤 3:创建自定义通用 Mapper 接口
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 自定义通用 Mapper 接口(所有实体 Mapper 继承它)
* @param <T> 实体类型
*/
public interface MyCustomBaseMapper<T> extends BaseMapper<T> {
/**
* 批量逻辑删除(基于 Wrapper 条件)
* @param wrapper 条件构造器
* @return 影响行数
*/
@Update("UPDATE ${ew.paramAlias.tableName} SET status = 0 WHERE ${ew.sqlSegment}")
int deleteBatchByCondition(@Param("ew") Wrapper<T> wrapper);
/**
* 按条件更新指定字段(如 status)
* @param status 新状态
* @param wrapper 条件
* @return 影响行数
*/
@Update("UPDATE ${ew.paramAlias.tableName} SET status = #{status} WHERE ${ew.sqlSegment}")
int updateStatusByCondition(@Param("status") Integer status, @Param("ew") Wrapper<T> wrapper);
/**
* 分页查询 + 条件(可扩展字段)
*/
<P extends IPage<T>> P selectPageByCondition(P page, @Param("ew") Wrapper<T> wrapper);
}
🔔 关键说明:
${ew.paramAlias.tableName}
:动态获取实体对应表名。${ew.sqlSegment}
:动态生成 WHERE 条件(来自Wrapper
)。@Param("ew") Wrapper<T> wrapper
:必须命名ew
(MP 约定)。
步骤 4:实体类添加逻辑删除字段
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String email;
@TableLogic // 启用逻辑删除
private Integer status; // 1-正常,0-删除
}
步骤 5:创建具体 Mapper 接口继承自定义 Mapper
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends MyCustomBaseMapper<User> {
// 继承了 BaseMapper 的所有方法 + 自定义方法
}
✅ 只需继承,无需实现,MP 会自动解析 SQL。
步骤 6:Service 层调用自定义方法
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 批量逻辑删除:删除 name 包含 "测试" 的用户
*/
public void deleteTestUsers() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "测试");
userMapper.deleteBatchByCondition(wrapper);
}
/**
* 更新状态:将 email 为 null 的用户状态设为 2(待完善)
*/
public void updateStatusForNullEmail() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNull("email");
userMapper.updateStatusByCondition(2, wrapper);
}
/**
* 分页查询:查询状态为 1 的用户
*/
public IPage<User> getPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1);
return userMapper.selectPageByCondition(page, wrapper);
}
}
步骤 7:配置分页插件(必须)
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 {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
步骤 8:Controller 测试
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@DeleteMapping("/test")
public String deleteTestUsers() {
userService.deleteTestUsers();
return "已删除测试用户";
}
@PutMapping("/status")
public String updateStatus() {
userService.updateStatusForNullEmail();
return "状态更新完成";
}
@GetMapping("/page")
public IPage<User> getPage(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return userService.getPage(pageNum, pageSize);
}
}
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
Invalid bound statement |
自定义方法未被扫描 | 确保 Mapper 接口被 @MapperScan 扫描 |
${ew.tableName} 报错 |
paramAlias 未正确解析 |
使用 ${ew.paramAlias.tableName} |
Wrapper 条件未生效 |
未加 @Param("ew") |
必须命名 ew |
分页无效 | 未配置 PaginationInnerInterceptor |
添加分页插件 |
逻辑删除字段未更新 | 未加 @TableLogic |
实体字段加 @TableLogic |
四、注意事项
@Param("ew")
是硬性要求:MP 通过ew
识别Wrapper
对象。- SQL 注入风险:
${}
用于表名、字段名,#{}
用于值。避免用户输入直接拼接。 - 方法不能有实现:自定义 Mapper 是接口,SQL 由注解定义。
- 事务支持:自定义方法同样支持
@Transactional
。 - XML 优先级更高:如果在 XML 中定义同名方法,注解将失效。
五、使用技巧
5.1 使用 SqlParserHelper
(高级)
可结合 SqlParserHelper
做更复杂的 SQL 解析。
5.2 定义通用返回类型
int updateByCondition(@Param("updateParam") T entity, @Param("ew") Wrapper<T> wrapper);
5.3 结合自动填充
在自定义更新方法中,可触发 @TableField(fill = FieldFill.UPDATE)
。
六、最佳实践
实践 | 说明 |
---|---|
✅ 通用操作抽象到 BaseMapper 子接口 |
减少重复代码 |
✅ 使用 Wrapper<T> 构建动态条件 |
灵活可复用 |
✅ 表名用 ${ew.paramAlias.tableName} |
安全获取表名 |
✅ 条件用 ${ew.sqlSegment} |
自动生成 WHERE |
✅ 所有 Mapper 继承自定义 Mapper | 统一扩展 |
✅ 配置分页插件 | 支持分页方法 |
七、性能优化建议
- 避免 N+1 查询:自定义方法应尽量一次性查完。
- 索引优化:为
Wrapper
中常用查询字段加索引。 - 批量操作:使用
updateBatchById
等内置方法。 - 缓存:对高频查询可结合 Redis 缓存。
- SQL 优化:避免
SELECT *
,只查必要字段。
八、总结
方式 | 适用场景 | 推荐指数 |
---|---|---|
直接使用 BaseMapper |
简单 CRUD | ⭐⭐⭐⭐⭐ |
自定义 BaseMapper 子接口 |
通用扩展方法 | ✅ 强烈推荐 |
XML 实现 | 复杂 SQL | ⭐⭐⭐⭐☆ |
注解实现 | 简单 SQL | ⭐⭐⭐⭐☆ |