Skip to content

事务

这一页解决多步数据库操作如何作为一个整体提交或回滚,以及隐式事务和显式事务如何协作。

适用场景

  • 一次业务操作里要执行多次写入
  • 既有 saveChanges(),又有手写 SQL
  • 需要明确异常时的回滚边界
  • 业务方法既可能独立调用,也可能被上层事务包裹

核心问题

事务的目标是保证一组数据库操作要么全部成功,要么全部失败。

SqlSharp 的事务设计有一个关键特点:saveChanges() 会自动判断当前是否已在事务中——如果已经存在事务,就直接在当前事务内保存;如果不存在,就自动开启一个事务包裹本次保存。这意味着你的业务方法不需要关心调用方是否已经开启了事务,代码自然具备可组合性。

快速开始

只用 saveChanges(),不写任何事务代码:

cangjie
let user = User(name: "张三", email: "zhangsan@example.com")
ctx.add(user)
ctx.saveChanges()

这段代码背后发生了什么:

text
saveChanges()

    ├─ hasTransaction()? ──是──▶ 直接在当前事务内保存

    └─ 否 ──▶ 自动 beginTransaction()

                ├─ 执行 SQL
                ├─ commit()
                └─ close()

对于绝大多数单表写入场景,你不需要手动管理事务。

核心概念

显式事务:beginTransaction()

当你需要把多次 saveChanges() 或手写 SQL 放在同一个事务里时,显式开启:

cangjie
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()  // 统一提交
}

流程:

text
beginTransaction()  ◀── 开启事务,_currentTransaction 置为当前事务

    ├─ saveChanges()  ← hasTransaction() = true,直接保存,不创建新事务
    ├─ execute()      ← 复用同一连接和事务

    ├─ commit()       ← 统一提交
    └─ close()        ← 清理 _currentTransaction

DbContext 内部维护 _currentTransaction: ?DbContextTransactionhasTransaction() 检查它是否存在:

cangjie
public func hasTransaction(): Bool {
    _currentTransaction.isSome()
}

这就是可组合性的关键——saveChanges() 通过 hasTransaction() 判断当前环境,有外层事务就复用,没有就自动创建。

自动回滚:close() 的兜底行为

DbContextTransactionclose() 方法有一个重要保障:

text
close()

    ├─ 已 commit() 或已 rollback()? ──是──▶ 跳过

    └─ 否 ──▶ 自动调用 rollback()

这意味着:

cangjie
try (ctx = DbContext(), _tx = ctx.beginTransaction()) {
    ctx.add(Product(name: "测试", price: 999))
    ctx.saveChanges()
    // 注意:没有调用 tx.commit()
}  // close() 自动 rollback,数据不会持久化

try-with-resource 退出时如果事务未提交,close() 会自动回滚,不会留下半截数据。

同样,如果事务中途抛出异常,commit() 没有执行,close() 同样会检测到未提交并自动回滚:

cangjie
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。

常见用法

可组合的业务方法

这是事务设计带来的最大收益——业务方法不需要知道自己是否在事务中:

cangjie
// 这个方法可以被独立调用,也可以被上层事务包裹
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(),不主动管理事务。上层调用方决定事务边界。

服务层编排:多仓储 + 事务

真实业务场景中,一次操作往往涉及多个仓储。以下是一个典型的下单服务:

cangjie
// 仓储层 —— 纯数据访问,不管理事务
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——每个服务方法都会重复这个模板:

text
try (ctx = ...) {
    try (tx = ctx.beginTransaction()) {
        // 唯一不同的部分:业务逻辑
        tx.commit()
    }
}

当服务方法越来越多,这个模式就成了横切关注点。Mediator 中介器IPipelineHandler 正好解决这个问题——把事务模板提走,处理器只写业务逻辑:

cangjie
// 事务管道 —— 写一次,所有请求复用
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 中介器 → 管道:事务行为

嵌套事务会抛异常

cangjie
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:

cangjie
ctx.execute("SAVEPOINT sp1")
// ... 操作 ...
ctx.execute("ROLLBACK TO SAVEPOINT sp1")

与其他模块组合

  • 常规实体写入来自 变更跟踪
  • 手写 SQL 来自 手写 SQL
  • 在 Web 项目里,事务边界通常放在应用服务层,由 依赖注入 管理 DbContext 生命周期

生产建议

  • saveChanges() 处理单步事务 —— 绝大多数场景不需要显式 beginTransaction()
  • 事务边界放在服务层 —— 不要在控制器里直接管理事务
  • 尽量缩短事务持续时间 —— 先做校验和计算,再开事务
  • 事务内不要有外部调用 —— HTTP 请求、文件 IO、消息队列等不要放在事务里
  • 关注并发 —— 高并发写场景结合乐观锁(isConcurrencyToken())使用

常见问题

什么时候用隐式事务,什么时候用显式事务?

单次 saveChanges() 用隐式事务就够了。当你需要把多次 saveChanges() 或手写 SQL 绑成一个原子操作时,用显式事务。

saveChanges() 在事务内和事务外行为有什么不同?

行为完全一致,区别只在于事务归属:

场景hasTransaction()行为
无显式事务false自动创建事务 → 保存 → 提交
在显式事务内true直接保存,事务由外层统一提交

事务里 saveChanges() 失败后还能继续操作吗?

不能。一旦事务中发生异常,整个事务会被回滚,后续操作应该在新的 try 块(新事务)中重试。

为什么显式事务的 close() 不会重复回滚?

DbContextTransaction 内部维护了 _isSubmit 标记。一旦调用过 commit()rollback()close() 就不再执行回滚操作。