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,亲身体会这两种开发模式的异同和优劣。
Mybatis - 精巧的持久层框架 - 注解开发深刻理解的更多相关文章
- MyBatis(四):自定义持久层框架优化
本文所有代码已上传至码云:https://gitee.com/rangers-sun/mybatis 修改IUserDao.UserMapper.xml package com.rangers; im ...
- MyBatis(三):自定义持久层框架实现
代码已上传至码云:https://gitee.com/rangers-sun/mybatis 新建Maven工程 架构端MyPersistent.使用端MyPersistentTest,使用端引入架构 ...
- MyBatis(二):自定义持久层框架思路分析
使用端 引入架构端Maven依赖 SqlMapConfig.xml-数据库配置信息(数据库连接jar名称.连接URL.用户名.密码),引入Mapper.xml的路径 XxMapper.xml-SQL配 ...
- Mybatis详解系列(一)--持久层框架解决了什么及如何使用Mybatis
简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...
- 开源顶级持久层框架——mybatis(ibatis)——day02
mybatis第二天 高级映射 查询缓存 和spring整合 课程复习: mybatis是什么? mybatis是一个持久层框架,mybatis ...
- Mybatis学习之自定义持久层框架(二) 自定义持久层框架设计思路
前言 上一篇文章讲到了JDBC的基本用法及其问题所在,并提出了使用Mybatis的好处,那么今天这篇文章就来说一下该如何设计一个类似Mybatis这样的持久层框架(暂时只讲思路,具体的代码编写工作从下 ...
- spring-boot+mybatis开发实战:如何在spring-boot中使用myabtis持久层框架
前言: 本项目基于maven构建,使用mybatis-spring-boot作为spring-boot项目的持久层框架 spring-boot中使用mybatis持久层框架与原spring项目使用方式 ...
- 持久层框架之MyBatis
1.mybatis框架介绍: MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并 ...
- Java数据持久层框架 MyBatis之背景知识三
摘录自:http://www.cnblogs.com/lcngu/p/5437281.html 对于MyBatis的学习而言,最好去MyBatis的官方文档:http://www.mybatis.or ...
- Java数据持久层框架 MyBatis之背景知识二
对于MyBatis的学习而言,最好去MyBatis的官方文档:http://www.mybatis.org/mybatis-3/zh/index.html 对于语言的学习而言,马上上手去编程,多多练习 ...
随机推荐
- Redis 过期键删除和内存淘汰策略【Redis 系列之四】
〇.前言 对于 Redis 服务器来说,内存资源非常宝贵,如果一些过期键一直不被删除,就会造成资源浪费. 那么,本文将结合博主收集的资料,简单介绍下过期键删除.内存淘汰两个策略,仅供参考. 博主 Re ...
- 抽象类的注意事项、abstract关键字的冲突--java进阶day02
1.注意事项 1.抽象类不允许创建对象 2.抽象类存在构造方法 3.抽象类中可以存在普通成员方法 4.抽象类的子类存在两种处理方式 第一种不多解释,主要讲第二种,子类继承了抽象类,相当于子类里面有了抽 ...
- C#元数据的概念,以及一个使用了lambda表达式的简单例子
先看一个例子 假设你写了一个 C# 类库 MathUtils.dll: public class Calculator { public int Add(int a, int b) => a + ...
- WebKit Inside: 渲染树
经过CSS的匹配,就要进入渲染树的构建. 渲染树也叫RenderObject树,因为渲染树上每一个节点,都是RenderObject的子类. 首先来看一下RenderObject的继承类图. 1 Re ...
- rabbitmq的消息的有顺序性
一.rabbitmq:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点:或者就一个queue但是对应一个consumer,然后这个consumer内部用内 ...
- Avalnoia跨平台实战记录(一),Avalonia初始化
前言: 记录一下小菜鸟程序员从WPF一知半解转向Avalonia跨平台桌面端开发的一点记录和感想,我个人是比较喜欢用.NET来开发的,当然,这也和我的技术栈有很大关系,本人只是从大专出来的,在学校里学 ...
- python获取指定文件夹内文件名称
比如下图,文件夹内有若干文件,且文件夹路径:C:\Users\Administrator\Desktop\2147\1024 4行代码,解决问题 import os path = "C:\\ ...
- Java编程之容器类
一.ArrayList 1.创建ArrayList对象 ArrayList<String> arr=new ArrayList<>(); //添加<>的为泛型 // ...
- 2025dsfz集训Day9:树状数组、LCA、RMQ
Day8 I:树状数组 \[Designed\ By\ FrankWkd\ -\ Luogu@Lwj54joy,uid=845400 \] \[特别感谢 此次课的主讲.图源侵删 \] 后记:关于本文的 ...
- jdbc写一个访问数据库的工具类
操作的工具类 package com.zjw.jdbc2; /** * jdbc操作的工具类 * @author Administrator * */ import java.sql.Connecti ...