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

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

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

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

.NET

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

C#

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

解決済

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

Yas.T
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つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

3回答

0グッド

0クリップ

1475閲覧

投稿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 (現在)」です。

以下のような質問にはグッドを送りましょう

  • 質問内容が明確
  • 自分も答えを知りたい
  • 質問者以外のユーザにも役立つ

グッドが多くついた質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

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

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

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

下記のような質問は推奨されていません。

  • 間違っている
  • 質問になっていない投稿
  • スパムや攻撃的な表現を用いた投稿

適切な質問に修正を依頼しましょう。

SurferOnWww

2021/09/16 23:00 編集

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

2021/09/16 23:47

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

2021/09/17 00:11

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

2021/09/17 03:05

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

回答3

2

実際のデータベースはロールバックされているがメモリ上の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

総合スコア9439

TN8001, SurferOnWww👍を押しています

良いと思った回答にはグッドを送りましょう。
グッドが多くついた回答ほどページの上位に表示されるので、他の人が素晴らしい回答を見つけやすくなります。

下記のような回答は推奨されていません。

  • 間違っている回答
  • 質問の回答になっていない投稿
  • スパムや攻撃的な表現を用いた投稿

このような回答には修正を依頼しましょう。

回答へのコメント

Yas.T

2021/09/17 07:43

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

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/20 00:06

こちらの回答が他のユーザーから「過去の低評価」という指摘を受けました。

回答へのコメント

SurferOnWww

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 という動作をします。
SurferOnWww

2021/09/18 00:30

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

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
SurferOnWww

総合スコア17328

良いと思った回答にはグッドを送りましょう。
グッドが多くついた回答ほどページの上位に表示されるので、他の人が素晴らしい回答を見つけやすくなります。

下記のような回答は推奨されていません。

  • 間違っている回答
  • 質問の回答になっていない投稿
  • スパムや攻撃的な表現を用いた投稿

このような回答には修正を依頼しましょう。

回答へのコメント

Yas.T

2021/09/17 06:05

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

2021/09/17 06:20

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

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から取得して値を更新しては?というのが私の回答になります。
SurferOnWww

2021/09/17 10:34

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

2021/09/17 10:53 編集

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

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は関係なくメモリ上のデータの話であれば「ロールバックされていない」というのであれば理解できます。

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

ただいまの回答率
86.12%

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

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

質問する

関連した質問

同じタグがついた質問を見る

Entity Framework

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

.NET

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

C#

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