深浅模式
新增操作
在系统中想要完成“新增”操作,通常都绕不开三件事:
接收请求 → 写入基础信息 → 写入关联信息
比如在狼群管理系统中,当管理员录入一只新狼时,除了基础信息(名字、毛色、年龄等),还可能一并登记它过去的狩猎经历。
这意味着,新增功能不只是单表插入,而是一整个完整的业务流程。
为了实现这一流程,我们通常会按照固定的思路来拆解步骤:
- 完成准备工作,引入实体类、Mapper 接口、XML 映射文件,以及用于接收请求参数的实体类。
- 保存狼的基础信息。
- 批量保存狼的狩猎经历信息。
先搭好这条主线,后续的所有细节都围绕它展开。
准备工作
新增功能的第一步,是把基础骨架搭好。
这一步没有复杂的逻辑,目标很单纯——让系统能够接收到一条完整的“狼”的信息,并具备写入数据库的能力。
在这个案例中,一只狼的数据拆成了两部分:
wolf
—— 记录狼的基础信息;
sql
-- 狼基础信息表
create table wolf (
id int primary key auto_increment,
name varchar(50),
color varchar(20),
age int,
create_time datetime,
update_time datetime
);
hunt_expr
—— 记录它的狩猎经历信息。
sql
-- 狩猎经历表
create table hunt_expr (
id int primary key auto_increment,
wolf_id int,
region varchar(50),
begin_date date,
end_date date,
foreign key (wolf_id) references wolf(id)
);
wolf_id
是关键,它用来把一只狼和它的多条经历记录关联起来。
接下来,我们需要准备好三类内容来支撑这条新增链路:
- 实体类:用于封装请求参数,并映射到数据库表;
- Mapper 与 XML:负责执行数据库插入;
- Controller / Service:承接外部请求,编排业务逻辑。
实体类如下:
java
// Wolf.java
public class Wolf {
private Integer id;
private String name;
private String color;
private Integer age;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private List<HuntExpr> exprList; // 狩猎经历列表
}
// HuntExpr.java
public class HuntExpr {
private Integer id;
private Integer wolfId;
private String region;
private LocalDate beginDate;
private LocalDate endDate;
}
这里的
exprList
就是新增操作的关键,它能让我们在一次请求中同时带上多条狩猎经历信息。
保存数据
新增流程的核心在于两步:
- 先存基础信息
- 再批量存经历信息
关键问题在于如何拿到数据库生成的主键,并用它把经历记录一次性挂到这只狼名下。
MyBatis 提供了两种方式把主键带回来:
保存基础信息
注解方式
注解只需指定两个核心参数即可:
useGeneratedKeys = true
:开启返回主键;keyProperty = "id"
:告诉 MyBatis 把主键写回到wolf.id
字段。
java
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into wolf(name, color, age, create_time, update_time) " +
"values(#{name}, #{color}, #{age}, #{createTime}, #{updateTime})")
void insert(Wolf wolf);
XML 方式
等价的 XML 方式如下
xml
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into wolf(name, color, age, create_time, update_time)
values(#{name}, #{color}, #{age}, #{createTime}, #{updateTime})
</insert>
批量保存经历信息
基础信息插入完成后,我们就已经能拿到数据库生成的主键 ID 了:
java
Integer wolfId = wolf.getId();
接下来要做的,就是把前端接收到的,对应的多条狩猎经历,一次性写进 hunt_expr
表中:
- 遍历经历列表;
- 给每条经历补上
wolfId
; - 使用批量插入语句写进数据库。
如果一条条插入,每执行一次 SQL 就要与数据库交互一次,性能会很差。所以我们选择批量插入,只需要一次数据库交互,速度更快,也能保证同一批数据一起落地。
MyBatis 中最方便的方式就是使用 <foreach>
标签来动态拼接多条 values
。
xml
<insert id="insertBatch">
insert into hunt_expr(wolf_id, region, begin_date, end_date)
<foreach collection="exprList" item="expr" separator="," open="values">
(#{expr.wolfId}, #{expr.region}, #{expr.beginDate}, #{expr.endDate})
</foreach>
</insert>
collection
:指定遍历的集合,这里对应的是请求中携带的exprList
;item
:遍历出来的单个元素separator
:在每两条 values 之间加的分隔符open
/close
:控制整段拼接的前后结构,保证 SQL 语句最终形如values (...), (...), (...)
。
如果经历列表为空,业务层就应该直接跳过,不要让 <foreach>
拼出空 SQL。完成这一步后,一只狼和它的全部狩猎经历就正式落进数据库了。
事务处理
到目前为止,新增操作分成了两步:
- 插入基础信息(
wolf
); - 批量插入狩猎经历(
hunt_expr
)。
这两步在逻辑上是一个整体。
如果第一步失败、第二步成功,就可能出现一种非常糟糕的情况——数据库里多了一些“没有对应狼的狩猎经历”。
这会导致数据不一致,也就是所谓的脏数据。
在真实业务中,这种情况是绝对不能接受的。
我们必须确保:
- 要么两步都成功;
- 要么两步都失败,数据恢复到初始状态。
要达到这种“要么全有,要么全无”的效果,就得靠——事务。
Mysql 的事务控制
事务(Transaction)是一组操作的集合,它是一个不可分割的工作单元。这些操作要么一起提交,要么一起撤销。
一旦有任何一步失败,就会触发回滚,保证数据库数据的一致性。
默认情况下,MySQL 是“自动提交模式”:
执行一条 INSERT
、UPDATE
或 DELETE
,就会立刻生效,相当于系统在你每条 DML(数据修改语句)后面都自动执行了 commit
。
如果第二条语句执行失败,第一条已经生效了,没法再自动撤回。所以我们需要手动开启事务,把这几条操作“绑”在一起。
sql
-- 开启事务
start transaction;
-- 1. 插入基础信息
insert into wolf values (null, '灰牙', '灰色', 3, now(), now());
-- 2. 插入狩猎经历
insert into hunt_expr(wolf_id, region, begin_date, end_date)
values (last_insert_id(), '暗林', '2022-01-01', '2022-05-01'),
(last_insert_id(), '雪原', '2023-01-01', '2023-06-01');
-- 成功时提交
commit;
-- 出错时回滚
rollback;
只要在同一个事务里操作,一旦其中某一步报错,执行 rollback
就能让数据“撤回”到执行前的状态,确保不会出现孤零零的一条基础记录。
Spring 中的事务控制
在 Spring 里,我们只要在 Service 方法上加一个注解,Spring 就能自动帮我们完成事务的开启与回滚:
java
@Transactional
public void save(Wolf wolf) {
wolfMapper.insert(wolf);
Integer wolfId = wolf.getId();
List<HuntExpr> exprList = wolf.getExprList();
if (exprList != null && !exprList.isEmpty()) {
exprList.forEach(expr -> expr.setWolfId(wolfId));
huntExprMapper.insertBatch(exprList);
}
}
- 方法执行前,Spring 会自动开启事务;
- 所有操作正常完成,就自动提交;
- 如果中途抛出异常,就会自动回滚。
@Transactional
注解其实既可以加在方法上,也可以加在类上,甚至是接口上:
- 加在类上,表示类中所有公共方法都受事务管理;
- 加在方法上,只对这个方法生效。
在实际开发中,更推荐直接加在方法上,这样事务的范围最清晰,也更容易控制。
比如你只想让“新增狼”这个方法具备事务,而不影响其他查询或辅助操作,这样的粒度更合适。
事务调试
在开发过程中,我们经常需要确认事务是否真的生效,比如事务有没有成功开启、是否在预期的地方回滚。
这时,最直接的方式就是——打开事务日志。
properties
# 开启 Spring 事务管理的 debug 级别日志
logging.level.org.springframework.jdbc.support.JdbcTransactionManager=debug
开启后,控制台会清楚地打印出事务的开启、提交、回滚等关键过程,非常直观。
一旦事务出现异常,你也能从日志中快速定位问题发生在哪一步。
除了 debug
,Spring 还支持不同的日志等级。
通过调整日志等级,可以灵活控制输出信息的详细程度:
等级 | 说明 |
---|---|
error | 只输出严重错误 |
warn | 输出警告和错误 |
info | 输出基本运行信息(适合生产环境) |
debug | 输出调试信息,包括事务的开启、提交与回滚 |
trace | 输出更细节的调试信息(包括调用链路,几乎所有细节) |
日常开发调试时建议使用 debug
;
若要深入排查事务执行细节,比如多层调用间的传播行为,可以临时调高到 trace
。
日志就是你事务是否正常工作的“放大镜”。
事务进阶
事务不仅能保证“要么全成,要么全败”,还能更灵活地控制在什么情况下回滚、事务之间怎么协作。
这主要依赖两个常用属性:
rollbackFor
决定事务遇到什么异常时会回滚,适用于业务异常处理;propagation
决定事务之间的协作方式,影响的是多层业务的执行边界。
rollbackFor
Spring 默认只在遇到 RuntimeException
时才会自动回滚,
如果抛出的是普通 Exception
,事务并不会回滚。
在实际开发中,业务异常往往是自定义的 Exception
,这就需要显式指定:
java
@Transactional(rollbackFor = Exception.class)
public void saveWolf(Wolf wolf) throws Exception {
wolfMapper.insert(wolf);
throw new Exception("模拟业务异常");
}
这样,无论抛出的是运行时异常还是检查型异常,事务都会回滚。
如果你的业务中存在自定义异常,这是一个非常常用的配置。
propagation
当一个事务方法调用另一个事务方法时,Spring 需要知道它们之间该怎么协作。
最常用的就是下面两种传播行为:
REQUIRED
(默认)
如果当前有事务,就加入;没有就新建。
—— 适合主业务和子业务必须“同进同退”的场景。REQUIRES_NEW
无论外层有没有事务,都新建一个事务,互不影响。
—— 常用于记录日志、补偿操作等不希望被主业务回滚影响的逻辑。
java
@Transactional(propagation = Propagation.REQUIRED)
public void addWolf(Wolf wolf) {
wolfMapper.insert(wolf);
huntExprService.saveExpr(wolf.getExprList()); // 跟主事务绑定
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
logMapper.insert(message); // 独立事务,不跟随主事务回滚
}
这两个配置是事务控制的重点,一旦掌握,你就能在复杂业务中精确控制事务行为。
ACID 四大特性
谈到事务,必须理解它的四个核心特性,也就是经典的 ACID 模型。
这是事务可靠性的基础,也是面试中最常被问到的知识点之一。
- 原子性确保成败一体;
- 一致性保证状态不乱;
- 隔离性应对并发冲突;
- 持久性保证结果可信。
原子性(Atomicity)
事务是最小的执行单元,不可再分。
要么所有操作都执行成功,要么全部回滚,就像一根绳上的珠子,要掉就全掉。
一致性(Consistency)
事务执行前后,数据必须处于合法且一致的状态。
比如插入一只狼的基础信息和经历,要么两者都存在,要么都不存在,不能只剩一半。
隔离性(Isolation)
多个事务并发执行时,彼此之间互不干扰。
就算有多个用户同时插入、修改,也不能让操作互相“踩”到。
持久性(Durability)
事务一旦提交,数据就被永久写入数据库。
即使系统突然宕机,事务的结果也不会丢失。
评论