一、核心概念
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 方法 |
四、注意事项
必须使用
@Param
注解:
当方法参数多于一个,或参数为基本类型/包装类时,必须使用@Param("name")
指定参数名。字段名映射问题:
- 数据库字段如
create_time
需映射到 Java 属性createTime
- 方案①:SQL 中使用
AS createTime
- 方案②:在
application.yml
中开启驼峰映射:mybatis-plus: configuration: map-underscore-to-camel-case: true
- 数据库字段如
不支持自动填充:
自定义 SQL 不会触发MetaObjectHandler
的insertFill
/updateFill
,需手动写入:INSERT INTO user(create_time) VALUES(NOW())
不支持逻辑删除:
若启用了@TableLogic
,自定义 SQL 需手动添加AND deleted = 0
条件。不支持多租户:
需手动添加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 文件 |
⚡ 性能优化建议
合理使用索引:
-- 确保 WHERE、JOIN、ORDER BY 字段有索引 CREATE INDEX idx_status_create ON user(status, create_time DESC);
分页查询优化:
@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);
批量操作使用
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>
连接池配置:
- 使用 HikariCP,合理设置
maximumPoolSize
- 使用 HikariCP,合理设置
七、高级技巧
技巧 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 用
BaseMapper
或Wrapper
- 复杂查询、高性能场景用自定义 SQL
总结
MyBatis-Plus 的自定义 SQL 是处理复杂业务场景的利器,关键要点如下:
🔑 核心四步:
- ✅ 在 Mapper 接口中使用
@Select
,@Update
等注解- ✅ 所有参数使用
@Param("name")
注解- ✅ 注意字段映射、逻辑删除、自动填充等特性需手动处理
- ✅ 复杂 SQL 建议使用 XML 文件替代注解
掌握这些,你就能在灵活性与安全性之间找到最佳平衡,构建高性能的数据访问层。