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

改造步骤:

  1. 创建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);
    }
    
  2. 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文件。

  3. 创建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_nameuser_pass,而User实体类的属性是usernamepassword

// 在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>,用于处理一对一和一对多关系。

企业级例子:查询用户及其所有订单(一对多)

  1. 准备新的实体类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; // 一个用户有多个订单
    }
    
  2. 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会:

    1. 执行主查询SELECT * FROM user WHERE id = 1
    2. 拿到结果中的id值(=1)。
    3. 将这个id值作为参数,去调用findOrdersByUserId(1)方法。
    4. findOrdersByUserId返回的订单列表,设置到User对象的orders属性中。
    5. 最后返回完整的User对象。

3. @SelectProvider: 注解方式的动态SQL

注解的value属性只能写固定的SQL字符串,如果想实现动态SQL怎么办?Mybatis提供了@SelectProvider(以及@InsertProvider等)注解。

你需要创建一个Provider类,在其中编写一个返回String(即SQL语句)的Java方法。这个方法可以接收参数,并在Java代码中用if-elseStringBuilder等逻辑来动态构建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,亲身体会这两种开发模式的异同和优劣。

posted @ 2025-07-02 17:17  清明雨上~  阅读(48)  评论(0)    收藏  举报