作为MyBatis的增强工具,MyBatis-Plus在保留原生MyBatis特性的基础上,提供了更便捷的CRUD操作方式。但在实际开发中,我们经常会遇到需要自定义SQL的场景。直接在Mapper接口方法上编写SQL语句,可以避免频繁切换XML文件的麻烦,特别适合简单查询和少量复杂SQL的场景。
要在方法上直接编写SQL,首先需要确保项目基础配置正确:
@MapperScan注解,或为每个Mapper接口添加@Mapper注解@TableName指定表名(当表名与类名不一致时)java复制@Mapper
public interface UserMapper extends BaseMapper<User> {
// 方法定义将在这里展开
}
关键点:继承BaseMapper后,既可以使用MyBatis-Plus提供的通用方法,也可以自定义SQL方法,两者可以完美共存。
对于简单的静态SQL,直接使用MyBatis原生的四大注解即可:
@Select:查询操作@Insert:插入操作@Update:更新操作@Delete:删除操作每个注解内部直接编写SQL语句,参数使用#{}占位符。例如查询单个用户:
java复制@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
插入操作时,如果需要获取自增主键,需要额外添加@Options注解:
java复制@Insert("INSERT INTO user(name,age) VALUES(#{name},#{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
经验之谈:虽然MyBatis-Plus的BaseMapper已经提供了insert方法,但当需要特殊处理主键或使用数据库特有语法时,自定义SQL会更灵活。
在注解中实现动态SQL需要特别注意格式问题。所有动态标签(如if、foreach等)必须包裹在<script>标签内,否则会被当作普通文本处理。
java复制@Select("<script>" +
"SELECT * FROM user WHERE 1=1 " +
"<if test='name != null'> AND name = #{name} </if>" +
"<if test='age != null'> AND age > #{age} </if>" +
"</script>")
List<User> selectByCondition(@Param("name") String name, @Param("age") Integer age);
常见问题:Java字符串中的双引号需要转义,动态SQL中的条件判断字符串比较要格外小心,建议使用
test='name != null and name != ""'的形式。
当方法有多个参数时,必须使用@Param注解指定参数名,否则SQL中的#{}将无法正确解析:
java复制@Select("SELECT * FROM user WHERE name = #{name} AND age > #{age}")
User selectByNameAndAge(@Param("name") String name, @Param("age") int age);
对于集合参数,可以使用<foreach>标签实现IN查询:
java复制@Select("<script>" +
"SELECT * FROM user WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<User> selectByIds(@Param("ids") List<Long> ids);
当查询结果字段与实体类属性不一致时,可以使用@Results和@Result注解进行手动映射:
java复制@Select("SELECT user_id, user_name, create_time FROM t_user")
@Results({
@Result(column = "user_id", property = "id"),
@Result(column = "user_name", property = "name"),
@Result(column = "create_time", property = "createTime")
})
List<User> selectAllUsers();
优化建议:在application.yml中配置
mybatis-plus.configuration.map-underscore-to-camel-case=true可以自动将下划线命名转为驼峰命名,减少手动映射的工作量。
对于多表关联查询,注解方式同样适用,但需要注意结果映射:
java复制@Select("SELECT u.*, d.name AS deptName FROM user u LEFT JOIN department d ON u.dept_id = d.id")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "deptName", property = "deptName")
})
List<UserDTO> selectUserWithDept();
对于复杂的联表查询,建议创建一个专门的DTO来接收结果,而不是直接使用实体类。
<foreach>实现批量插入/更新java复制@Insert("<script>" +
"INSERT INTO user(name, age) VALUES " +
"<foreach collection='users' item='user' separator=','>" +
"(#{user.name}, #{user.age})" +
"</foreach>" +
"</script>")
int batchInsert(@Param("users") List<User> users);
虽然SQL写在注解中,但事务控制依然通过Spring的@Transactional实现:
java复制@Transactional
@Update("UPDATE account SET balance = balance - #{amount} WHERE id = #{fromId}")
int deductBalance(@Param("fromId") Long fromId, @Param("amount") BigDecimal amount);
@Transactional
@Update("UPDATE account SET balance = balance + #{amount} WHERE id = #{toId}")
int addBalance(@Param("toId") Long toId, @Param("amount") BigDecimal amount);
重要提示:事务注解应该加在Service层方法上,而不是Mapper层。
使用#{}占位符可以防止SQL注入,绝对不要使用字符串拼接:
java复制// 错误示范:存在SQL注入风险
@Select("SELECT * FROM user WHERE name = '" + "${name}" + "'")
List<User> selectByName(@Param("name") String name);
// 正确做法:使用#{}占位符
@Select("SELECT * FROM user WHERE name = #{name}")
List<User> selectByName(@Param("name") String name);
SQL中的特殊符号(如<, >)在XML中需要转义,但在注解中可以直接使用:
java复制// 查询年龄小于指定值的用户
@Select("SELECT * FROM user WHERE age < #{maxAge}")
List<User> selectYoungerThan(@Param("maxAge") int maxAge);
结合MyBatis-Plus的分页插件,可以轻松实现分页查询:
java复制@Select("SELECT * FROM user WHERE age > #{age}")
Page<User> selectByAge(Page<User> page, @Param("age") int age);
调用时需要先创建Page对象:
java复制Page<User> page = new Page<>(1, 10); // 第一页,每页10条
userMapper.selectByAge(page, 18);
List<User> users = page.getRecords();
Java 15+支持文本块语法,可以大幅提升长SQL的可读性:
java复制@Select("""
SELECT u.id, u.name, d.name AS deptName
FROM user u
LEFT JOIN department d ON u.dept_id = d.id
WHERE u.status = 1
AND u.create_time > #{startDate}
ORDER BY u.id DESC
""")
List<UserDeptDTO> selectActiveUsersAfterDate(@Param("startDate") LocalDate startDate);
对于重复使用的SQL片段,可以定义为常量:
java复制public interface UserSql {
String BASE_COLUMNS = "id, name, age, email, create_time";
String BASE_TABLE = "user";
String SELECT_BASE = "SELECT " + BASE_COLUMNS + " FROM " + BASE_TABLE;
}
@Select(UserSql.SELECT_BASE + " WHERE id = #{id}")
User selectById(Long id);
在开发环境开启SQL日志,方便调试:
yaml复制# application.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 简单CRUD | MP自带方法 | 零SQL,最高效 |
| 条件查询 | QueryWrapper | 类型安全,IDE支持好 |
| 简单自定义SQL | 注解 | 代码集中,无需文件切换 |
| 复杂动态SQL | XML | 标签支持完善,可读性好 |
| 多表关联查询 | 视复杂度而定 | 简单用注解,复杂用XML |
java复制// 批量更新示例
@Update("<script>" +
"<foreach collection='users' item='user' separator=';'>" +
"UPDATE user SET name=#{user.name} WHERE id=#{user.id}" +
"</foreach>" +
"</script>")
int batchUpdate(@Param("users") List<User> users);
在实际项目中,我通常会根据SQL的复杂度和变更频率来决定使用注解还是XML。对于基础且稳定的查询,注解方式更加简洁;而对于业务复杂、可能频繁调整的SQL,XML提供了更好的可维护性。特别是在团队协作中,清晰的代码组织比个人编码习惯更重要。