Mybatis - 精巧的持久层框架 - 注解开发深刻理解
Mybaits的注解开发是现代Java项目(特别是Spring Boot项目)中非常主流的开发方式。它能让你摆脱繁琐的XML文件,以一种更“Java-Native”的方式编写数据访问层,代码更简洁,开发效率更高。
Mybatis注解开发深度解析与实战
引子:为什么需要注解开发?XML不香了吗?
XML开发方式非常强大和灵活,特别是对于复杂的动态SQL。但它也有一些缺点:
- 文件繁多:每个Mapper都需要一个
.java接口文件和一个.xml映射文件,管理起来比较分散。 - 跳转不便:在IDE中,从Java方法跳转到对应的XML SQL语句,有时不如直接看代码方便。
- 对于简单SQL,显得“重”:一个简单的
SELECT * FROM user WHERE id = ?,也需要配置一整个XML文件。
注解开发正是为了解决这些问题而生。它的核心思想是:
将SQL语句直接写在Mapper接口的方法上,用注解来代替XML标签的功能。
这样做的好处是:
- 代码聚合:SQL和它对应的Java方法紧密地写在一起,一目了然。
- 文件精简:不再需要独立的
.xml文件,一个.java文件搞定一切。 - 开发高效:对于中小型项目或简单的CRUD操作,注解开发速度非常快。
当然,这是一种选择,而不是替代。在企业开发中,常常是两者结合使用:简单的、固定的SQL用注解;复杂的、需要动态拼接的SQL用XML。
第一部分:注解开发的核心注解与基础实践
1. 核心CRUD注解
Mybatis提供了一套与SQL操作对应的核心注解:
@Select: 用于执行查询操作 (SELECT)。@Insert: 用于执行插入操作 (INSERT)。@Update: 用于执行更新操作 (UPDATE)。@Delete: 用于执行删除操作 (DELETE)。
这些注解的值(value)就是一个字符串数组,里面直接填写你的SQL语句。
2. 动手实践:从XML到注解的改造
我们将以之前的UserMapper为例,一步步将其从XML方式改造为注解方式。
项目结构准备:
我们将创建一个新的Mapper接口UserAnnotationMapper.java,以示区分,但项目结构保持不变。
mybatis-cache-demo/
├── ...
└── src/
└── main/
├── java/
│ └── com/
│ └── example/
│ ├── ...
│ ├── mapper/
│ │ ├── UserMapper.java // (保留XML版本,用于对比)
│ │ └── UserAnnotationMapper.java // 【新增】我们的注解版Mapper
│ └── test/
│ └── AnnotationTest.java // 【新增】我们的注解测试类
└── resources/
├── mappers/
│ └── UserMapper.xml // (保留)
└── mybatis-config.xml
改造步骤:
-
创建
UserAnnotationMapper.java接口这个接口里,我们将用注解来定义之前在
UserMapper.xml中写的SQL。// src/main/java/com/example/mapper/UserAnnotationMapper.java package com.example.mapper; import com.example.entity.User; import org.apache.ibatis.annotations.*; public interface UserAnnotationMapper { /** * 1. 查询操作 * @Select 注解,将SQL语句直接写在注解的值里 * Mybatis会自动将方法的参数(id)与SQL中的#{id}进行绑定 */ @Select("SELECT * FROM user WHERE id = #{id}") User findById(Integer id); /** * 2. 插入操作 * @Insert 注解 * @Options 注解可以配置一些额外选项,比如获取自增主键 * - useGeneratedKeys = true: 表示要获取数据库生成的键(通常是自增ID) * - keyProperty = "id": 将获取到的键值,设置到传入的参数对象(user)的id属性上 */ @Insert("INSERT INTO user(username, password) VALUES(#{username}, #{password})") @Options(useGeneratedKeys = true, keyProperty = "id") int insertUser(User user); /** * 3. 更新操作 * @Update 注解 * 对于多个参数,推荐使用 @Param 注解为每个参数命名,SQL中通过名字引用 */ @Update("UPDATE user SET username = #{newUsername} WHERE id = #{id}") int updateUsername(@Param("id") Integer id, @Param("newUsername") String newUsername); /** * 4. 删除操作 * @Delete 注解 */ @Delete("DELETE FROM user WHERE id = #{id}") int deleteById(Integer id); } -
在
mybatis-config.xml中注册新的Mapper接口对于注解开发的Mapper,我们不再使用
<mapper resource="...">来指向XML文件,而是使用<mapper class="...">来直接指向Java接口。<!-- src/main/resources/mybatis-config.xml --> <mappers> <!-- 保留XML方式的注册 --> <mapper resource="mappers/UserMapper.xml"/> <!-- 【新增】注解方式的注册 --> <mapper class="com.example.mapper.UserAnnotationMapper"/> </mappers>Mybatis会自动识别接口上的注解,并为它们生成动态代理,无需XML文件。
-
创建
AnnotationTest.java进行测试// src/main/java/com/example/test/AnnotationTest.java package com.example.test; import com.example.entity.User; import com.example.mapper.UserAnnotationMapper; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class AnnotationTest { public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 使用try-with-resources, autoCommit设置为false,方便手动控制事务 try (SqlSession session = sqlSessionFactory.openSession(false)) { UserAnnotationMapper mapper = session.getMapper(UserAnnotationMapper.class); // --- 测试查询 --- System.out.println("--- Testing @Select ---"); User foundUser = mapper.findById(1); System.out.println("Found User: " + foundUser); // --- 测试插入 --- System.out.println("\n--- Testing @Insert ---"); User newUser = new User(); newUser.setUsername("annotationUser"); newUser.setPassword("anno123"); System.out.println("Before insert, newUser id: " + newUser.getId()); mapper.insertUser(newUser); System.out.println("After insert, newUser id (auto-generated): " + newUser.getId()); // --- 测试更新 --- System.out.println("\n--- Testing @Update ---"); int updatedRows = mapper.updateUsername(newUser.getId(), "updatedAnnotationUser"); System.out.println("Updated rows: " + updatedRows); User updatedUser = mapper.findById(newUser.getId()); System.out.println("After update: " + updatedUser); // --- 测试删除 --- System.out.println("\n--- Testing @Delete ---"); int deletedRows = mapper.deleteById(newUser.getId()); System.out.println("Deleted rows: " + deletedRows); // 因为我们openSession时是手动提交,所以可以选择提交或回滚 // 如果想让数据库真正发生改变,就取消这行注释 session.commit(); // 如果只是测试,不想污染数据库,就用回滚 // session.rollback(); } } }
3. 运行与分析
运行AnnotationTest.java,你会看到所有CRUD操作都通过注解成功执行了。特别注意@Insert的测试中,通过@Options注解,我们成功获取了数据库自增的ID并设置回了newUser对象中。
第二部分:进阶注解与企业级实践
简单的CRUD用注解很爽,但如果遇到列名和属性名不匹配、一对多/多对一关联查询等复杂情况,注解还能胜任吗?答案是可以的,Mybatis提供了一套更强大的注解来处理这些场景。
1. @Results 和 @Result:解决列名与属性名不匹配
这套注解完全等同于XML中的<resultMap>。
@Results: 相当于<resultMap>标签,它是一个容器,可以包含多个@Result。它有一个id属性,可以被其他查询引用。@Result: 相当于<result>或<id>标签,用于定义单个列到属性的映射。column: 数据库的列名。property: Java对象的属性名。id = true: 表明这是主键,相当于<id>标签。
企业级例子:数据库列名带下划线,Java属性是驼峰命名
假设我们的user表,列名是user_name和user_pass,而User实体类的属性是username和password。
// 在UserAnnotationMapper.java中新增方法
public interface UserAnnotationMapper {
// ... 其他方法 ...
/**
* 使用 @Results 和 @Result 手动映射列名和属性名
*/
@Results(id = "userResultMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "username", column = "user_name"),
@Result(property = "password", column = "user_pass")
})
@Select("SELECT id, user_name, user_pass FROM user WHERE id = #{id}")
User findByIdWithResultMap(Integer id);
/**
* 使用 @ResultMap 注解复用上面定义好的映射关系
*/
@ResultMap("userResultMap") // 通过id引用上面定义好的@Results
@Select("SELECT id, user_name, user_pass FROM user")
List<User> findAll();
}
注意:虽然可以手动映射,但在企业开发中,更推荐的做法是在mybatis-config.xml中开启驼峰命名自动映射,这样就不需要为每个查询都写@Results了。
<setting name="mapUnderscoreToCamelCase" value="true"/>
2. @One 和 @Many:处理关联查询
这套注解完全等同于XML中的<association>和<collection>,用于处理一对一和一对多关系。
企业级例子:查询用户及其所有订单(一对多)
-
准备新的实体类
Order.java和修改User.java@Data public class Order implements Serializable { private Integer orderId; private String orderName; private Integer userId; // 外键 } @Data public class User implements Serializable { private Integer id; private String username; // ... private List<Order> orders; // 一个用户有多个订单 } -
在
UserAnnotationMapper中定义关联查询public interface UserAnnotationMapper { // ... @Select("SELECT * FROM `order` WHERE user_id = #{userId}") List<Order> findOrdersByUserId(Integer userId); @Select("SELECT * FROM user WHERE id = #{id}") @Results({ @Result(property = "id", column = "id", id = true), @Result(property = "username", column = "username"), // @Many注解处理一对多关系 // - property = "orders": 对应User类中的orders属性 // - select = "...findOrdersByUserId": 指定一个查询方法,它会根据主查询的结果(这里的id)作为参数去执行 // - column = "id": 将主查询的id列作为参数,传递给上面的select方法 @Result( property = "orders", javaType = List.class, column = "id", many = @Many(select = "com.example.mapper.UserAnnotationMapper.findOrdersByUserId") ) }) User findUserWithOrders(Integer id); }工作流程:当调用
findUserWithOrders(1)时,Mybatis会:- 执行主查询
SELECT * FROM user WHERE id = 1。 - 拿到结果中的
id值(=1)。 - 将这个
id值作为参数,去调用findOrdersByUserId(1)方法。 - 将
findOrdersByUserId返回的订单列表,设置到User对象的orders属性中。 - 最后返回完整的
User对象。
- 执行主查询
3. @SelectProvider: 注解方式的动态SQL
注解的value属性只能写固定的SQL字符串,如果想实现动态SQL怎么办?Mybatis提供了@SelectProvider(以及@InsertProvider等)注解。
你需要创建一个Provider类,在其中编写一个返回String(即SQL语句)的Java方法。这个方法可以接收参数,并在Java代码中用if-else、StringBuilder等逻辑来动态构建SQL。
例子:动态搜索用户
// 1. 创建一个Provider类
public class UserSqlProvider {
// 方法必须是public static,返回String
public String findUserByCondition(Map<String, Object> params) {
// 使用Mybatis内置的SQL构建器,比StringBuilder更优雅
return new SQL() {{
SELECT("*");
FROM("user");
if (params.get("username") != null) {
WHERE("username like #{username}");
}
if (params.get("email") != null) {
WHERE("email = #{email}");
}
}}.toString();
}
}
// 2. 在Mapper接口中使用它
public interface UserAnnotationMapper {
// ...
@SelectProvider(type = UserSqlProvider.class, method = "findUserByCondition")
List<User> findUserByCondition(Map<String, Object> params);
}
思考:虽然Provider可以实现动态SQL,但其逻辑写在Java代码中,不如XML的动态SQL标签直观。因此,对于复杂的动态SQL,业界普遍认为XML是更好的选择。
总结:注解 VS XML,如何选择?
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 简单的CRUD | 注解 | 快速、简洁、代码聚合 |
| 列名与属性名不匹配 | 注解 (@Results) 或 全局配置 |
注解灵活,全局配置一劳永逸 |
| 简单的关联查询 | 注解 (@One, @Many) |
对于清晰的一对一、一对多关系,注解足够清晰 |
| 复杂的动态SQL | XML | XML标签的可读性和组合能力远超Java代码拼接 |
| 超长、复杂的SQL | XML | 避免在Java注解里写一个超长的、难以阅读的SQL字符串 |
| SQL需要后期优化和维护 | XML | DBA或后端开发者可以不改Java代码,直接优化XML中的SQL |
企业级开发的黄金法则:
使用注解处理简单、固定的SQL,让开发飞起来;使用XML处理复杂、动态的SQL,让维护和优化更从容。两者是互补的战友,而不是互斥的敌人。
现在,你可以开始动手把之前的例子用注解重写一遍,并尝试一下关联查询和动态SQL Provider,亲身体会这两种开发模式的异同和优劣。

浙公网安备 33010602011771号