在 Spring 中,使用 @Transactional
注解管理事务,可以确保多个数据库操作在同一个事务中进行。在 @Transactional
注解的方法中,如果要对两个表执行批量插入操作,并使用 MyBatis 的 BATCH 执行器类型的 SqlSession
,可以通过自定义获取 SqlSession
来实现。
关键点:
@Transactional
注解:用于保证两个批量插入操作在同一个事务中执行,且只会在事务提交时才将数据持久化到数据库。- BATCH 执行器类型的
SqlSession
:通过指定ExecutorType.BATCH
来执行批量操作。 - 手动触发批处理 :在合适的时机调用
sqlSession.flushStatements()
来执行批量操作,并在事务最后提交。
示例代码
1. Service 层代码:使用 @Transactional
和 BATCH
模式
java
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class BatchInsertService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
private static final int BATCH_SIZE = 500;
// 事务管理注解,确保方法内的操作在同一个事务中
@Transactional
public void batchInsertUsersAndOrders(List<User> users, List<Order> orders) {
// 获取批处理模式的 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper userBatchMapper = sqlSession.getMapper(UserMapper.class);
OrderMapper orderBatchMapper = sqlSession.getMapper(OrderMapper.class);
// 批量插入用户数据
for (int i = 0; i < users.size(); i++) {
userBatchMapper.insertUser(users.get(i));
// 达到批次大小时,手动触发批处理执行
if (i % BATCH_SIZE == 0 && i != 0) {
sqlSession.flushStatements(); // 执行批量插入
}
}
// 批量插入订单数据
for (int i = 0; i < orders.size(); i++) {
orderBatchMapper.insertOrder(orders.get(i));
// 达到批次大小时,手动触发批处理执行
if (i % BATCH_SIZE == 0 && i != 0) {
sqlSession.flushStatements(); // 执行批量插入
}
}
// 手动提交所有批量操作
sqlSession.flushStatements(); // 执行剩余的批处理
sqlSession.commit(); // 提交事务
}
}
}
2. Mapper 层代码:定义单条插入方法
UserMapper
和 OrderMapper
都是 MyBatis 的 Mapper 接口,定义批量插入的单条 SQL 操作。
UserMapper.java
java
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
// 单条插入用户数据
@Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")
void insertUser(User user);
}
OrderMapper.java
java
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper {
// 单条插入订单数据
@Insert("INSERT INTO orders (order_id, user_id, product_id) VALUES (#{orderId}, #{userId}, #{productId})")
void insertOrder(Order order);
}
关键点解释
-
@Transactional
注解:- 确保整个方法
batchInsertUsersAndOrders
在同一个事务中执行。如果中途出现异常,所有的插入操作都会回滚,不会部分提交。
- 确保整个方法
-
ExecutorType.BATCH
:- 使用
ExecutorType.BATCH
,MyBatis 将 SQL 语句批量缓存起来,直到flushStatements()
被调用时才会实际执行。
- 使用
-
flushStatements()
:- 手动触发批处理操作。每当批次达到一定大小(如 500 条记录),或者在所有数据插入完毕时,调用该方法来执行批量插入。
-
sqlSession.commit()
:- 在
SqlSession
中的批处理完成后,显式调用commit()
提交事务。如果不调用commit()
,即使flushStatements()
执行,数据也不会最终提交到数据库。
- 在
-
批次大小:
- 批处理操作是按一定大小来执行的。这里设置每 500 条数据执行一次批处理,防止批处理缓存过大导致内存占用问题。
总结
- 事务控制 :
@Transactional
确保了两个批量操作都在同一个事务中进行。 - 批量插入 :通过
ExecutorType.BATCH
执行批量插入,使用sqlSession.flushStatements()
和commit()
来控制批处理的执行和事务提交。 - 性能优化:批处理可以减少数据库交互次数,显著提升批量插入性能。
通过这种方式,你可以在 @Transactional
方法中对两个表执行批量插入,并在一个事务中提交所有操作,从而保证数据的一致性和批处理性能。
将 batchInsertUsersAndOrders
方法拆成两个不同的 SqlSession
来执行批量插入,是一种控制粒度更细的方式,可以独立地对 User
和 Order
执行插入操作,甚至可以选择独立提交每个操作的事务。如果仍然需要它们在同一个事务中执行,只需确保使用 @Transactional
注解将这两个 SqlSession
操作包含在同一个事务上下文中。
下面是拆分成两个 SqlSession
来执行的示例:
拆分后的代码结构
java
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class BatchInsertService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
private static final int BATCH_SIZE = 500;
// 事务管理注解,确保两个批量操作在同一个事务中
@Transactional
public void batchInsertUsersAndOrders(List<User> users, List<Order> orders) {
batchInsertUsers(users); // 使用一个 SqlSession 执行用户批量插入
batchInsertOrders(orders); // 使用另一个 SqlSession 执行订单批量插入
}
// 使用一个 SqlSession 批量插入用户
private void batchInsertUsers(List<User> users) {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper userBatchMapper = sqlSession.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
userBatchMapper.insertUser(users.get(i));
if (i % BATCH_SIZE == 0 && i != 0) {
sqlSession.flushStatements(); // 批量提交
}
}
sqlSession.flushStatements(); // 提交剩余的批处理
sqlSession.commit(); // 提交事务
}
}
// 使用另一个 SqlSession 批量插入订单
private void batchInsertOrders(List<Order> orders) {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
OrderMapper orderBatchMapper = sqlSession.getMapper(OrderMapper.class);
for (int i = 0; i < orders.size(); i++) {
orderBatchMapper.insertOrder(orders.get(i));
if (i % BATCH_SIZE == 0 && i != 0) {
sqlSession.flushStatements(); // 批量提交
}
}
sqlSession.flushStatements(); // 提交剩余的批处理
sqlSession.commit(); // 提交事务
}
}
}
关键点解释:
-
两个独立的
SqlSession
:batchInsertUsers
和batchInsertOrders
使用了各自独立的SqlSession
。这意味着每个SqlSession
负责管理自己的数据库连接和批处理操作。
-
同一个事务:
- 尽管这两个方法使用了不同的
SqlSession
,因为在方法上使用了@Transactional
注解,Spring 会确保这两个SqlSession
操作仍然在同一个事务中执行。如果一个插入失败,整个事务会回滚,两个表的操作都不会被提交。
- 尽管这两个方法使用了不同的
-
flushStatements()
和commit()
:- 每个
SqlSession
负责自己批处理的提交。flushStatements()
确保在批量插入操作中定期执行提交,防止内存溢出或SQL语句过长。 commit()
方法会提交该SqlSession
中的事务,但由于使用了@Transactional
,整个事务是在 Spring 的控制下提交的,commit()
相当于在每个SqlSession
中确认所有插入操作。
- 每个
-
为什么拆分
SqlSession
:- 这种拆分可以让你更灵活地控制不同表的批量操作,特别是当批量操作非常大或者有复杂的批处理逻辑时,拆分成多个
SqlSession
能更好地控制数据库资源。
- 这种拆分可以让你更灵活地控制不同表的批量操作,特别是当批量操作非常大或者有复杂的批处理逻辑时,拆分成多个
拆分成两个 SqlSession
的优缺点
优点:
- 更灵活的控制:你可以分别控制不同表的插入操作。如果某个表的批量插入出问题,仍然可以选择回滚或处理其他表的数据。
- 内存控制 :对于大批量数据,拆分可以减少内存压力。每个
SqlSession
的批处理缓存更容易管理,减少溢出风险。 - 可扩展性:当表的数据结构和逻辑独立时,拆分可以让代码更清晰,也更容易扩展。
缺点:
- 更多的复杂性 :每个
SqlSession
都需要手动管理其批量操作和事务提交。如果业务逻辑要求非常强的原子性(所有操作要么成功要么失败),需要额外小心确保事务的一致性。
总结
通过拆分成两个 SqlSession
,你可以对批量插入操作进行更灵活的控制,同时仍然可以利用 @Transactional
来确保两个表的操作在同一个事务中执行。
sqlsession.flushStatements()的作用
仅执行 sqlSession.commit()
而不执行 sqlSession.flushStatements()
是可以的,但是这样做有一些潜在的影响,需要根据具体情况来判断是否合适。
1. sqlSession.commit()
vs sqlSession.flushStatements()
-
sqlSession.flushStatements()
:这是 MyBatis 提供的一个方法,用于在批量操作时将缓存中的 SQL 语句立即发送到数据库执行。它不会提交事务,而是执行批量缓存的 SQL 语句。你可以在批处理过程中多次调用它,确保 SQL 不会一次性缓存太多。 -
sqlSession.commit()
:这个方法不仅会执行所有的批处理操作,还会提交事务,意味着你在这个SqlSession
中所有的数据库更改都会被提交并永久生效。如果调用commit()
时尚未调用flushStatements()
,MyBatis 会自动在commit()
时先执行flushStatements()
,然后提交事务。
2. 是否可以只使用 commit()
而不调用 flushStatements()
是可以的,因为在调用 commit()
的时候,MyBatis 会自动执行批处理操作 ,即它会自动调用 flushStatements()
将所有缓存的 SQL 语句发送到数据库。
关键点:
-
自动触发
flushStatements()
:当你调用sqlSession.commit()
时,MyBatis 会自动先将缓存的 SQL 语句通过flushStatements()
发送到数据库,然后再提交事务。因此,如果你直接调用commit()
,它实际上已经隐式地执行了flushStatements()
。 -
事务提交 :
commit()
不仅会将缓存的 SQL 发送到数据库,还会提交事务。如果你的整个批处理只需要在最后一次执行所有 SQL 并提交事务,那调用commit()
就足够了。
3. 何时需要显式调用 flushStatements()
你显式调用 flushStatements()
的场景通常是在你需要对批处理的执行进行更细粒度的控制时,比如以下情况:
-
大批量操作 :如果你正在批量插入大量数据,例如几千甚至上万条记录,可能希望在处理一部分数据后就将它们发送到数据库,而不是一次性缓存所有的 SQL。这可以防止内存占用过大或 SQL 语句缓存溢出的问题。显式调用
flushStatements()
允许你在批次内分阶段执行 SQL。 -
逐步执行批次:在处理大数据集时,可以按批次将 SQL 语句发送到数据库,而不必等到整个操作结束再统一提交。
示例:
java
for (int i = 0; i < users.size(); i++) {
userMapper.insertUser(users.get(i));
if (i % BATCH_SIZE == 0 && i != 0) {
sqlSession.flushStatements(); // 手动触发批处理
}
}
sqlSession.commit(); // 提交事务
在这个例子中,flushStatements()
用于分批执行插入操作,确保不会一次性缓存过多的 SQL 语句,而最后的 commit()
用于提交整个事务。
4. 仅使用 commit()
的场景
如果数据量相对较小(例如几百条记录以内),缓存的 SQL 语句不会占用太多资源,那么你可以直接调用 sqlSession.commit()
,而不必担心显式调用 flushStatements()
。MyBatis 会在提交时自动处理缓存的 SQL。
示例:
java
for (User user : users) {
userMapper.insertUser(user);
}
sqlSession.commit(); // 提交事务并自动执行批量操作
在这个例子中,你不需要手动调用 flushStatements()
,因为 commit()
会自动执行缓存的 SQL 并提交事务。
5. 总结
-
可以不调用
flushStatements()
:如果你只在最后调用sqlSession.commit()
,MyBatis 会自动在提交前执行所有缓存的 SQL 语句,相当于隐式调用了flushStatements()
。 -
什么时候需要调用
flushStatements()
:如果你处理的数据量非常大,或者需要在事务提交之前分批执行部分 SQL,显式调用flushStatements()
是必要的。 -
直接调用
commit()
的适用场景 :对于小规模批处理或不需要分阶段执行 SQL 的操作,直接调用sqlSession.commit()
即可。