はじめに
C#とEntity Framework Coreでデータ更新を行う処理を作成し、数年間エラーなく処理が動いていたプログラムがありました。ある日、更新できず処理が止まってしまう事態となりました。
今回はその現象のプログラムと解決策をとなります。
問題となった処理
Models.HogeDBContext db = new Models.HogeDBContext();
var items = db.V_Table // Viewテーブル
.Where(r => r.hoge == hoge); // なんらかの条件式
foreach (var item in items)
{
// なんらかの処理
var data = db.T_Table
.Where(r => r.id == id); // idから1件データを取得
if(data == null)
{
data = new T_Table {/* パラメータ追加 */}
db.T_Table.Add(data);
}
else
{
// dataの値を更新
db.Entry(data).State = EntityState.Modified;
}
db.SaveChanges(); // ここで止まる
}
エラー内容
上記処理の db.SaveChanges() で止まり、初回は An error occurred while saving the entity changes. See the inner exception for details. が出力され、2回目以降は 新しいトランザクションは許可されません。他のスレッドがこのセッションで実行されています。 と出力され続けるようになりました。
原因の考察
データが日々増えてくる事でView V_Table のパフォーマンスが遅くなる事で発生していると考えました。
また、このViewは リンクテーブル もJOINしている事で一層のパフォーマンス低下を及ぼしていると思われます。
これらが原因でIQueryableとして取得されたデータを直接ループ内で操作してしまい、データベースへのアクセスが繰り返され、トランザクションや接続の問題が発生しやすくなったのだと考えます。
この部分(View)の変更はテストを含めると時間がかかるので以下の方法で解決しました。
解決策
ToArray()の使用
ToArray()メソッドを使用することで、クエリが即時実行され、結果が配列としてメモリ内に格納されます。クエリの実行がループの外で行われるため、遅延実行による問題も解消されます。
修正後のコードでは、ToArray()を使用してクエリを即時実行し、結果を配列として格納しています。これにより、データベースへのアクセスがループの外で一度だけ行われ、トランザクションの競合が発生しにくくなります。
var items = db.V_Table // Viewテーブル
.Where(r => r.hoge == hoge) // なんらかの条件式
.ToArray(); // ToArray()で必要なデータを取得
まとめ(教訓)
- 遅延実行する必要が無いものはToArray()等で確定させる