更新 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(); } } }