結論
- using句を使ってトランザクションのRollback()呼び出しを省略しても問題は起きないのではないか。
- それをしない方がいいと書いているMSDNの方がむしろ問題ではないか。(過激な意見)
- 2021.8.30 追記 結局やはり、MSDNのいう通りにした方が無難か…という事になりつつあります。コメント欄を併せてお読みください。
経緯
.NETシステムでDBを扱っているソースコードにおいて、トランザクション処理は以下のようにtry-catchを使って明示的にRollbackされることが多いようです。
using (var trans = db.Database.BeginTransaction()) {
try
{
// DB処理
// コミット
trans.Commit();
}
catch (Exception ex)
{
trans.Rollback();
}
}
トランザクションはDisposableであり、Dispose()する必要があるリソースなので、using句を使って必ずDispose()されるように書いています。
しかし、トランザクションのDispose()時に未完了のトランザクションは自動的にRollback()されないのでしょうか。
合理的に考えれば、トランザクションが破棄される際に未完了のトランザクションを「放置」したままにすべきではなく、それはRollback()されるべきな気がします。
もしRollback()してくれるのであれば、上記は以下のようにシンプルに記述できます。
using (var trans = db.Database.BeginTransaction()) {
// DB処理
// コミット
trans.Commit();
}
MSDNを調べると、次のように記載されています。
DbTransaction.Dispose Method
https://docs.microsoft.com/en-us/dotnet/api/system.data.common.dbtransaction.dispose?redirectedfrom=MSDN&view=net-5.0#System_Data_Common_DbTransaction_Dispose
Dispose should rollback the transaction. However, the behavior of Dispose is provider specific , and should not replace calling Rollback.
Disposeはトランザクションをロールバックした方がよいでしょう。しかし、Disposeの振る舞いはプロバイダの実装によって決まり、Rollback呼び出しをこれで置き換えない方が良いでしょう。
つまり、Disposeを呼び出した時、Rollbackも一緒に行った方が良いものの、実際にそうなっているかはプロバイダの実装に依存する為、Rollback呼び出しの代わりにDisposeを呼び出すというのはやめた方が良い、ということのようです。
それでは、実際のところプロバイダの実装はどうなっているのでしょうか?
各プロバイダの実装を、公開されているソースコードから探ってみます。
各データプロバイダの実装
SQL Server(SqlClient)
DisposeでRollbackしている。
private void Dispose(bool disposing)
{
SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.Dispose | RES | CPOOL | Object Id {0}, Disposing", ObjectID);
if (disposing)
{
if (null != _innerConnection)
{
// implicitly rollback if transaction still valid
_disposing = true;
this.Rollback();
}
}
}
PostgreSQL(Npgsql)
DisposeでRollbackしている。
/// <summary>
/// Disposes the transaction, rolling it back if it is still pending.
/// </summary>
protected override void Dispose(bool disposing)
{
if (IsDisposed)
return;
if (disposing)
{
if (!IsCompleted)
{
try
{
_connector.CloseOngoingOperations(async: false).GetAwaiter().GetResult();
Rollback();
}
catch (Exception ex)
{
Debug.Assert(_connector.IsBroken);
Log.Error("Exception while disposing a transaction", ex, _connector.Id);
}
}
IsDisposed = true;
_connector?.Connection?.EndBindingScope(ConnectorBindingScope.Transaction);
}
}
Oracle(Oracle Data Provider for .NET)
(おそらく)DisposeでRollbackしている。
※ソースコードが見つからなかったので、ドキュメントより。
https://docs.oracle.com/cd/E57425_01/121/ODPNT/OracleTransactionClass.htm#i1015663
このメソッドは、OracleTransactionオブジェクトが保持している管理リソースおよび非管理リソースの両方を解放します。トランザクションが完了した状態ではない場合、トランザクションをロールバックする試みが実行されます。
MySQL(MySQL Connector/NET)
DisposeでRollbackしている。
protected override void Dispose(bool disposing)
{
if (disposed) return;
base.Dispose(disposing);
if (disposing)
{
if ((Connection != null && Connection.State == ConnectionState.Open || Connection.SoftClosed) && open)
Rollback();
}
disposed = true;
}
SQLite(Microsoft.Data.Sqlite.Core)
DisposeでRollbackしている。
protected override void Dispose(bool disposing)
{
if (disposing
&& !_completed
&& _connection!.State == ConnectionState.Open)
{
RollbackInternal();
}
}
結果と考察
調べてみたものは全て、DisposeでRollbackしているようです。
MSDNの言う通り、DisposeでRollbackされるかどうかはプロバイダの実装依存かもしれませんが、「Rollbackされない」ことを前提に書かれたソースコードはともすれば冗長になり、不要なパフォーマンス低下や不具合を生み出しかねません。
Microsoft自身が「DisposeでRollbackした方がよい」と明言している以上、プロバイダにもそれを強く求め、ここは「Rollbackされることを前提」として頂いた方がみんな幸せになれるのでは…と思ったのですが、そうできない理由があるのでしょうか。
個人的には、上記のプロバイダを使っており、今後DBが変更されるとしても上記以外が選ばれることは考えにくいのであれば、using句でのtry-catchを使ったRollback()呼び出しは省略しても良いのではないかと思いました。
この記事に対するご意見や追加情報などがありましたら、お気軽にコメント頂ければ幸いです。