MyBatis注解开发实战:从基础CRUD到动态SQL与混合使用策略
1. 从XML到注解:为什么我选择在项目中混合使用
如果你和我一样,是从MyBatis的XML配置时代一路走过来的开发者,那么第一次看到满篇的@Select、@Insert注解时,心里多半会犯嘀咕:这玩意儿能行吗?XML多清晰啊,SQL和Java代码分离,维护起来也方便。我最初也是这么想的,直到在一个快速迭代、需求频繁变动的内部工具项目中,我被XML文件之间来回切换和繁琐的namespace+id匹配搞得有点烦躁,才决定认真试试注解开发。
几年用下来,我的结论是:MyBatis的注解开发,绝不是为了取代XML,而是一种强有力的补充和特定场景下的最优解。它特别适合那些SQL逻辑相对简单、变动频繁、或者你希望DAO层接口能“自成一体”、减少文件跳转的场合。比如,管理后台的CRUD接口、简单的报表查询、或者是一些原型验证阶段的功能。当你看到一个接口方法,同时就能看到它要执行的SQL,这种“所见即所得”的体验,在开发调试阶段效率提升非常明显。
当然,网上很多教程只告诉你注解怎么用,却很少说清楚什么时候用、用了有什么坑。这篇内容,我就结合自己趟过的路,把MyBatis注解开发从入门到进阶,再到生产环境下的混合使用策略,掰开揉碎了讲清楚。我们会涵盖基础的CRUD、复杂的结果映射、动态SQL、以及二级缓存等核心主题,目标就是让你看完后,不仅能写出注解代码,更能做出合适的技术选型。
2. 环境搭建与基础CRUD注解实战
开始写注解之前,得先把场子搭起来。这里假设你已经在用Spring Boot,整合MyBatis无非就是加依赖、配数据源。关键点在于,要确保MyBatis能扫描到你的Mapper接口。
2.1 核心依赖与配置要点
在你的pom.xml里,需要的是mybatis-spring-boot-starter。版本选择上,我建议直接上较新的稳定版,比如3.0.x以上。新版本对注解的支持更完善,也修复了不少历史遗留问题。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> <!-- 示例版本,请使用最新稳定版 --> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>在application.yml中,基础的数据库配置必不可少。但关于MyBatis本身,有一个配置项对注解开发至关重要:
mybatis: configuration: map-underscore-to-camel-case: true # 强烈建议开启!下划线转驼峰,省去大量@Result映射开启这个配置后,数据库字段user_name会自动映射到Java实体属性userName上,对于简单的查询,你甚至可以不写任何@Result映射,非常省事。这是很多新手会忽略的一个高效技巧。
2.2 核心CRUD注解逐一看
假设我们有一个User实体和对应的UserMapper接口。下面我们看最常用的四个注解。
@Select:查询的基石
@Mapper public interface UserMapper { @Select("SELECT id, user_name, age, email FROM user WHERE id = #{id}") User selectById(Long id); @Select("SELECT * FROM user WHERE age > #{minAge}") List<User> selectByMinAge(Integer minAge); }注意:
SELECT *在注解里要谨慎使用。如果数据库表字段顺序或数量发生变化,而你的实体类没及时更新,可能会导致映射错误。显式列出字段是更稳妥的做法,尤其是在生产环境。
@Insert:插入数据与返回主键
插入操作简单,但如何获取数据库生成的主键(比如自增ID)是个关键点。
@Insert("INSERT INTO user (user_name, age, email) VALUES (#{userName}, #{age}, #{email})") @Options(useGeneratedKeys = true, keyProperty = "id") // 关键在这里 int insertUser(User user);@Options注解的useGeneratedKeys = true告诉MyBatis使用JDBC的getGeneratedKeys方法来获取数据库生成的主键。keyProperty = "id"指定将这个值回填到参数对象user的id属性中。执行完这个方法后,user.getId()就能拿到插入的ID了。
@Update 与 @Delete:更新与删除
这两个注解相对直接。
@Update("UPDATE user SET email = #{email} WHERE id = #{id}") int updateEmailById(@Param("id") Long id, @Param("email") String email); @Delete("DELETE FROM user WHERE id = #{id}") int deleteById(Long id);这里引入了@Param注解。当方法有多个参数时,你必须使用@Param给每个参数命名,在SQL中通过#{参数名}来引用。如果只有一个参数且是基本类型/String,可以不用@Param,但为了清晰,我习惯都加上。
2.3 关于@Param注解的深度理解
@Param的作用不仅仅是解决多参数问题。它本质上是为参数创建了一个在SQL上下文中可用的名字。即使你只有一个复杂对象参数,有时也需要它。
// 场景:模糊查询,参数是一个User对象,但我们只想用其中的name属性进行模糊匹配 @Select("SELECT * FROM user WHERE user_name LIKE CONCAT('%', #{user.name}, '%')") List<User> selectByUserCondition(@Param("user") User user);如果没有@Param("user"),在SQL中直接写#{name}是取不到值的,因为MyBatis不知道name属性属于哪个参数对象。通过@Param指定后,就可以用#{user.name}这种OGNL表达式来访问对象属性了。这是处理复杂查询条件时的一个实用模式。
3. 解决复杂映射:@Results与@ResultMap
基础CRUD的SQL简单,映射也简单。但遇到查询结果包含关联对象、集合,或者字段名与属性名无法通过驼峰规则自动映射时,就需要@Results和@ResultMap出场了。
3.1 处理字段名不对应与简单关联
假设我们查询用户及其所属部门(假设部门信息在user表中有dept_id和dept_name字段,但实体类中是deptId和deptName)。
@Select("SELECT u.id, u.user_name, u.dept_id, d.name as dept_name FROM user u LEFT JOIN department d ON u.dept_id = d.id WHERE u.id = #{id}") @Results({ @Result(column = "id", property = "id", id = true), // id=true表示这是主键 @Result(column = "user_name", property = "userName"), @Result(column = "dept_id", property = "deptId"), @Result(column = "dept_name", property = "deptName") }) User selectUserWithDeptById(Long id);@Results注解可以理解为一个结果映射的集合。@Result中的column是数据库查询结果的列名(或别名),property是Java实体类的属性名。当开启了map-underscore-to-camel-case后,像user_name到userName的映射其实可以省略,但这里为了演示完整性还是写了出来。对于主键,建议明确标记id = true,这有助于提高性能(尤其是在嵌套查询时)。
3.2 使用@ResultMap复用映射规则
如果同一个映射规则在多个@Select方法中都要用到,每次都写一遍@Results太冗余了。这时可以用@ResultMap。
首先,在一个方法上定义@Results,并指定一个唯一的id:
@Select("SELECT * FROM user WHERE id = #{id}") @Results(id = "userResultMap", value = { @Result(column = "id", property = "id", id = true), @Result(column = "user_name", property = "userName"), @Result(column = "dept_id", property = "deptId") }) User selectByIdForMap(Long id);然后,在其他方法中,通过@ResultMap来引用这个映射规则:
@Select("SELECT * FROM user WHERE age = #{age}") @ResultMap("userResultMap") // 直接复用上面定义的映射 List<User> selectByAge(Integer age);这种方式极大地减少了重复代码。但请注意,@ResultMap引用的id必须在同一个Mapper接口内定义。它无法跨Mapper接口引用,这是注解开发的一个局限性。
3.3 一对多、多对一关联映射的挑战与方案
这是注解开发最棘手的部分之一。在XML中,我们可以使用<collection>和<association>轻松定义复杂关联。在注解中,MyBatis提供了@One和@Many注解来模拟,但用起来颇为繁琐。
例如,查询用户及其所有订单(一对多)。
// OrderMapper.java @Mapper public interface OrderMapper { @Select("SELECT * FROM `order` WHERE user_id = #{userId}") List<Order> selectByUserId(Long userId); } // UserMapper.java @Select("SELECT * FROM user WHERE id = #{id}") @Results({ @Result(id = true, column = "id", property = "id"), @Result(column = "user_name", property = "userName"), @Result(column = "id", property = "orderList", // 将用户id作为参数,传递给另一个查询 many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId")) }) User selectUserWithOrders(Long id);@Many注解的select属性指定了另一个Mapper方法的全限定名(或同一Mapper内的方法名)。MyBatis会先执行主查询SELECT * FROM user,然后对每一条结果,再执行一次OrderMapper.selectByUserId(user.id),最后将查询到的List<Order>集合设置到User对象的orderList属性中。
这种方式的优缺点非常明显:
- 优点:逻辑清晰,直接利用已有的Mapper方法,符合SQL直观思维。
- 缺点:容易产生N+1查询问题。如果主查询返回10个用户,那么就会产生1(主查询)+10(查询订单)=11次数据库查询,性能隐患巨大。对于数据量大的场景,这是不可接受的。
因此,对于复杂的关联查询,我个人的强烈建议是:不要在注解里硬扛,回归XML写一个联表查询才是正道。你可以用@Select注解执行一个联表查询SQL,然后通过@Results手动映射每一个字段(包括嵌套对象的字段),但这会让@Results变得极其庞大和难以维护。此时,XML的清晰和强大就体现出来了。这也引出了我的核心观点:注解与XML混合使用,各取所长。
