質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Entity Framework

Entity Frameworkは、.NET Framework 3.5より追加されたデータアクセス技術。正式名称は「ADO.NET Entity Framework」です。データベースエンジンに依存しておらず、データプロバイダの変更のみで様々なデータベースに対応できます。

.NET

.NETとは、主に.NET Frameworkと呼ばれるアプリケーションまたは開発環境を指します。CLR(共通言語ランタイム)を搭載し、入力された言語をCIL(共通中間言語)に変換・実行することが可能です。そのため、C#やPythonなど複数の言語を用いることができます。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

3回答

3914閲覧

[C#,.NET5,EFCore5+Microsfot.Data.Sqlite] トランザクションのロールバックが意図通りに動かない

Yas.T

総合スコア6

Entity Framework

Entity Frameworkは、.NET Framework 3.5より追加されたデータアクセス技術。正式名称は「ADO.NET Entity Framework」です。データベースエンジンに依存しておらず、データプロバイダの変更のみで様々なデータベースに対応できます。

.NET

.NETとは、主に.NET Frameworkと呼ばれるアプリケーションまたは開発環境を指します。CLR(共通言語ランタイム)を搭載し、入力された言語をCIL(共通中間言語)に変換・実行することが可能です。そのため、C#やPythonなど複数の言語を用いることができます。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2021/09/16 09:27

編集2021/09/17 03:03

前提・実現したいこと

Entity Framework Core の勉強を兼ねて、
C# + SQLite + MSTest で DB アクセスのラーニングテストをしているんですが、
トランザクションのロールバックができない、という状況ではまってます。
ターゲットの .NET バージョンは 5.0 です。

使っている (NuGet) パッケージの問題なのか Entity Framework の使い方が悪いのか。
問題の切り分けが不十分で申し訳ありませんが、動作しない理由/原因について
心当たりのことがあればご教示ください。

使っているNuGetパッケージは下記、ターゲットフレームワークは .NET5 です。
- Microsoft.Data.Sqlite.Core, 5.0.10
- Microsoft.EntityFrameworkCore.Sqlite, 5.0.10

発生している問題・エラーメッセージ

テストの期待動作は下記。3回目のコンソール書き込み内容が3、というのが問題点です。

  • 初期状態:DBにテーブルはあるが、エンティティは登録されていない

 - 1回め:新規エンティティ作成、1をテーブルに書き込んで1を返す
- 2回目:テーブルから1を読み込み、+1した値2を書き込んで2を返す

  • transaction.Rollback() により書き込んだ値が1に戻るはず

 - 3回目:テーブルから1を読み込み、+1した2を書き込んで2を返すはず
⇔実際にはテーブルから読み取った値が2、+1した3を書き込んで3が返ってくる

該当のソースコード

.NET5 のコンソールアプリを作成し、Mainでテストコードを実行しています。
見通しが悪いのでコードを分割しました。

テストコード:

C#

1static void Main(string[] args) 2{ 3 // test.db 作成済、SEQUENCE も作成済み:CREATE TABLE SEQUENCES ( NAME TEXT NOT NULL PRIMARY KEY, VALUE INT ) 4 var testDB = new SqliteConnection("Data Source=./test.db;Mode=ReadWrite"); 5 testDB.Open(); 6 using (var cmd = testDB.CreateCommand()) 7 { 8 cmd.CommandText = "DELETE FROM SEQUENCES"; 9 cmd.ExecuteNonQuery(); 10 } 11 12 var seqName = "SEQ1"; 13 using (var context = new TestDBContext(testDB)) 14 { 15 var seq1 = new SequenceValue() { Name = seqName, Value = 1 }; 16 context.Sequences.Add(seq1); 17 context.SaveChanges(); 18 // OK: 1 が出力される 19 Console.WriteLine(seq1.Value); 20 21 using (var transaction = context.Database.BeginTransaction()) 22 { 23 var seq2 = context.Sequences.Find(seqName); 24 seq2.Value++; 25 context.SaveChanges(); 26 // OK: 2 が出力される 27 Console.WriteLine(seq2.Value); 28 transaction.Rollback(); 29 } 30 31 var seq3 = context.Sequences.Find(seqName); 32 seq3.Value++; 33 context.SaveChanges(); 34 35 // NG:ローバックされているので 2 が出力されるはず:実際には3が出力される 36 Console.WriteLine(seq3.Value); 37 } 38 39 testDB.Close(); 40}

SequenceValueクラス

DB上の SEQUENCES テーブルに対応する、Entity Framework のエンティティクラス

C#

1[Table("SEQUENCES")] 2class SequenceValue 3{ 4 [Key, Column("NAME")] public string Name { get; set; } 5 [Column("VALUE")] public int Value { get; set; } 6}

TestContext クラス

C#

1public class TestDBContext : DbContext 2{ 3 private SqliteConnection _conn; 4 public TestDBContext(SqliteConnection conn) { _conn = conn; } 5 protected override void OnConfiguring(DbContextOptionsBuilder builder) 6 { 7 builder.UseSqlite(_conn); 8 } 9 public DbSet<SequenceValue> Sequences { get; set; } 10}

試したこと

以下試しましたが、いずれもテスト結果は変わりません。
(3回目の出力呼出が3:2を出力してほしい。)

  • context.SaveChanges() を呼び出さない
  • インクリメント(seqValue.Value++) の後で context.Sequences.Update(seqValue) 実施

transaction.Rollback() を transaction.Commit() に変えると3回目のNext()が3を返すのは意図通りの振る舞いなんですが、Rollback() しても3回目のNext()が3を返すのは不思議です。

また context.Dtabase 経由で SQL を直接実行したときは transaction.Rollback()
が利くことを確認しています:

C#

1using (var context = new TestDBContext(testDB)) 2{ 3 context.Database.ExecuteSqlRaw("INSERT INTO SEQUENCES VALUES ('SEQ1', 1)"); 4 using(var transaction = context.Database.BeginTransaction()) 5 { 6 context.Database.ExecuteSqlRaw("UPDATE SEQUENCES SET VALUE = 2 WHERE NAME='SEQ1'"); 7 transaction.Rollback(); 8 } 9 using (var cmd3 = context.Database.GetDbConnection().CreateCommand()) 10 { 11 cmd3.CommandText = "SELECT VALUE FROM SEQUENCES WHERE NAME='SEQ1'"; 12 var reader = cmd3.ExecuteReader(); 13 reader.Read(); 14 // コンソールには1が出力される:意図通りロールバックできている 15 Console.WriteLine(reader.GetInt32(0)); 16 } 17}

補足情報(FW/ツールのバージョンなど)

Visual Studio 2019、
MSTest を使う単体テストプロジェクトを作成。
コンソールアプリケーションを作成。
ターゲットフレームワークは「.NET 5.0 (現在)」です。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2021/09/16 23:00 編集

単体テストが期待通り動かないことを解決するのが課題なのか、EF Core + SQLite の DbContext.SaveChanges でロールバックすることが課題なのか、どっちでしょう? 後者なら単体テストを外したサンプルコードもアップできませんか? あと、インメモリーデータベースではなく普通にファイルベースにしてください。
Yas.T

2021/09/16 23:47

指摘ありがとうございます。ロールバックが課題です。 ファイルベースで、ということは「ファイルには書き込まれていない(ロールバックされている)」可能性がある、ということでしょうか。 確認してみます。
退会済みユーザー

退会済みユーザー

2021/09/17 00:11

> ファイルベースで、ということは「ファイルには書き込まれていない(ロールバックされている)」可能性がある、ということでしょうか。 分かりませんが、切り分けのため、単体テストのやり方の問題、インメモリーデータベースの影響など普通でないことはすべて排除して試してみるということは必要だと思いますが。 それから、「後者なら単体テストを外したサンプルコードもアップできませんか?」と書いた理由ですが、単体テストとか質問者さん独自の環境・問題含みでは調べてみようと思う人が少なくてレスが付きにくいのではと思うからです。実際自分もコードを読む気力は沸いてきませんし、他の人も同様でそれゆえレスがついてないのではと思います。
Yas.T

2021/09/17 03:05

ご指摘ありがとうございます…そのとおりですね。 元の質問の意図を変えない範囲でコードを見直しました。 また必要と思われるEntityやDbCotnextの定義は、別コードに分けました。
guest

回答3

0

実際のデータベースはロールバックされているがメモリ上のDBSetはロールバックされていないようです。

C#

1var seq3 = context.Sequences.Find(seqName); 2context.Entry(seq3).Reload(); 3seq3.Value++; 4context.SaveChanges(); 5Console.WriteLine(seq3.Value); 6

と再読み込みするようにすると2と出力されます。

投稿2021/09/17 06:03

編集2021/09/17 06:08
YAmaGNZ

総合スコア10489

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Yas.T

2021/09/17 07:43

回答ありがとうございます。Reload() は気づきませんでした。
guest

0

自己解決

SurferOnWww さん、YAmaGNZ さん、その他気にしてくださった皆様、ありがとうございます。

自己解決を含めまとめてみました。

解決策1:Reloadする

C#

1var seq3 = context.Sequences.Find(seqName); 2context.Entry(seq3).Reload(); 3seq3.Value++; 4context.SaveChanges();
解決策2:DbContextの使い回しをやめる

C#

1var seqName = "SEQ1"; 2using (var context = new TestDBContext(testDB)) 3{ 4 var seq1 = new SequenceValue() { Name = seqName, Value = 1 }; 5 context.Sequences.Add(seq1); 6 context.SaveChanges(); 7 // OK: 1 が出力される 8 Console.WriteLine(seq1.Value); 9} 10 11using (var context = new TestDBContext(testDB)) 12using(var transaction = context.Database.BeginTransaction()) 13{ 14 var seq2 = context.Sequences.Find(seqName); 15 seq2.Value++; 16 context.SaveChanges(); 17 // OK: 2 が出力される 18 Console.WriteLine(seq2.Value); 19 transaction.Rollback(); 20} 21 22using (var context = new TestDBContext(testDB)) 23{ 24 var seq3 = context.Sequences.Find(seqName); 25 seq3.Value++; 26 context.SaveChanges(); 27 28 // OK!:ローバックされているので 2 が出力されるはず:意図通り! 29 Console.WriteLine(seq3.Value); 30}

複数の変更をまとめてリロードできるか、等まだわからない部分はありますが、
そこはもう少し自力で調べてみます。
また DbContext の使い回しは意図しない結果を招くことがわかったので
一旦質問をクローズしたいと思います。

ご協力いただいた方々、本当にありがとうございました。

ちなみに SaveChanges() の動作についてですがこんな感じに振る舞うようです。

  1. 自分がトランザクションの外にいるとき:
BEGIN TRANSACTION try { INSERT, UPDATE, DELETE COMMIT } catch(Exception) { ROLLBACK }

2.自分がトランザクションの中にいるとき

SAVEPOINT sp try { INSERT, UPDATE, DELETE RELEASE SAVEPOINT sp } catch(Exception) { ROLLBACK TO SAVEPOINT sp }

DbContext.Savechanges() 呼出時にトランザクションの中にいるかどうかは、
DbContext.Database.CurrentTransaction が null かどうかで判断できます。
(少なくともSQLiteでは。他は試していません。機会があったら確認したいです。)

下記2つのサイト・ページを参考にしました。

「4. データの挿入、読み出し、更新、削除 densan-labs.net」
https://densan-labs.net/tech/codefirst/adddelete.html

"4.6. SaveChangesの動作
SaveChangesは,DbContext内でトラッキングされているオブジェクトのうち, UnchangedまたはDetached状態以外のものを発見すると,それらの情報を反映させるSql文をDBに対して発行します.

SaveChangesはトランザクショナルな関数です. 例えばデータの更新と削除をDbContextに対して行い,SaveChangesを呼び出すとします. SaveChangesを呼び出した段階で,DBに対してUpdate文とDelete文が発行されます. ここで,Updateには成功して,Deleteには失敗した場合,Updateしたデータは自動的に ロールバックされます."

「Transaction in Entity Framework」(Entity Framework Tutorial)
https://www.entityframeworktutorial.net/entityframework6/transaction-in-entity-framework.aspx

"Multiple SaveChanges() calls, create separate transactions, perform CRUD operations and then commit each transaction.
... (中略) ...
In the above example, we create new Standard,Student and Course entities and save them to the database by calling two SaveChanges(), which excute INSERT commands within one transaction."

投稿2021/09/17 08:08

編集2021/09/17 08:11
Yas.T

総合スコア6

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2021/09/18 00:34 編集

> 解決策1:Reloadする 私の回答の追記を見てもらえたでしょうか? DB を直接見ると 3 になっていて、ロールバックはされてないという結果だったのですが。←【訂正】間違ってました。transaction.Rollback(); でロールバックはされていました。下の 2021/09/18 09:30 の私のコメントに書いたキャッシュの問題だったようです。 > 解決策2:DbContextの使い回しをやめる 使いまわしは全く問題ないです。やり方の問題です。 そもそもトランザクション/ロールバックのやり方が普通ではないので、そのあたりから考え直した方が良さそうな気がします。
YAmaGNZ

2021/09/17 12:57

実際にReloadを入れて動作させると var seq3 = context.Sequences.Find(seqName); Console.WriteLine(seq3.Value);  //この時点ではseq3.Valueは2 context.Entry(seq3).Reload(); Console.WriteLine(seq3.Value);  //Reloadするとseq3.Valueは1 という動作をします。
退会済みユーザー

退会済みユーザー

2021/09/18 00:30

今頃になってやっとその意味が分かりました。質問者さんのコードの、 context.Sequences.Add(seq1);  でキャッシュされ、その後 2 箇所ある、 context.Sequences.Find(seqName) ではキャッシュから取得される。ロールバックはキャッシュに反映されないが、コードの Value++ はキャッシュに反映され Modified マークがついて SaveChanges によって Value++ の結果で UPDATE されたということだったのですね。 いろいろ思い違いしてました。訂正しておきます。
guest

0

SaveChanges とトランザクション/ロールバックに関する思い違いがあるような気がします。

トランザクションは、保留中の状態 (BeginTransaction の呼び出し後、Commit の呼び出し前) だけからロールバックできるようになっています。SQL Server は間違いなくそうです。SQLite も多分同じだと思います。

以下の記事は読んでますか?

トランザクションの使用
https://docs.microsoft.com/ja-jp/ef/core/saving/transactions

"既定では、データベース プロバイダーがトランザクションをサポートしている場合は、SaveChanges への 1 回の呼び出しに含まれるすべての変更がトランザクションに適用されます。 いずれかの変更が失敗した場合、トランザクションはロールバックされ、変更は、データベースにまったく適用されません。 つまり、SaveChanges は、完全に成功するか、エラーが発生した場合はデータベースを未変更のままにすることが保証されます。"

質問者さんのコードで 3 つある context.SaveChanges() はすべて「完全に成功」していて、ロールバックは効かないはずです。

【2021/9/18 訂正&追記】ここから

間違ってました。context.SaveChanges() はすべて「完全に成功」していても、2 つ目の SaveChanges を囲っている transaction は SaveChanges でコミットされない(保留中の状態にある)ので transaction.Rollback(); でロールバックされるようです。

コンテキストのキャッシュの問題でした。質問のコードおよび下の【追記】のコードの、

context.Sequences.Add(seq1); 

でキャッシュされ、その後 2 箇所ある、

context.Sequences.Find(seqName)

ではキャッシュから取得される。ロールバックはキャッシュに反映されないが、コードの Value++ の結果の値(1 回目は 2, 2回目は 3)はキャッシュに反映され Modified マークがついて、SaveChanges によって最終的に 3 に UPDATE されたということでした。

【2021/9/18 訂正&追記】ここまで

テストコードを見直して、上に紹介した記事の「トランザクションを制御する」のサンプルコードのようにして、例えば 2 つ目の SaveChanges で失敗するように細工するとロールバックされて、一回目の SaveChanges は無かったことになるはずです。

【2021/9/17 追記】

下のコメント欄で「やってみましたけど、最終結果は 3 ですね。ロールバックはされないようです。後で回答欄に詳細を書いておきます」と書いた件です。

コードは以下の通りで質問者さんのものと同じです。EF Code First でプロジェクトのフォルダに DB を生成したのでその関係がほんのちょっと異なるぐらいです。Visual Studio 2019 のテンプレートで作った .NET 5.0 のコンソールアプリです。

using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; using Microsoft.Data.Sqlite; namespace ConsoleAppSQLite { class Program { static void Main(string[] args) { var seqName = "SEQ1"; using (var context = new TestDBContext()) { var seq1 = new SequenceValue() { Name = seqName, Value = 1 }; context.Sequences.Add(seq1); context.SaveChanges(); // OK: 1 が出力される Console.WriteLine(seq1.Value); using (var transaction = context.Database.BeginTransaction()) { var seq2 = context.Sequences.Find(seqName); seq2.Value++; context.SaveChanges(); // OK: 2 が出力される Console.WriteLine(seq2.Value); transaction.Rollback(); } var seq3 = context.Sequences.Find(seqName); seq3.Value++; context.SaveChanges(); // NG:ローバックされているので 2 が出力されるはず:実際には3が出力される Console.WriteLine(seq3.Value); } } } [Table("SEQUENCES")] public class SequenceValue { [Key, Column("NAME")] public string Name { get; set; } [Column("VALUE")] public int Value { get; set; } } public class TestDBContext : DbContext { //private SqliteConnection _conn; //public TestDBContext(SqliteConnection conn) { _conn = conn; } protected override void OnConfiguring(DbContextOptionsBuilder builder) { var path = @"C:\Users\surfe\Documents\Visual Studio 2019\Core5App\ConsoleAppSQLite\ConsoleAppSQLite\test.db"; var connStr = "Data Source=" + path; builder.UseSqlite(connStr); } public DbSet<SequenceValue> Sequences { get; set; } } }

実行結果は以下の通り(質問者さんと同じ):

イメージ説明

DB Browser for SQLite で結果を見ると 3 です(実行する前は NAME, VALUE ともカラです):

イメージ説明

ロールバックはされてないという結果です。 ← 間違ってました。上の【2021/9/18 訂正&追記】を見てください。ロールバックはされているのですが、キャッシュの問題で最後に 3 に UPDATE されたというのが上の結果です。

投稿2021/09/17 04:42

編集2021/09/18 01:05
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Yas.T

2021/09/17 06:05

ありがとうございます。ご指摘のMicrosof DOCS は読んでいました。 記事の内容ですが、SaveChagnes() を含め try 句内で例外をスローさせることはあまり関係なくて「コミットせずにロールバックしたらトランザクション開始時の状態に戻る」(途中でセーブポイントを作っていたらそのときの状態に戻せる) というデータベースの挙動を説明しているのだと思うのです。 もう一度コードを含め見直してみます。 重ねて、ご指摘ありがとうございました。
退会済みユーザー

退会済みユーザー

2021/09/17 06:20

SaveChanges では Commit したことになってないなら、まだ保留中ということでロールバックはされるかもしれないですね。YAmaGNZ さんが試したところロールバックされたそうですから。自分も試してみます。
退会済みユーザー

退会済みユーザー

2021/09/17 09:06

やってみましたけど、最終結果は 3 ですね。ロールバックはされないようです。後で回答欄に詳細を書いておきます。
YAmaGNZ

2021/09/17 09:59 編集

その結果の”3”ってプログラムを最後まで実行した結果ですか? それならそれで合ってると思います。 ”2”に更新するための2番目のSaveChanges後のロールバックで実際のDBは"1"となっていますが、メモリ上では”2”となっており var seq3 = context.Sequences.Find(seqName); で得られる値は”2”です。 それを+1してSaveChangesしているので最終結果は"3"となります。 質問者さんはロールバックしているのだから var seq3 = context.Sequences.Find(seqName); で得られる値は"1"となりそれを+1するのだから”2”になるのではという話です。 それなのにメモリ上の"2"を取得するので、Reloadで再度DBから取得して値を更新しては?というのが私の回答になります。
退会済みユーザー

退会済みユーザー

2021/09/17 10:34

回答欄に追記した通りです。読んでください。
YAmaGNZ

2021/09/17 10:53 編集

「ロールバックされていない」というのが結論なのですよね? ロールバックされていないということであれば transaction.Rollback(); ここでプログラムを終了したら実DBの値は2ということですか? 「ロールバックされていない」という部分を読んで、変更は全部DBに反映されていると読んだ私が間違っていますか?
退会済みユーザー

退会済みユーザー

2021/09/17 11:02

何を言いたいのか分かりません。回答の追記を読んでください。やったこととその結果が書いてあります。私がやったことは質問者さんがやったことと同じです。その結果が書いてあります。
YAmaGNZ

2021/09/17 11:16

「ロールバックされていない」という表現から transaction.Rollback(); が実行された後のデータはその前の var seq2 = context.Sequences.Find(seqName); seq2.Value++; context.SaveChanges(); という変更が実際のDBに反映されていると解釈したのでこのような質問をした次第です。 実DBは関係なくメモリ上のデータの話であれば「ロールバックされていない」というのであれば理解できます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問