背景
保守してたら、try-finallyでDBのコネクションの接続をクローズして管理しているコードを発見しました。
ぱっと見usingを使った方が良さげに思え書き換えたけど、例外が発生することに…。
※. usingはJavaでいうtry-with-resourcesという認識
結論
- コネクションの再利用する前提なのにusingで破棄していたことが原因
- コネクションの再利用しないならusing、再利用するならtry-finallyするなり自分で管理しよう!
調査
テストコードで検証してみる
try-finallyとusingのそれぞれを使用した場合のサンプルコードで実験してみました。
長いので詳細はこちらをご参照ください。
public class SampleDao
{
// 省略
/// <summary>
/// try-finallyでCloseする
/// </summary>
public void execute()
{
SqlCommand command = new SqlCommand();
command.CommandText = "SELECT GETDATE()";
command.Connection = this.Connection;
try {
command.Connection.Open();
command.ExecuteReader();
} finally {
command.Connection.Close();
}
}
/// <summary>
/// usingでCloseする
/// </summary>
public void executeWithUsing()
{
SqlCommand command = new SqlCommand();
command.CommandText = "SELECT GETDATE()";
command.Connection = this.Connection;
command.Connection.Open();
using (command.Connection)
{
command.ExecuteReader();
}
}
// 省略
}
上記のサンプルを用いて以下のようなユニットテストで検証してみます。
[TestClass]
public class UnitTest1
{
// 省略
[TestMethod]
public void 複数利用で再利用する場合()
{
SampleDao dao1 = new SampleDao();
// 初回
try
{
dao1.execute();
}
catch (Exception e)
{
Assert.Fail();
}
Assert.AreEqual(ConnectionState.Closed, dao1.Connection.State);
Assert.AreNotEqual("", dao1.Connection.ConnectionString);
// 2回目
try
{
dao1.execute();
}
catch (Exception e)
{
Assert.Fail();
}
Assert.AreEqual(ConnectionState.Closed, dao1.Connection.State);
Assert.AreNotEqual("", dao1.Connection.ConnectionString);
}
// 省略
[TestMethod]
public void using_複数利用で再利用する場合()
{
SampleDao dao1 = new SampleDao();
// 初回
try
{
dao1.executeWithUsing();
}
catch (Exception e)
{
Assert.Fail();
}
Assert.AreEqual(ConnectionState.Closed, dao1.Connection.State);
Assert.AreEqual("", dao1.Connection.ConnectionString);
// 2回目
try
{
dao1.executeWithUsing();
Assert.Fail();
}
catch (Exception e)
{
Assert.AreEqual("", dao1.Connection.ConnectionString);
}
}
}
結果は以下のようになり、usingを使用したケースではコネクションの再利用はできないことになりました。
usingを使用しない
初回 | 2回目 | |
---|---|---|
単一利用で再利用しない場合 | 正常終了 | N/A |
複数利用で再利用しない場合 | 正常終了 | 正常終了 |
複数利用で再利用する場合 | 正常終了 | 正常終了 |
usingを使用
初回 | 2回目 | |
---|---|---|
単一利用で再利用しない場合 | 正常終了 | N/A |
複数利用で再利用しない場合 | 正常終了 | 正常終了 |
複数利用で再利用する場合 | 正常終了 | InvalidOperationException |
そもそもusingってどういうもの?
Microsoftのusing ステートメント (C# リファレンス)を引用すると以下のように説明されていました。
IDisposable オブジェクトの正しい使用を保証する簡易構文を提供します。 C# 8.0 以降は、using ステートメントによって IAsyncDisposable オブジェクトが適切に使用されるようになります。
上記の参考サイトから詳細として紹介されているステートメント > using ステートメントでは
ステートメントは、 using 1 つまたは複数のリソースを取得し、ステートメントを実行してから、リソースを破棄します。
と説明されていました。
私の言葉でまとめると、リソースは必ずDisposeメソッドによって破棄されるよ、ということが肝のようです。
※. ただし、MicrosoftSqlConnection.Close メソッド > 注釈では以下のように説明されておりちょっと紛らわしい
Close と Dispose は機能的に同等です
今回のケースにおけるusingの使用有無での違い
こんな感じになります。
usingを使用しない
- コネクションをオープン
- SQL実行
- コネクションをクローズ
- コネクションは破棄されていないので 1. に戻る
usingを使用
- コネクションをオープン
- SQL実行
- コネクションをクローズ
- Disposeでコネクションが破棄
- コネクションは破棄されているので 1. に戻ろうとすると例外が発生
まとめ
今回は書き換えたusingによってコネクションが破棄されたことが、既存のコードとの違いであり、例外発生の原因でした。
そのため、実装によっては必ずしもusingがベストではない場合もあることがわかりました。
便利ですが仕様を理解しつつ注意して使っていきたいです!