事务
这一页解决多步数据库操作如何作为一个整体提交或回滚,以及隐式事务和显式事务如何协作。
适用场景
- 一次业务操作里要执行多次写入
- 既有
saveChanges(),又有手写 SQL - 需要明确异常时的回滚边界
- 业务方法既可能独立调用,也可能被上层事务包裹
核心问题
事务的目标是保证一组数据库操作要么全部成功,要么全部失败。
SqlSharp 的事务设计有一个关键特点:saveChanges() 会自动判断当前是否已在事务中——如果已经存在事务,就直接在当前事务内保存;如果不存在,就自动开启一个事务包裹本次保存。这意味着你的业务方法不需要关心调用方是否已经开启了事务,代码自然具备可组合性。
快速开始
只用 saveChanges(),不写任何事务代码:
let user = User(name: "张三", email: "zhangsan@example.com")
ctx.add(user)
ctx.saveChanges()这段代码背后发生了什么:
saveChanges()
│
├─ hasTransaction()? ──是──▶ 直接在当前事务内保存
│
└─ 否 ──▶ 自动 beginTransaction()
│
├─ 执行 SQL
├─ commit()
└─ close()对于绝大多数单表写入场景,你不需要手动管理事务。
核心概念
显式事务:beginTransaction()
当你需要把多次 saveChanges() 或手写 SQL 放在同一个事务里时,显式开启:
try (tx = ctx.beginTransaction()) {
ctx.add(Product(name: "A", price: 100))
ctx.saveChanges() // hasTransaction() = true,直接保存,不创建新事务
let p = DbParameters()
p.add("id", Int64(1))
ctx.execute("UPDATE inventory SET count = count - 1 WHERE product_id = @id", p)
tx.commit() // 统一提交
}流程:
beginTransaction() ◀── 开启事务,_currentTransaction 置为当前事务
│
├─ saveChanges() ← hasTransaction() = true,直接保存,不创建新事务
├─ execute() ← 复用同一连接和事务
│
├─ commit() ← 统一提交
└─ close() ← 清理 _currentTransactionDbContext 内部维护 _currentTransaction: ?DbContextTransaction。hasTransaction() 检查它是否存在:
public func hasTransaction(): Bool {
_currentTransaction.isSome()
}这就是可组合性的关键——saveChanges() 通过 hasTransaction() 判断当前环境,有外层事务就复用,没有就自动创建。
自动回滚:close() 的兜底行为
DbContextTransaction 的 close() 方法有一个重要保障:
close()
│
├─ 已 commit() 或已 rollback()? ──是──▶ 跳过
│
└─ 否 ──▶ 自动调用 rollback()这意味着:
try (ctx = DbContext(), _tx = ctx.beginTransaction()) {
ctx.add(Product(name: "测试", price: 999))
ctx.saveChanges()
// 注意:没有调用 tx.commit()
} // close() 自动 rollback,数据不会持久化try-with-resource 退出时如果事务未提交,close() 会自动回滚,不会留下半截数据。
同样,如果事务中途抛出异常,commit() 没有执行,close() 同样会检测到未提交并自动回滚:
try (tx = ctx.beginTransaction()) {
ctx.add(Product(name: "A", price: 100))
ctx.saveChanges()
// 如果这里抛异常
let p = DbParameters()
ctx.execute("UPDATE inventory SET count = count - 1", p) // 假设这条 SQL 错了
tx.commit()
} // close() 检测到未提交,自动 rollback显式事务下抛异常,不需要手动 try-catch 包裹 rollback。
常见用法
可组合的业务方法
这是事务设计带来的最大收益——业务方法不需要知道自己是否在事务中:
// 这个方法可以被独立调用,也可以被上层事务包裹
public func createOrder(ctx: AppDbContext, order: Order): Unit {
ctx.add(order)
ctx.saveChanges() // 有外层事务就用外层,没有就自动创建
}
public func deductStock(ctx: AppDbContext, productId: Int64, count: Int64): Unit {
let p = DbParameters()
p.add("id", productId)
p.add("cnt", count)
ctx.execute("UPDATE products SET stock = stock - @cnt WHERE id = @id", p)
}
// 场景1:独立调用 —— 各自在自己的事务中执行
createOrder(ctx, order1)
deductStock(ctx, 1, 5)
// 场景2:组合调用 —— 两个操作在同一个事务中
try (tx = ctx.beginTransaction()) {
createOrder(ctx, order1) // saveChanges 发现 hasTransaction()=true,直接保存
deductStock(ctx, 1, 5) // execute 在同一事务内执行
tx.commit()
}核心要点:业务方法只写 saveChanges(),不主动管理事务。上层调用方决定事务边界。
服务层编排:多仓储 + 事务
真实业务场景中,一次操作往往涉及多个仓储。以下是一个典型的下单服务:
// 仓储层 —— 纯数据访问,不管理事务
public class OrderRepository {
private let _ctx: AppDbContext
public init(ctx: AppDbContext) {
_ctx = ctx
}
public func create(order: Order): Unit {
_ctx.add(order)
_ctx.saveChanges()
}
}
public class InventoryRepository {
private let _ctx: AppDbContext
public init(ctx: AppDbContext) {
_ctx = ctx
}
public func deduct(productId: Int64, count: Int64): Unit {
let p = DbParameters()
p.add("id", productId)
p.add("cnt", count)
_ctx.execute("UPDATE products SET stock = stock - @cnt WHERE id = @id", p)
}
}
// 服务层 —— 管理上下文和事务边界
public class OrderService {
private let _sp: IServiceProvider
public init(sp: IServiceProvider) {
_sp = sp
}
public func placeOrder(order: Order): Unit {
try (ctx = _sp.getService<AppDbContext>()) {
try (tx = ctx.beginTransaction()) {
let orderRepo = OrderRepository(ctx)
let inventoryRepo = InventoryRepository(ctx)
orderRepo.create(order) // INSERT 已执行
inventoryRepo.deduct( // 假设这里库存不足,抛异常
order.productId, order.count
)
tx.commit() // 异常下永远执行不到
} // close() 检测到未提交 → 自动 rollback,INSERT 也被撤销
}
}
}服务层不需要写任何 rollback 代码。 deduct() 抛异常后 commit() 执行不到,try-with-resource 退出时 close() 发现事务未提交,自动回滚——包括已经执行的 INSERT。
从重复模板到管道 / AOP
观察上面的 placeOrder——每个服务方法都会重复这个模板:
try (ctx = ...) {
try (tx = ctx.beginTransaction()) {
// 唯一不同的部分:业务逻辑
tx.commit()
}
}当服务方法越来越多,这个模式就成了横切关注点。Mediator 中介器 的 IPipelineHandler 正好解决这个问题——把事务模板提走,处理器只写业务逻辑:
// 事务管道 —— 写一次,所有请求复用
public class TransactionBehavior <: IPipelineHandler {
private let _sp: IServiceProvider
public init(sp: IServiceProvider) {
_sp = sp
}
public func handle(request: Any, next: RequestDelegate): Any {
try (ctx = _sp.getService<AppDbContext>()) {
try (tx = ctx.beginTransaction()) {
let result = next()
tx.commit()
return result
}
}
}
}
// 业务处理器 —— 不再碰事务代码
public class PlaceOrderHandler <: IRequestHandler<PlaceOrderCommand, Int64> {
private let _orderRepo: OrderRepository
private let _inventoryRepo: InventoryRepository
public func handle(cmd: PlaceOrderCommand): Int64 {
_orderRepo.create(cmd.order)
_inventoryRepo.deduct(cmd.productId, cmd.count)
return cmd.order.id
}
}管道把 DbContext 管理和事务包裹一把提走,所有处理器不再重复 try(ctx) { try(tx) { ... tx.commit() } } 模板。这是中介器管道最典型的 AOP 应用——详见 Mediator 中介器 → 管道:事务行为。
嵌套事务会抛异常
try (outer = ctx.beginTransaction()) {
ctx.add(Product(name: "A", price: 100))
ctx.saveChanges()
try (inner = ctx.beginTransaction()) { // ❌ 抛出 UnsupportedException
// ...
}
}beginTransaction() 发现 _currentTransaction 已存在时会直接抛出:
UnsupportedException("A transaction is already in progress. Nested transactions are not supported.")如果需要保存点语义,可以用数据库原生 SQL:
ctx.execute("SAVEPOINT sp1")
// ... 操作 ...
ctx.execute("ROLLBACK TO SAVEPOINT sp1")与其他模块组合
生产建议
- 让
saveChanges()处理单步事务 —— 绝大多数场景不需要显式beginTransaction() - 事务边界放在服务层 —— 不要在控制器里直接管理事务
- 尽量缩短事务持续时间 —— 先做校验和计算,再开事务
- 事务内不要有外部调用 —— HTTP 请求、文件 IO、消息队列等不要放在事务里
- 关注并发 —— 高并发写场景结合乐观锁(
isConcurrencyToken())使用
常见问题
什么时候用隐式事务,什么时候用显式事务?
单次 saveChanges() 用隐式事务就够了。当你需要把多次 saveChanges() 或手写 SQL 绑成一个原子操作时,用显式事务。
saveChanges() 在事务内和事务外行为有什么不同?
行为完全一致,区别只在于事务归属:
| 场景 | hasTransaction() | 行为 |
|---|---|---|
| 无显式事务 | false | 自动创建事务 → 保存 → 提交 |
| 在显式事务内 | true | 直接保存,事务由外层统一提交 |
事务里 saveChanges() 失败后还能继续操作吗?
不能。一旦事务中发生异常,整个事务会被回滚,后续操作应该在新的 try 块(新事务)中重试。
为什么显式事务的 close() 不会重复回滚?
DbContextTransaction 内部维护了 _isSubmit 标记。一旦调用过 commit() 或 rollback(),close() 就不再执行回滚操作。