目标:为所有 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

四、注意事项

  1. @Param("ew") 是硬性要求:MP 通过 ew 识别 Wrapper 对象。
  2. SQL 注入风险${} 用于表名、字段名,#{} 用于值。避免用户输入直接拼接。
  3. 方法不能有实现:自定义 Mapper 是接口,SQL 由注解定义。
  4. 事务支持:自定义方法同样支持 @Transactional
  5. 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 统一扩展
✅ 配置分页插件 支持分页方法

七、性能优化建议

  1. 避免 N+1 查询:自定义方法应尽量一次性查完。
  2. 索引优化:为 Wrapper 中常用查询字段加索引。
  3. 批量操作:使用 updateBatchById 等内置方法。
  4. 缓存:对高频查询可结合 Redis 缓存。
  5. SQL 优化:避免 SELECT *,只查必要字段。

八、总结

方式 适用场景 推荐指数
直接使用 BaseMapper 简单 CRUD ⭐⭐⭐⭐⭐
自定义 BaseMapper 子接口 通用扩展方法 强烈推荐
XML 实现 复杂 SQL ⭐⭐⭐⭐☆
注解实现 简单 SQL ⭐⭐⭐⭐☆