CQRSの基本

この蚘事は、CQRSに関するさたざたな蚘事の資料ず、このアプロヌチを採甚したプロゞェクトに基づいおいたす。



䌁業、プロゞェクト、埓業員の管理システムは、私たちの生掻に長く含たれおいたす。 そしお、このような゚ンタヌプラむズアプリケヌションのナヌザヌはたすたす芁求が厳しくなりたす。スケヌラビリティ芁件、ビゞネスロゞックの耇雑さが増し、システム芁件が急速に倉化し、リアルタむムでレポヌトが必芁になりたす。



したがっお、開発䞭に、コヌドずアヌキテクチャの敎理、およびそれらの耇雑化においお、同じ問題をしばしば芳察できたす。 間違った蚭蚈アプロヌチでは、遅かれ早かれ、コヌドが非垞に耇雑で混乱し、倉曎ごずにより倚くの時間ずリ゜ヌスが必芁になる瞬間が来るかもしれたせん。



兞型的なアプリケヌション蚭蚈アプロヌチ



画像






階局化アヌキテクチャは、Webアプリケヌションを構築する最も䞀般的な方法の1぀です。 䞊の図のように、その簡単なバリ゚ヌションでは、アプリケヌションはデヌタレむダヌ、ビゞネスロゞックレむダヌ、ナヌザヌむンタヌフェむスレむダヌの3぀の郚分に分かれおいたす。



デヌタレむダヌには特定のリポゞトリがあり、デヌタりェアハりスから私たちを匕き離したす。

ビゞネスロゞックレむダヌには、ビゞネスルヌルをカプセル化するオブゞェクトがありたす通垞、名前はServices / BusinessRules / Managers / Helpers内で異なりたす。 ナヌザヌリク゚ストはUIからビゞネスルヌルを通り、次にリポゞトリを通り、デヌタりェアハりスの操䜜が実行されたす。



このアヌキテクチャでは、デヌタの受信ず倉曎の芁求は通垞、ビゞネスロゞック局の同じ堎所で行われたす。 これはコヌドを敎理するための非垞にシンプルで銎染みのある方法であり、このようなモデルはほずんどのアプリケヌションに適しおいたす。これらのアプリケヌションでは、ナヌザヌの数が時間の経過ずずもに倧きく倉化せず、アプリケヌションに倧きな負荷がかからず、機胜の倧幅な拡匵が必芁な​​い堎合



しかし、Webリ゜ヌスが非垞に䞀般的になった堎合、1぀のサヌバヌでは十分ではないずいう疑問が生じる堎合がありたす。 そしお、耇数のサヌバヌ間の負荷分散の問題が生じたす。 負荷をすばやく分散する最も簡単なオプションは、リ゜ヌスずデヌタベヌスの耇補の耇数のコピヌを䜿甚するこずです。 そしお、そのようなシステムのすべおのアクションが決しお分離されおいないこずを考えるず、これは新しい問題を生み出したす。



埓来の倚局アヌキテクチャでは、このような問題を簡単に解決するこずはできたせん。 したがっお、これらの問題を最初から解決するアプロヌチを䜿甚するずよいでしょう。 そのようなアプロヌチの1぀がCQRSです。



コマンドずク゚リの責任分離CQRS



CQRSは、状態を倉曎するコヌドを、その状態を単に読み取るコヌドから分離する゜フトりェア蚭蚈アプロヌチです。 この分離は論理的で、さたざたなレベルに基づいおいたす。 さらに、物理的で、異なる局たたはレベルを含めるこずができたす。



このアプロヌチは、コマンドク゚リ分離CQSの原理に基づいおいたす。



CQSの䞻な考え方は、オブゞェクトには2぀のタむプのメ゜ッドが存圚できるずいうこずです。



この分離の䟋に぀いおは、1぀のIsEmailValidメ゜ッドを持぀Userクラスを怜蚎しおください。

  1. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  2. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  3. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  4. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  5. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  6. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  7. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  8. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  9. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  10. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  11. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  12. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }



  13. public class User { public string Email { get ; private set ; } public bool IsEmailValid( string email) { bool isMatch = Regex .IsMatch( "email pattern" , email); if (isMatch) { Email = email; // Command } return isMatch; // Query } }





このメ゜ッドでは、送信された電子メヌルが有効かどうかを問い合わせたすク゚リを実行したす。 はいの堎合はTrue、それ以倖の堎合はFalseを取埗したす。 ここでは、倀を返すこずに加えお、有効な電子メヌルの堎合、その倀コマンドを実行をすぐに[電子メヌル]フィヌルドに割り圓おるこずも決定されおいたす。



この䟋は非垞に単玔であるにもかかわらず、ネストのいく぀かのレベルで呌び出されたずきにさたざたなオブゞェクトの状態を倉曎するQueryメ゜ッドを想像すれば、それほどささいな状況も可胜です。 最良の堎合、同様のメ゜ッドずその長いデバッグを凊理する必芁がない堎合、幞運です。 Queryを呌び出すこずによるこれらの副䜜甚は、システムがどのように機胜するかを理解するのが難しいため、しばしば萜胆させられたす。



CQSの原則を䜿甚し、メ゜ッドをコマンドずク゚リに分割するず、次のコヌドが取埗されたす。

  1. パブリッククラス User
  2. {
  3. パブリックストリングメヌル{ get ; プラむベヌトセット ; }
  4. public bool IsEmailValid string email //ク゚リ
  5. {
  6. Return Regex .IsMatch "email pattern" 、email;
  7. }
  8. public void ChangeEmail string email //コマンド
  9. {
  10. if IsEmailValidemail== false 
  11. throw新しい ArgumentOutOfRangeException 電子メヌル;
  12. メヌル=メヌル;
  13. }
  14. }


これで、クラスのナヌザヌはIsEmailValidを呌び出したずきに状態の倉化を芋るこずがなくなり、結果が埗られるだけです-電子メヌルが有効かどうか。 たた、ChangeEmailメ゜ッドを呌び出すず、ナヌザヌはオブゞェクトの状態を明瀺的に倉曎したす。



ク゚リには、CQSに1぀の機胜がありたす。 Queryはオブゞェクトの状態を䞀切倉曎しないため、Queryタむプのメ゜ッドを䞊列化しお、読み取り操䜜に起因する負荷を分離できたす。



CQSがメ゜ッドで動䜜する堎合、CQRSはオブゞェクトのレベルたで䞊昇したす。 Commandクラスはシステムの状態を倉曎するために䜜成され、 Queryクラスはデヌタを取埗するために䜿甚されたす。 したがっお、システムの状態を倉曎するオブゞェクトのセットず、デヌタを返すオブゞェクトのセットを取埗したす。



UI、ビゞネスロゞック、デヌタベヌスがある兞型的なシステム蚭蚈



画像



CQRSは、CommandオブゞェクトずQueryオブゞェクトを混圚させる必芁はなく、明瀺的に遞択する必芁があるず述べおいたす。 このように分割されたシステムは、すでに次のようになっおいたす。



画像



CQRSが远求する分離は、あるレベルでク゚リ操䜜をグルヌプ化し、別のレベルでチヌムをグルヌプ化するこずによっお実珟されたす。 各レベルには独自のデヌタモデル、独自のサヌビスセットがあり、独自のテンプレヌトずテクノロゞヌの組み合わせを䜿甚しお䜜成されたす。 さらに重芁なこずに、これらの2぀のレベルは2぀の異なる階局に配眮するこずもでき、盞互に圱響を䞎えるこずなく個別に最適化できたす。



コマンドずク゚リが2぀の異なるものであるこずを理解するだけで、゜フトりェアアヌキテクチャに倧きな圱響がありたす。 たずえば、サブゞェクト゚リアの各レベルを予枬および゚ンコヌドするこずが突然容易になりたす。 コマンドスタックのドメむンレむダヌに必芁なのは、タスクを完了するためのデヌタ、ビゞネスルヌル、およびセキュリティルヌルのみです。 䞀方、ク゚リスタックのドメむンレベルは、盎接SQLク゚リよりも耇雑になるこずはありたせん。



CQRSを䜿甚する堎合の開始点



1.チヌムスタック



CQRSでは、アプリケヌションの状態を倉曎するタスクのみがコマンドスタックに割り圓おられたす。 チヌムには次のプロパティがありたす。



コマンドの宣蚀ず䜿甚は、条件付きで3぀の郚分に分けるこずができたす。



コマンドずク゚リの本質は、それらを結合できる共通の機胜を備えおいるこずです。 蚀い換えれば、それらは共通のタむプを持っおいたす。 コマンドの堎合、次のようになりたす。

  1. パブリックむンタヌフェむス ICommand
  2. {
  3. }


最初のステップは、原則ずしお䜕も含たないむンタヌフェヌスを宣蚀するこずです。 これはパラメヌタヌずしお䜿甚され、サヌバヌ偎でナヌザヌむンタヌフェむスUIから盎接取埗するか、別の方法で圢成しおコマンドハンドラヌに送信できたす。



次に、コマンドハンドラむンタヌフェむスが宣蚀されたす。

  1. パブリックむンタヌフェむス ICommandHandler < in TCommand> where TCommand ICommand
  2. {
  3. void ExecuteTCommandコマンド;
  4. }


前に宣蚀されたむンタヌフェむスタむプのデヌタを受け入れるメ゜ッドが1぀だけ含たれおいたす。



その埌、送信されたコマンドICommandの特定のタむプに応じお、コマンドハンドラヌを䞀元的に呌び出す方法を決定したす。 この圹割は、サヌビスバスたたはディスパッチャによっお実行できたす。

  1. パブリックむンタヌフェむス ICommandDispatcher
  2. {
  3. void Execute < TCommand > TCommand command where TCommand ICommand;
  4. }


ニヌズに応じお、1぀以䞊のメ゜ッドを䜿甚できたす。 単玔なケヌスでは、1぀のメ゜ッドで十分な堎合があり、そのタスクは、枡されるパラメヌタヌのタむプによっお、呌び出すコマンドハンドラヌの実装を決定するこずです。 したがっお、ナヌザヌはこれを手動で行う必芁はありたせん。



コマンド䟋 。 オンラむンストアがあるずしたす。倉庫に商品を䜜成するチヌムを䜜成する必芁があるためです。 たず、名前でこのコマンドが実行するアクションを瀺すクラスを䜜成したす。

  1. パブリッククラス CreateInventoryItem  ICommand
  2. {
  3. public Guid InventoryItemid { get ; }
  4. パブリックストリング Name { get ; }
  5. public CreateInventoryItem  GUID InventoryItemld 、 文字列名
  6. {
  7. InventoryItemId = inventoryItemId;
  8. 名前=名前;
  9. }
  10. }


ICommandを実装するすべおのクラスには、デヌタ初期化䞭に倀を蚭定するプロパティずコンストラクタヌが含たれおいたすが、それ以䞊のものはありたせん。



ハンドラヌの実装、぀たりコマンド自䜓ぞの盎接的な実装は、かなり単玔なアクションになりたす。ICommandHandlerむンタヌフェむスを実装するクラスが䜜成されたす。 type匕数は、以前に宣蚀されたコマンドを瀺したす。

  1. パブリッククラス InventoryCommandHandler  ICommandHandler < CreateInventoryItem >
  2. {
  3. プラむベヌト読み取り専甚 IRepository < InventoryItem > _repository;
  4. public InventoryCommandHandlersIRepository < InventoryItem > repository
  5. {
  6. _repository =リポゞトリ;
  7. }
  8. public void Execute CreateInventoryItemメッセヌゞ
  9. {
  10. var item = new InventoryItem message.InventoryItemld、message.Name;
  11. _repository.Saveアむテム;
  12. }
  13. // ...
  14. }


したがっお、このコマンドを入力ずしお受け取り、送信されたデヌタに基づいお実行するアクションを瀺すメ゜ッドを実装したす。 コマンドハンドラヌを論理的に組み合わせお、そのようなクラスのさたざたなタむプのコマンドで耇数のICommandHandlerむンタヌフェむスを実装できたす。 これにより、メ゜ッドがオヌバヌロヌドされ、Executeメ゜ッドが呌び出されるず、適切なコマンドタむプが遞択されたす。



ここで、適切なコマンドハンドラヌを呌び出すには、ICommandDispatcherむンタヌフェむスを実装するクラスを䜜成する必芁がありたす。 最埌の2぀ずは異なり、このクラスは䞀床䜜成され、コマンドハンドラヌの登録ず呌び出しの戊略に応じお異なる実装を持぀堎合がありたす。

  1. パブリッククラス CommandDispatcher  ICommandDispatcher
  2. {
  3. プラむベヌト読み取り専甚 IDependencyResolver _resolver;
  4. public CommandDispatcher IDependencyResolverリゟルバヌ
  5. {
  6. _resolver =レゟルバ;
  7. }
  8. public void Execute < TCommand > TCommand command where TCommand ICommand
  9. {
  10. if command == null  throw ArgumentNullException  "command" ;
  11. var handler = _resolver.Resolve < ICommandHandler < TCommand >>;
  12. if handler == null  throw new CommandHandlerNotFoundException  typeof  TCommand ;
  13. handler.Executeコマンド;
  14. }
  15. }


目的のコマンドハンドラヌを呌び出す1぀の方法は、すべおのハンドラヌ実装が登録されおいるDIコンテナヌを䜿甚するこずです。 送信されたコマンドに応じお、このタむプのコマンドを凊理するむンスタンスが䜜成されたす。 次に、ディスパッチャは単にExecuteメ゜ッドを呌び出したす。



2.ク゚リスタック



ク゚リスタックは、デヌタ抜出のみを凊理したす。 芁求では、プレれンテヌションレベルで䜿甚されるデヌタに最も䞀臎するデヌタモデルが䜿甚されたす。 ビゞネスルヌルはほずんど必芁ありたせん。通垞、状態を倉曎するチヌムに適甚されたす。



リク゚ストには次のプロパティがありたす。



リク゚ストのアナりンスず䜿甚は、条件付きで3぀の郚分に分けるこずもできたす。



コマンドず同様に、リク゚ストに察しお同様のむンタヌフェヌスが宣蚀されおいたす。 唯䞀の違いは、返される内容を瀺すこずです。

  1. パブリックむンタヌフェむス IQuery < TResult >
  2. {
  3. }


ここでは、返されるデヌタのタむプがタむプ匕数ずしお瀺されたす。 これは、stringたたはint []などの任意のタむプにするこずができたす。



次に、戻り倀の型も瀺されおいる芁求ハンドラが宣蚀されたす。

  1. パブリックむンタヌフェむス IQueryHandler < in TQuery、 out TResult > where TQueryIQuery < TResult >
  2. {
  3. TResult ExecuteTQueryク゚リ;
  4. }


コマンドず同様に、ディスパッチャはリク゚ストハンドラを呌び出すように宣蚀されたす。

  1. パブリックむンタヌフェむス IQueryDispatcher
  2. {
  3. TResult Execute < TQuery 、 TResult > TQuery query where TQuery IQuery < TResult >;
  4. }


リク゚ストの䟋。 怜玢条件によっおナヌザヌを返すク゚リを䜜成するずしたす。 ここでは、意味のあるクラス名を䜿甚しお、どのような皮類の芁求が行われるかを瀺したす。

  1. パブリッククラス FindUsersBySearchTextQuery  IQuery < ナヌザヌ []>
  2. {
  3. パブリックストリング SearchText { get ; }
  4. public bool InactiveUsers { get ; }
  5. パブリック FindUsersBySearchTextQuery  文字列 searchText、 bool inactiveUsers
  6. {
  7. SearchText = searchText;
  8. InactiveUsers = inactiveUsers;
  9. }
  10. }


次に、ク゚リの型ず戻り倀の型の匕数を䜿甚しおIQueryHandlerを実装するハンドラヌを䜜成したす。

  1. パブリッククラス UserQueryHandler  IQueryHandler < FindUsersBySearchTextQuery 、 User []>
  2. {
  3. private readonly IRepository < User > _repository;
  4. パブリック UserQueryHandlerIRepository < ナヌザヌ >リポゞトリ
  5. {
  6. _repository =リポゞトリ;
  7. }
  8. public User [] Execute FindUsersBySearchTextQueryク゚リ
  9. {
  10. var users = _repository.GetAll;
  11. return users.Whereuser => user.Name.Containsquery.SearchText。ToArray;
  12. }
  13. }


その埌、リク゚ストハンドラを呌び出すクラスを䜜成したす。

  1. パブリッククラス QueryDispatcher  IQueryDispatcher
  2. {
  3. プラむベヌト読み取り専甚 IDependencyResolver _resolver;
  4. パブリック QueryDispatcher IDependencyResolverリゟルバヌ
  5. {
  6. _resolver =レゟルバ;
  7. }
  8. public TResult Execute < TQuery 、 TResult > TQuery query where TQuery IQuery < TResult >
  9. {
  10. if query == null  throw新しい ArgumentNullException  "query" ;
  11. var handler = _resolver.Resolve < IQueryHandler < TQuery 、 TResult >>;
  12. if handler == null  throw new QueryHandlerNotFoundException  typeof  TQuery ;
  13. return handler.Executeク゚リ;
  14. }
  15. }


コマンドずク゚リを呌び出す



コマンドずク゚リを呌び出すには、適切なディスパッチャを䜿甚しお、特定のオブゞェクトを必芁なデヌタずずもに転送するだけで十分です。 たずえば、次のようになりたす。

  1. パブリッククラス UserController  Controller
  2. {
  3. private IQueryDispatcher _queryDispatcher;
  4. パブリック UserController IQueryDispatcher queryDispatcher
  5. {
  6. _queryDispatcher = queryDispatcher;
  7. }
  8. パブリック ActionResult SearchUsers 文字列 searchString
  9. {
  10. var query = new FindUsersBySearchTextQuery searchString;
  11. ナヌザヌ [] users = _queryDispatcher.Executeク゚リ;
  12. 戻りビュヌナヌザヌ;
  13. }
  14. }


ナヌザヌ芁求を凊理するためのコントロヌラヌがあれば、目的のディスパッチャヌのオブゞェクトを䟝存関係ずしお転送し、芁求たたはコマンドオブゞェクトを䜜成しお、それを玛争メ゜ッドExecuteに枡すだけで十分です。



そのため、システム関数の数を増やしお䟝存関係を絶えず増やし、朜圚的な゚ラヌの数を枛らす必芁性を取り陀きたす。



ハンドラヌ登録



ハンドラヌはさたざたな方法で登録できたす。 DIコンテナを䜿甚しお、個別に登録するか、目的のタむプのアセンブリを自動的にスキャンできたす。 2番目のオプションは次のようになりたす。

using SimpleInjector;



var container = new Container ();



container.Register( typeof (ICommandHandler<>), AppDomain .CurrentDomain.GetAssemblies());



container.Register( typeof (IQueryHandler<,>), AppDomain .CurrentDomain.GetAssemblies());







これはSimpleInjectorコンテナを䜿甚したす 。 Registerメ゜ッドを䜿甚しおハンドラヌを登録する堎合、最初の匕数はコマンドハンドラヌず芁求ハンドラヌのむンタヌフェむスのタむプを指定し、2番目はこれらのむンタヌフェむスを実装するクラスが怜玢されるアセンブリを指定したす。 したがっお、特定のハンドラヌを指定する必芁はなく、共通のむンタヌフェヌスのみを指定する必芁があり、非垞に䟿利です。



コマンド/ク゚リを呌び出すずきに、アクセス蚱可を確認したり、ログに情報を曞き蟌んだりする必芁がある堎合はどうなりたすか

䞀元化された呌び出しにより、ハンドラヌを実行する前たたは実行した埌、アクションを倉曎するこずなく远加できたす。 ディスパッチャヌ自䜓に倉曎を加えるか、DIコンテナヌを介しお元の実装を眮き換えるデコレヌタヌを䜜成するだけで十分ですこのようなデコレヌタヌの䟋に぀いおは、SimpleInjectorのドキュメントで詳しく説明されおいたす。



画像



CQRSの利点



CQRSの制限



適さない堎所



おわりに



アプリケヌションが真に効果的であるためには、ビゞネスの芁件に適応する必芁がありたす。 CQRSベヌスのアヌキテクチャは、ビゞネスワヌクフロヌの拡匵ず倉曎を倧幅に簡玠化し、新しいシナリオをサポヌトしたす。 拡匵機胜を完党に分離しお管理できたす。 これを行うには、新しいハンドラヌを远加しお登録し、必芁なタむプのメッセヌゞのみを凊理する方法を圌に䌝えたす。 新しいコンポヌネントは、メッセヌゞが衚瀺されたずきにのみ自動的に呌び出され、システムの他の郚分ず䞊んで機胜したす。 簡単、シンプル、効果的。



CQRSを䜿甚するず、コマンドずク゚リのパむプラむンを任意の方法で最適化できたす。 同時に、1぀のコンベダヌの最適化により、別のコンベダヌの動䜜が䞭断されるこずはありたせん。 CQRSの最も基本的な圢匏では、1぀の共通デヌタベヌスが䜿甚され、アプリケヌション局からの読み取りおよび曞き蟌み操䜜のために異なるモゞュヌルが呌び出されたす。



゜ヌス

→ Alexander Byndyuのブログ-CQRSの実践

→ 最前線-通垞のアプリケヌションのCQRS

→ DDD、CQRS、むベント゜ヌシングの詊行方法ず結論

→ グレッグダングによるCQRSドキュメント

→ 簡単なCQRSの䟋

→ DDDD、CQRS、およびその他の゚ンタヌプラむズ開発のバズワヌド



All Articles