一、核心概念

1. 什么是自定义 SQL?

在 MyBatis-Plus 中,自定义 SQL 是指开发者通过 @Select, @Insert, @Update, @Delete 等注解,在 Mapper 接口中直接编写原生 SQL 语句,绕过 MP 的 Wrapper 动态拼接机制,实现复杂或高性能的数据库操作。

✅ 优势:

  • 精确控制 SQL,适用于复杂查询(如多表 JOIN、子查询、聚合函数)
  • 性能更高,避免 Wrapper 的解析开销
  • 支持数据库特有语法(如 MySQL 的 ON DUPLICATE KEY UPDATE

⚠️ 注意:

  • 自定义 SQL 不参与 MP 的自动填充(如 @TableField(fill = ...))、逻辑删除、多租户等插件逻辑(除非手动处理)
  • SQL 编写需注意 SQL 注入风险

二、操作步骤(超详细,适合快速掌握)

步骤 1:创建实体类(Entity)

@Data
@TableName("user") // 指定数据库表名
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private LocalDateTime createTime;
    private Integer status; // 0:禁用, 1:启用
}

步骤 2:创建 Mapper 接口(继承 BaseMapper)

@Mapper
public interface UserMapper extends BaseMapper<User> {

    // 后续自定义 SQL 将写在这里
}

BaseMapper 提供了常用的 CRUD 方法(如 selectById, insert, update 等)


步骤 3:使用 @Select 注解编写查询 SQL

场景 1:简单查询(单表)

@Select("SELECT id, name, age, email FROM user WHERE id = #{id}")
User selectByIdCustom(Long id);

场景 2:复杂查询(多表 JOIN)

@Select("""
    SELECT u.id, u.name, u.age, d.name AS deptName, r.role_name AS roleName
    FROM user u
    LEFT JOIN dept d ON u.dept_id = d.id
    LEFT JOIN user_role ur ON u.id = ur.user_id
    LEFT JOIN role r ON ur.role_id = r.id
    WHERE u.id = #{userId} AND u.status = 1
    """)
Map<String, Object> selectUserWithDeptAndRole(@Param("userId") Long userId);

🔍 使用 @Param("userId") 明确指定参数名,避免歧义。

场景 3:返回 List + 条件查询

@Select("SELECT id, name, age FROM user WHERE age >= #{minAge} AND status = #{status}")
List<User> selectUsersByAgeAndStatus(@Param("minAge") Integer minAge, @Param("status") Integer status);

场景 4:使用 LIKE 模糊查询(防 SQL 注入)

// ❌ 错误方式:可能导致 SQL 注入或语法错误
// @Select("SELECT * FROM user WHERE name LIKE '%${name}%'")

// ✅ 正确方式:使用 CONCAT 或数据库函数
@Select("SELECT id, name, age FROM user WHERE name LIKE CONCAT('%', #{name}, '%')")
List<User> selectUsersByNameLike(@Param("name") String name);

步骤 4:使用 @Insert 注解插入数据

@Insert("INSERT INTO user(name, age, email, create_time, status) VALUES(#{name}, #{age}, #{email}, NOW(), #{status})")
int insertUser(@Param("name") String name, 
               @Param("age") Integer age, 
               @Param("email") String email, 
               @Param("status") Integer status);

✅ 返回值为 int,表示插入影响的行数。


步骤 5:使用 @Update 注解更新数据

@Update("UPDATE user SET name = #{name}, age = #{age}, status = #{status} WHERE id = #{id}")
int updateUserById(@Param("id") Long id,
                   @Param("name") String name,
                   @Param("age") Integer age,
                   @Param("status") Integer status);

步骤 6:使用 @Delete 注解删除数据

@Delete("DELETE FROM user WHERE id = #{id}")
int deleteByIdCustom(@Param("id") Long id);

步骤 7:在 Service 中调用自定义方法

@Service
@Transactional
@Slf4j
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Long id) {
        return userMapper.selectByIdCustom(id);
    }

    public List<User> getUsersByAgeAndStatus(Integer minAge, Integer status) {
        return userMapper.selectUsersByAgeAndStatus(minAge, status);
    }

    public int addUser(String name, Integer age, String email, Integer status) {
        return userMapper.insertUser(name, age, email, status);
    }
}

三、常见错误与解决方案

错误现象 原因 解决方案
BindingException: Parameter 'xxx' not found 参数未使用 @Param 注解 所有命名参数必须加 @Param
SQL 语法错误 SQL 字符串拼写错误 使用 """...""" 多行字符串提高可读性
返回字段映射失败 字段名与属性名不一致 使用 AS 别名或开启 mapUnderscoreToCamelCase
LIKE 查询无效 使用 ${} 导致转义问题 改用 CONCAT('%', #{value}, '%')
插入后主键未回填 自定义 SQL 不支持 @TableId 自动生成 手动处理或改用 insert 方法

四、注意事项

  1. 必须使用 @Param 注解
    当方法参数多于一个,或参数为基本类型/包装类时,必须使用 @Param("name") 指定参数名。

  2. 字段名映射问题

    • 数据库字段如 create_time 需映射到 Java 属性 createTime
    • 方案①:SQL 中使用 AS createTime
    • 方案②:在 application.yml 中开启驼峰映射:
      mybatis-plus:
        configuration:
          map-underscore-to-camel-case: true
      
  3. 不支持自动填充
    自定义 SQL 不会触发 MetaObjectHandlerinsertFill/updateFill,需手动写入:

    INSERT INTO user(create_time) VALUES(NOW())
    
  4. 不支持逻辑删除
    若启用了 @TableLogic,自定义 SQL 需手动添加 AND deleted = 0 条件。

  5. 不支持多租户
    需手动添加 AND tenant_id = #{tenantId} 条件。


五、使用技巧

1. 使用多行字符串(Java 15+)提升可读性

@Select("""
    SELECT u.id, u.name, d.name AS deptName
    FROM user u
    LEFT JOIN dept d ON u.dept_id = d.id
    WHERE u.status = #{status}
      AND u.create_time >= #{startDate}
    ORDER BY u.id DESC
    """)
List<Map<String, Object>> complexQuery(@Param("status") Integer status,
                                       @Param("startDate") LocalDateTime startDate);

2. 返回 Map<String, Object> 灵活处理

@Select("SELECT COUNT(*) AS total, AVG(age) AS avgAge FROM user WHERE status = 1")
Map<String, Object> getUserStats();

3. 使用 <script> 标签(XML 风格)—— 不推荐用于注解

⚠️ 注解中不支持 <if>, <where> 等动态 SQL 标签。
如需动态 SQL,应使用 XML 映射文件。

4. 调试 SQL 输出

开启 MyBatis 日志,查看最终执行的 SQL:

# application.yml
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

六、最佳实践与性能优化

✅ 最佳实践

实践 说明
命名规范 自定义方法名加 Custom 后缀,如 selectByIdCustom
SQL 大小写 建议 SQL 关键字大写,字段名小写,提高可读性
**避免 SELECT *** 明确指定字段,减少网络传输
使用索引字段查询 WHERE 条件尽量使用索引字段
复杂 SQL 用 XML 超过 5 行的 SQL 建议迁移到 XML 文件

⚡ 性能优化建议

  1. 合理使用索引

    -- 确保 WHERE、JOIN、ORDER BY 字段有索引
    CREATE INDEX idx_status_create ON user(status, create_time DESC);
    
  2. 分页查询优化

    @Select("SELECT id, name FROM user WHERE status = #{status} LIMIT #{offset}, #{size}")
    List<User> selectUsersPaged(@Param("status") Integer status,
                                @Param("offset") Integer offset,
                                @Param("size") Integer size);
    
  3. 批量操作使用 foreach(需 XML)

    注解不支持 IN 子句批量,需用 XML:

    <select id="selectByIds" resultType="User">
      SELECT * FROM user WHERE id IN
      <foreach item="id" collection="ids" open="(" separator="," close=")">
        #{id}
      </foreach>
    </select>
    
  4. 连接池配置

    • 使用 HikariCP,合理设置 maximumPoolSize

七、高级技巧

技巧 1:调用数据库函数

@Select("SELECT NOW() AS currentTime, DATABASE() AS dbName FROM DUAL")
Map<String, Object> getDatabaseInfo();

技巧 2:UPSERT(MySQL)

@Insert("""
    INSERT INTO user(name, email) VALUES(#{name}, #{email})
    ON DUPLICATE KEY UPDATE name = #{name}
    """)
int insertOrUpdate(@Param("name") String name, @Param("email") String email);

技巧 3:返回主键(MySQL)

@Insert("INSERT INTO user(name) VALUES(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertWithKey(User user); // 主键会回填到 user.id

八、与 Wrapper 的对比

特性 自定义 SQL MP Wrapper
复杂查询支持 ✅ 强 ❌ 有限
性能 ✅ 高(直接执行) ⚠️ 有解析开销
安全性 ⚠️ 需防 SQL 注入 ✅ 自动防注入
可维护性 ⚠️ 分散在代码中 ✅ 集中
插件支持 ❌ 不支持自动填充等 ✅ 完全支持
动态 SQL ❌ 注解不支持 ✅ 支持

建议

  • 简单 CRUD 用 BaseMapperWrapper
  • 复杂查询、高性能场景用自定义 SQL

总结

MyBatis-Plus 的自定义 SQL 是处理复杂业务场景的利器,关键要点如下:

🔑 核心四步

  1. ✅ 在 Mapper 接口中使用 @Select, @Update 等注解
  2. ✅ 所有参数使用 @Param("name") 注解
  3. ✅ 注意字段映射、逻辑删除、自动填充等特性需手动处理
  4. ✅ 复杂 SQL 建议使用 XML 文件替代注解

掌握这些,你就能在灵活性与安全性之间找到最佳平衡,构建高性能的数据访问层。