Solon 事务使用指南
概述
在 Solon 框架中,有两种方式使用事务:
- 声明式事务(需要额外依赖):使用
@Tran注解 - 编程式事务(推荐):使用
DB.Tx.run()API
本文档主要介绍 DLZ-DB 提供的编程式事务用法,这种方式更灵活、更可靠。
一、编程式事务(推荐)
1. 基本用法
@Component
public class UserService {
/**
* 基本事务提交
*/
public void createUser(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
// 其他操作...
});
}
}
2. 事务回滚
事务中抛出异常会自动回滚:
public void createUserWithRollback(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
if (someCondition) {
throw new RuntimeException("业务校验失败,触发回滚");
}
});
}
3. 带返回值的事务
public Long createUserAndGetId(User user) {
return DB.Tx.run(() -> {
DB.Pojo.insert(user);
return user.getId(); // 返回插入后的ID
});
}
4. 指定数据源的事务
public void createInSlaveDb(User user) {
DB.Tx.run("slave", () -> {
DB.Pojo.insert(user);
});
}
5. 嵌套事务
public void outerMethod() {
DB.Tx.run(() -> {
// 外层操作
DB.Pojo.insert(outerEntity);
// 调用内层方法(也是事务)
innerMethod();
});
}
public void innerMethod() {
DB.Tx.run(() -> {
// 内层操作
DB.Pojo.insert(innerEntity);
});
}
注意:每个 DB.Tx.run() 都是独立的事务。如果需要共享同一事务,应该在一个 DB.Tx.run() 块中完成所有操作。
二、完整示例
Service 类示例
package com.example.service;
import com.dlz.db.modal.DB;
import com.example.entity.User;
import org.noear.solon.annotation.Component;
import java.util.List;
@Component
public class UserService {
/**
* 创建用户(事务)
*/
public void createUser(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
});
}
/**
* 批量创建用户(事务)
*/
public void batchCreateUsers(List<User> users) {
DB.Tx.run(() -> {
for (User user : users) {
DB.Pojo.insert(user);
}
});
}
/**
* 更新用户(事务)
*/
public void updateUser(Long id, String newName) {
DB.Tx.run(() -> {
User user = DB.Pojo.select(User.class)
.eq(User::getId, id)
.queryBean();
if (user != null) {
user.setName(newName);
DB.Pojo.update(user).eq(User::getId, id).execute();
}
});
}
/**
* 删除用户(事务)
*/
public void deleteUser(Long id) {
DB.Tx.run(() -> {
DB.Pojo.delete(User.class)
.eq(User::getId, id)
.execute();
});
}
/**
* 转账业务(事务)
*/
public void transfer(String fromName, String toName, int amount) {
DB.Tx.run(() -> {
// 查找转出账户
User fromUser = DB.Pojo.select(User.class)
.eq(User::getName, fromName)
.queryBean();
// 查找转入账户
User toUser = DB.Pojo.select(User.class)
.eq(User::getName, toName)
.queryBean();
if (fromUser == null || toUser == null) {
throw new RuntimeException("用户不存在");
}
// 检查余额
if (fromUser.getBalance() < amount) {
throw new RuntimeException("余额不足");
}
// 扣款
fromUser.setBalance(fromUser.getBalance() - amount);
DB.Pojo.update(fromUser).eq(User::getId, fromUser.getId()).execute();
// 加款
toUser.setBalance(toUser.getBalance() + amount);
DB.Pojo.update(toUser).eq(User::getId, toUser.getId()).execute();
});
}
/**
* 查询用户(不需要事务)
*/
public List<User> queryUsers(String namePattern) {
return DB.Pojo.select(User.class)
.like(User::getName, namePattern)
.queryBeanList();
}
}
测试类示例
package com.example.test;
import com.dlz.db.modal.DB;
import com.dlz.test.db.config.BaseDBTest;
import com.example.entity.User;
import com.example.service.UserService;
import org.junit.jupiter.api.*;
import org.noear.solon.Solon;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserServiceTest extends BaseDBTest {
private UserService userService;
@BeforeEach
public void setUp() {
userService = Solon.context().getBean(UserService.class);
// 清理数据
DB.Jdbc.execute("DELETE FROM user");
}
@Test
@Order(1)
void testCreateUser() {
User user = new User();
user.setName("张三");
user.setAge(25);
userService.createUser(user);
assertNotNull(user.getId());
User found = DB.Pojo.select(User.class)
.eq(User::getId, user.getId())
.queryBean();
assertEquals("张三", found.getName());
}
@Test
@Order(2)
void testBatchCreateUsers() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + i);
users.add(user);
}
userService.batchCreateUsers(users);
int count = DB.Pojo.select(User.class).count();
assertEquals(5, count);
}
@Test
@Order(3)
void testTransferSuccess() {
// 准备数据
User fromUser = new User();
fromUser.setName("账户A");
fromUser.setBalance(200);
DB.Pojo.insert(fromUser);
User toUser = new User();
toUser.setName("账户B");
toUser.setBalance(100);
DB.Pojo.insert(toUser);
// 执行转账
userService.transfer("账户A", "账户B", 50);
// 验证结果
User fromAfter = DB.Pojo.select(User.class)
.eq(User::getName, "账户A")
.queryBean();
User toAfter = DB.Pojo.select(User.class)
.eq(User::getName, "账户B")
.queryBean();
assertEquals(150, fromAfter.getBalance());
assertEquals(150, toAfter.getBalance());
}
@Test
@Order(4)
void testTransferInsufficientBalance() {
// 准备数据
User fromUser = new User();
fromUser.setName("账户C");
fromUser.setBalance(30);
DB.Pojo.insert(fromUser);
User toUser = new User();
toUser.setName("账户D");
toUser.setBalance(100);
DB.Pojo.insert(toUser);
// 尝试转账(余额不足)
assertThrows(RuntimeException.class, () -> {
userService.transfer("账户C", "账户D", 50);
});
// 验证数据未改变(回滚)
User fromAfter = DB.Pojo.select(User.class)
.eq(User::getName, "账户C")
.queryBean();
assertEquals(30, fromAfter.getBalance());
}
}
三、最佳实践
1. 事务粒度
- ✅ 推荐:事务应该尽可能小,只包含必要的数据库操作
- ❌ 避免:在事务中执行耗时操作(如 HTTP 请求、文件 IO)
// ✅ 好的做法
public void createUser(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
// 只包含数据库操作
});
// 发送邮件等其他操作放在事务外
sendEmail(user);
}
// ❌ 不好的做法
public void createUserBad(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
sendEmail(user); // 耗时操作不应该在事务中
callExternalApi(user); // 外部调用不应该在事务中
});
}
2. 异常处理
- ✅ 推荐:让异常自然抛出,由事务机制自动回滚
- ❌ 避免:在事务内部捕获异常后不抛出
// ✅ 好的做法
public void createUserGood(User user) {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
if (!validate(user)) {
throw new RuntimeException("验证失败"); // 抛出异常触发回滚
}
});
}
// ❌ 不好的做法
public void createUserBad(User user) {
DB.Tx.run(() -> {
try {
DB.Pojo.insert(user);
if (!validate(user)) {
throw new RuntimeException("验证失败");
}
} catch (Exception e) {
log.error("错误", e);
// 捕获了异常但没有重新抛出,事务不会回滚!
}
});
}
3. 嵌套事务
如果需要多个操作共享同一事务,应该在同一个 DB.Tx.run() 块中完成:
// ✅ 推荐:共享同一事务
public void createOrderAndItems(Order order, List<OrderItem> items) {
DB.Tx.run(() -> {
// 插入订单
DB.Pojo.insert(order);
// 插入订单项
for (OrderItem item : items) {
item.setOrderId(order.getId());
DB.Pojo.insert(item);
}
});
}
// ⚠️ 注意:这是两个独立的事务
public void createOrderAndItemsSeparate(Order order, List<OrderItem> items) {
DB.Tx.run(() -> {
DB.Pojo.insert(order);
});
DB.Tx.run(() -> {
for (OrderItem item : items) {
item.setOrderId(order.getId());
DB.Pojo.insert(item);
}
});
}
4. 查询操作
- 只读查询通常不需要事务
- 如果需要保证读取一致性,可以使用事务
// 简单查询 - 不需要事务
public List<User> queryUsers() {
return DB.Pojo.select(User.class).queryBeanList();
}
// 需要一致性的查询 - 使用事务
public List<User> queryUsersWithConsistency() {
return DB.Tx.run(() -> {
return DB.Pojo.select(User.class).queryBeanList();
});
}
四、常见问题
Q1: 为什么我的事务没有回滚?
可能原因:
- 异常被捕获但没有重新抛出
- 使用了独立的
DB.Tx.run()调用,而不是嵌套在同一事务中 - 数据库引擎不支持事务(如 MyISAM)
解决方案:
// 确保异常能抛出
DB.Tx.run(() -> {
DB.Pojo.insert(user);
if (error) {
throw new RuntimeException("错误"); // 必须抛出
}
});
Q2: 如何在事务中获取自增主键?
Long userId = DB.Tx.run(() -> {
User user = new User();
user.setName("张三");
DB.Pojo.insert(user);
return user.getId(); // insert 后会自动回填 ID
});
Q3: 事务超时怎么处理?
DLZ-DB 的编程式事务默认没有超时限制。如果需要超时控制,可以在应用层实现:
public void createUserWithTimeout(User user, long timeoutMs) {
CompletableFuture.runAsync(() -> {
DB.Tx.run(() -> {
DB.Pojo.insert(user);
});
}).orTimeout(timeoutMs, TimeUnit.MILLISECONDS);
}
五、总结
| 特性 | 编程式事务 DB.Tx.run() | 声明式事务 @Tran |
|---|---|---|
| 易用性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 可靠性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 学习成本 | 低 | 中 |
| 依赖要求 | 无 | 可能需要额外依赖 |
推荐:在 Solon 项目中使用 DB.Tx.run() 编程式事务,它更灵活、更可靠,且不需要额外的配置和依赖。