第 2 章 [核心] 实现转账功能与 CRUD 操作
第 2 章 [核心] 实现转账功能与 CRUD 操作
Prorise第 2 章 [核心] 实现转账功能与 CRUD 操作
摘要: 在上一章,我们成功搭建了项目并完成了首次查询。现在,是时候为我们的银行应用添加真正的业务功能了。本章将围绕核心的“转账”场景,引导您掌握 MyBatis 的 CUD (增/改/删) 操作。我们将学习并采用业界推荐的 Mapper 代理模式 进行开发,同时并列对比传统的 XML 和现代的 注解 两种 SQL 实现方式。最后,我们将深入探讨 MyBatis 中多种参数的传递机制,特别是 @Param 注解的使用,以及 #{} 与 ${} 的本质区别——这是每个 MyBatis 开发者必须掌握的关键知识点。
2.1. [模式] Mapper 代理开发模式
在上一章的测试中,我们通过以下代码获取 Mapper 并执行了查询:
1 | // Chapter 1 Test Code Snippet |
这种 sqlSession.getMapper(AccountMapper.class) 的方式,就是 MyBatis 的 Mapper 代理模式。它是官方推荐的、也是目前业界最主流的开发方式。它允许我们像调用一个普通的 Java 接口方法一样执行 SQL,而无需关心底层的实现细节。
原理点 · 面试题: 为什么我们只写 Mapper 接口而不用写实现类?MyBatis 是如何做到的?
这是因为 MyBatis 在调用 sqlSession.getMapper(YourMapper.class) 时,内部使用了 Java 的 动态代理 技术。
它会根据您提供的接口,在内存中动态地创建一个该接口的代理对象。当您调用接口中的任何方法(如 selectByActno)时,这个代理对象会拦截该调用,然后根据“接口的全限定名 + 方法名”(例如 com.example.bank.mapper.AccountMapper.selectByActno)作为唯一的 statementId,去 AccountMapper.xml 中查找并执行与之对应的 SQL 语句。这使得我们的代码可以完全面向接口编程,更加优雅和解耦。
2.2. [实践] 完成核心 CRUD
文件路径: src/main/resources/mappers/AccountMapper.xml
1 |
|
文件路径: src/main/java/com/example/bank/mapper/AccountMapper.java
1 | package com.example.bank.mapper; |
3. 编写 update 功能的测试
现在,我们在测试类中验证 update 方法是否正常工作。
关键点: 所有的数据修改操作(增、删、改)执行完毕后,默认情况下事务是 不会自动提交 的。您必须手动调用 sqlSession.commit() 来持久化更改。
文件路径: src/test/java/com/example/bank/test/AccountMapperTest.java
1 | // ... (imports) |
4. 补充 insert 与 delete 操作
同理,为了功能完整,我们为银行应用添加开户 (insert) 和销户 (delete) 功能。
- 向
AccountMapper.java添加接口方法:1
2
3
4
5// 插入一条账户记录
int insert(Account account);
// 根据账号删除记录
int deleteByActno(String actno); - 添加对应的 SQL 映射:
文件路径: src/main/resources/mappers/AccountMapper.xml
1 | <insert id="insert"> |
文件路径: src/main/java/com/example/bank/mapper/AccountMapper.java
1 |
|
2.3. [核心] 掌握参数处理
到目前为止,我们的方法都只接收一个参数。但在实际业务中,我们经常需要传递多个参数。
1. 多参数传递的问题
假设我们需要一个新功能:查询余额在某个范围内的所有账户。接口方法可能定义如下:
1 | // 这是一个有问题的定义,MyBatis 不知道哪个是 minBalance,哪个是 maxBalance |
当 MyBatis 看到这个方法时
它无法将 minBalance 和 maxBalance 这两个参数名与 XML 中的 #{minBalance} 和 #{maxBalance} 对应起来。
2. 解决方案:使用 @Param 注解
为了解决这个问题,MyBatis 提供了 @Param 注解,用于给参数“命名”。这是处理多参数时的 最佳实践。
文件路径: src/main/java/com/example/bank/mapper/AccountMapper.java (修改)
1 | import org.apache.ibatis.annotations.Param; // 引入 Param 注解 |
现在,MyBatis 就知道 minBalance 对应的是 #{min},maxBalance 对应的是 #{max}。
文件路径: src/main/resources/mappers/AccountMapper.xml (添加)
1 | <select id="selectByBalanceRange" resultType="com.example.bank.pojo.Account"> |
对我们的查询结果进行验证
1 |
|
3. 原理点 · #{} 与 ${} 的本质区别
这是 MyBatis 中一个极其重要的知识点,直接关系到应用的安全。
面试高频题: #{} 和 ${} 有什么区别?哪个会引起 SQL 注入?
#{}(占位符): 这是 推荐 的使用方式。MyBatis 在处理#{}时,会将其替换为?占位符,并使用PreparedStatement来设置参数。这种方式可以 有效防止 SQL 注入,因为用户输入的内容被视为纯粹的数据,而不是 SQL 指令的一部分。${}(拼接符): 这是 不安全 的字符串替换。MyBatis 在处理${}时,会直接将变量的 值 拼接到 SQL 语句中。如果这个值来自用户输入,将可能导致 SQL 注入 攻击。
结论: 永远优先使用 #{}。只有在极少数需要动态拼接 SQL 关键字(如表名、ORDER BY 的列名)的场景下,才考虑使用 ${},并且必须对输入源做严格的校验。
SQL 注入的危险示例:
假设我们错误地使用了 ${}:
1 | <select id="selectByActno" resultType="com.example.bank.pojo.Account"> |
如果一个恶意用户传入的 actno 是 act-001' OR '1'='1,那么拼接后的 SQL 将会是:
1 | select * from t_account where actno = 'act-001' OR '1'='1' |
这条 SQL 会绕过 where 条件,返回表中的所有数据,造成严重的数据泄露。而如果使用 #{},PreparedStatement 会将整个恶意字符串视为一个普通的值去匹配 actno,什么也查不到,从而保证了安全。







