更新 2021-12-31

今天遇到一个 bug, 2 个 services,  A 和 B

A 调用 B, 算是一个嵌套. A 无论如何都会执行, 哪怕 B fail 了.

B 在 save change 的时候 fail 了, 我就执行 rollback to savepoint. 然后交回给 A, A 继续执行然后 save changes. 结果又 fail 了.

才发现, 是因为 B 的 state 依然在. 没有被 rollback 掉.

原来我之前搞错了. 之前说, 当 ef core rollback to savepoint 也会把 state 给 rollback.. 天真...

DbContextTransaction should also rollback dbcontext

ef 认为 1 个 context 代表一个 unit of work. 

开启一个 context 之后做一堆的修修改改. 最终 save changes

所有的 changes record 就清空了. 这个 unit of work 就算是完成一个小任务了.

至于刚才的 save changes 有没有永久保存数据库, 那个是 transaction 负责的. 要看 commit/rollback.

所以 transaction 是 transaction, context 是 context. 没有直接关系. 请分开看待. 这也能比较好理解, 多个 context share 1 transaction 是怎么回事儿了. 

回到上面的例子, 2 个思路, 第 1 个 A,B 公用一个 context 是否合理?, 第 2 个, save change 失败后要如何处理 state ?

for 我的案子的话, 分开会好一些. save change 失败一般上都不太会管 state, 大部分情况就是 throw error 出去. 

 

更新: 2021-07-03

.net 5 之后 ef core 有了 save point 的功能 

https://docs.microsoft.com/en-gb/ef/core/saving/transactions#savepoints

sql server 是有这个概念的, 可以在 rollback 的时候选一个 save point. 这个也是模拟 nested transaction scope 的做法. 

之前我是用 system.transaction 来处理 nested transaction 的 (虽然 ef 并没有推荐). 而现在我觉得如果可以的话尽量用 ef 的 transaction 会更好.

好处 1 就是比较贴近 sql server 的方式,

好处 2 就是 system transaction 有些局限, 比如它不可能 rollback entity 的 state, 这导致了它只能要嘛全部 commit 要嘛全不 commit. 虽然说可以搞 new scope 但是如果要求是 child rollback, parent 要 commit, parent commit child 才可以 commit 就做不到了

好处 3 就是 ef 推荐.

坏处就是比较写起来比较麻烦, 需要判断是不是 root transaction, 要做 save point 等. 但是这些可以通过 wrap transaction 封装一下就可以解决的了. 

说说 save point. 它很厉害哦, 当 rollback to savepoint 的时候, entity state 是会改变的. 比如本来是 added, rollback 以后是 unchange 

附上一个 example : 

[HttpPost("CreateUserByPassword")]
public async Task<ActionResult> CreateUserByPasswordAsync([FromBody] CreateUser1Dto dto)
{
    using var transaction = await _db.Database.BeginTransactionAsync();
    try
    {
        await CreateCountryAsync();
    }
    catch
    {


    }
    try
    {
        var user = new User
        {
            UserName = "keatkeat90",
            Type = UserType.Internal
        };
        var result = await _userManager.CreateAsync(user, "keatkeat90");
        await transaction.CommitAsync();
    }
    catch (Exception ex)
    {

    }

    return Ok();
}

public async Task CreateCountryAsync()
{
    var isRootTransaction = _db.Database.CurrentTransaction == null;
    IDbContextTransaction transaction = isRootTransaction ? await _db.Database.BeginTransactionAsync() : _db.Database.CurrentTransaction!;
    var country = new Country
    {
        CountryName = "Philippines"
    };
    if (!isRootTransaction)
    {
        await transaction.CreateSavepointAsync("CreateCountrySavePoint");
    }
    try
    {
        _db.Countries.Add(country);
        var state = _db.Entry(country).State; // Added
        await _db.SaveChangesAsync();
        throw new Exception("error");
        if (isRootTransaction)
        {
            await transaction.CommitAsync();
        }
    }
    catch (Exception ex)
    {
        if (isRootTransaction)
        {
            await transaction.RollbackAsync();
        }
        else
        {
            await transaction.RollbackToSavepointAsync("CreateCountrySavePoint");
            var state = _db.Entry(country).State; // UnChanged
        }
        throw ex;
    }
    finally
    {
        if (isRootTransaction)
        {
            transaction.Dispose();
        }
    }
}
View Code

相关文章: