きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × SIer

手探り入門×Entity Framework Core×入れ子のトランザクション

この記事の目的

この記事は、
DBContextを入れ子にして呼び出した時の挙動をまとめること
を目的としています。
基本的に、動作ベースで調べつつ理解していこうというスタンスです。
.net core, ef ともに初心者なので、手始めに、DBContextをDIした時の挙動について調べ始めました!

本題

実行環境

.net core 3.1
Entity Frame work core 3.1.1
Postgres 12

DbContextをインジェクション

呼び出し元→呼び出し先1(FirstService)→呼び出し先2(SecondService)の順番で呼び出した時の挙動の確認。
→結果:DBContext.SaveChanges()をしたタイミングで、コミットが実行される。

呼び出し元

this._firstService.Execute();

呼び出し先

    public interface IFirstService { void Execute(); }
    public class FirstService : IFirstService
    {
        private dotnetContext _dotnetContext;
        private ISecondService _secondService;
        public FirstService(dotnetContext dotnetContext, ISecondService secondService)
        {
            this._dotnetContext = dotnetContext;
            this._secondService = secondService;
        }

        public void Execute()
        {
            this._secondService.Execute();
            this._dotnetContext.TransactTest.Add(new TransactTest()
            {
                Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                Text = "First Insert"
            }); ;
            this._dotnetContext.SaveChanges();
            this._secondService.Execute();
        }
    }

    public interface ISecondService { void Execute(); }
    public class SecondService : ISecondService
    {
        private dotnetContext _dotnetContext;
        public SecondService(dotnetContext dotnetContext) { this._dotnetContext = dotnetContext; }

        public void Execute()
        {
            this._dotnetContext.TransactTest.Add(new TransactTest()
            {
                Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                Text = "Second Insert"
            });
            this._dotnetContext.SaveChanges();
        }
    }

DbContextをインジェクション+呼び出し先すべての処理にトランザクションを張る

呼び出し元→呼び出し先1(ThirdService)→呼び出し先2(FourthService)の順番で呼び出した時の挙動の確認。Third,FourthServiceでDB操作処理をする際、Transactionを張って実行。
→結果:2回目のBeginTransactionする際に、エラー発生
error message = The connection is already in a transaction and cannot participate in another transaction.

呼び出し元

this._thirdService.Execute();

呼び出し先

    public interface IThirdService { void Execute(); }
    public class ThirdService : IThirdService
    {
        private dotnetContext _dotnetContext;
        private IFourthService _fourthService;
        public ThirdService(dotnetContext dotnetContext, IFourthService fourthService)
        {
            this._dotnetContext = dotnetContext;
            this._fourthService = fourthService;
        }

        public void Execute()
        {
            using (var transaction = this._dotnetContext.Database.BeginTransaction())
            {
                this._fourthService.Execute();
                this._dotnetContext.TransactTest.Add(new TransactTest()
                {
                    Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                    Text = "Third Insert"
                });
                this._dotnetContext.SaveChanges();
                transaction.Rollback();
                this._fourthService.Execute();
            }
        }
    }
    public interface IFourthService { void Execute(); }
    public class FourthService : IFourthService
    {
        private dotnetContext _dotnetContext;
        public FourthService(dotnetContext dotnetContext) { this._dotnetContext = dotnetContext; }

        public void Execute()
        {
            using (var transaction = this._dotnetContext.Database.BeginTransaction()) //Error発生!
            {
                this._dotnetContext.TransactTest.Add(new TransactTest()
                {
                    Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                    Text = "Fourth Insert"
                });
                this._dotnetContext.SaveChanges();
            }
        }
    }

DbContextをインジェクション+呼び出し先1つの処理にトランザクションを張る

呼び出し元→呼び出し先1(FifthService)→呼び出し先2(SixthService)の順番で呼び出した時の挙動の確認。FifthServiceでDB操作処理をする際、Transactionを張って実行。
→結果:子のサービスに対してもTransactionが有効になる。
    トランザクションスコープ内でも、Transactionがクローズした後に呼び出された子の処理は別トランザクションとして扱われる。

呼び出し元

this._fifthService.Execute();

呼び出し先

    public interface IFifthService { void Execute(); }
    public class FifthService : IFifthService
    {
        private dotnetContext _dotnetContext;
        private ISixthService _sixthService;
        public FifthService(dotnetContext dotnetContext, ISixthService sixthService)
        {
            this._dotnetContext = dotnetContext;
            this._sixthService = sixthService;
        }

        public void Execute()
        {
            using (var transaction = this._dotnetContext.Database.BeginTransaction())
            {
                this._sixthService.Execute();
                this._dotnetContext.TransactTest.Add(new TransactTest()
                {
                    Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                    Text = "Fifth Insert"
                });
                this._dotnetContext.SaveChanges(); //Commitされない
                transaction.Rollback(); //ここでSixthServiceの変更込みでロールバックされる
                this._sixthService.Execute(); //正常に実行+Commitされる
            }
        }
    }
    public interface ISixthService { void Execute(); }
    public class SixthService : ISixthService
    {
        private dotnetContext _dotnetContext;
        public SixthService(dotnetContext dotnetContext) { this._dotnetContext = dotnetContext; }

        public void Execute()
        {
            this._dotnetContext.TransactTest.Add(new TransactTest()
            {
                Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                Text = "Sixth Insert"
            });
            this._dotnetContext.SaveChanges();//Transactionがロールバックされた後は、実行される
        }
    }

DbContextをインジェクション+呼び出し先1つの処理にトランザクションを張る・その2

呼び出し元→呼び出し先1(SeventhService)→呼び出し先2(EighthService)の順番で呼び出した時の挙動の確認。EighthService(SeventhServiceから呼び出されるやつ)でDB操作処理をする際、Transactionを張って実行。
→結果:2回目のBeginTransactionする際に、エラー発生
error message = The connection is already in a transaction and cannot participate in another transaction.

呼び出し元

this._seventhService.Execute();

呼び出し先

    public interface ISeventhService { void Execute(); }
    public class SeventhService : ISeventhService
    {
        private dotnetContext _dotnetContext;
        private IEighthService _eighthService;
        public SeventhService(dotnetContext dotnetContext, IEighthService eighthService)
        {
            this._dotnetContext = dotnetContext;
            this._eighthService = eighthService;
        }

        public void Execute()
        {
            this._eighthService.Execute();
            this._dotnetContext.TransactTest.Add(new TransactTest()
            {
                Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                Text = "Seventh Insert"
            });
            this._dotnetContext.SaveChanges();
            this._eighthService.Execute();
        }
    }
    public interface IEighthService { void Execute(); }
    public class EighthService : IEighthService
    {
        private dotnetContext _dotnetContext;
        public EighthService(dotnetContext dotnetContext) { this._dotnetContext = dotnetContext; }

        public void Execute()
        {
            using (var transaction = this._dotnetContext.Database.BeginTransaction())
            {
                this._dotnetContext.TransactTest.Add(new TransactTest()
                {
                    Id = int.Parse(DateTime.Now.ToString("hhmmss")),
                    Text = "Eighth Insert"
                });
                this._dotnetContext.SaveChanges(); //↑の変更を保存
                transaction.Rollback(); //ロールバックが実行される
            }
        }
    }

まとめ

Transactionを張らずに、各サービスごとにDBContextをインジェクトした場合、
 →それぞれのサービスで独立してCommit可能
Transactionを張って、DBContextをインジェクトした子サービスを呼び出した場合、
 →呼び出し元のTransactionのCommit、Rollback時に子サービスの処理も含めて処理される。
  =子サービスでTransaction管理できない。
Transactionを張らずに、Transactionを有効にした子サービスを呼びだした場合、
 →それぞれのサービスで独立してCommit可能

参考

qiita.com

docs.microsoft.com