セレクターレイヤーの原則について
学習の目的
この単元を完了すると、次のことができるようになります。
- セレクターレイヤーを使用することの利点を説明する。
- セレクターレイヤーの機能について説明する。
- アプリケーションアーキテクチャおよびプラットフォームにサービスレイヤーがどのように適合するか理解する。
一緒にトレイルを進みましょう
エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 36:04 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
セレクターレイヤーについて知る
このモジュールではここまで、適切な関心の分離を行うことで、大規模で複雑なエンタープライズレベルのコードベースを、明確かつ堅牢で自己文書化された、リファクタリングや機能の進化による変更に適応可能なものにすることに重点を置いてきました。引き続きこの方向性で進めましょう。
この単元ではセレクターパターンについて学習します。このレイヤーのコードは、標準オブジェクトとカスタムオブジェクトからの情報を照会するロジックをカプセル化します。セレクターレイヤーはそのデータをドメインレイヤーとサービスレイヤーのコードに渡します。Apex 一括処理やコントローラーなど、クエリを必要とする他の領域からセレクタークラスを再利用することもできます。
次の図は、パターンクラス階層のどこにセレクタークラスが適合するかを示しています。ほとんどの場合は、ドメインクラスやサービスクラス自体など、サービスレイヤー (サービス境界の背後) 内のクラスによって再利用されます。ただし、UI コントローラーと Apex 一括処理クラスも直接セレクタークラスを利用できます。
セレクターレイヤーには、データベースからレコードを照会するコードが含まれます。SOQL クエリは他のレイヤーに配置することもできますが、コードが複雑になるほど、いくつかの問題が生じる可能性があります。
-
クエリの不整合: 同じクエリを異なる場所から同じ (またはわずかに異なる) 情報または条件に対して実行すると、アプリケーション内に不整合が生じることがあります。たとえば、長い間にコードをコピーしてあちこちに貼り付けたため、適用する必要がある特定の条件が失われる場合などです。クエリを 1 つのロジックとして考えてください。ご存じのように、ロジックをコピーしてあちこちに貼り付けることは好ましくありません。コードでもそれは避けてください。
-
クエリデータの不整合: 照会されたレコードデータ (基本的に sObject) が組織内のロジックの周辺を上下左右に渡され続けると、レコードを受信するコール元のメソッドが不安定になりかねません。たとえば、コードの断片が、Account または Opportunity を受け渡すように要求したが、事前にはどの項目が照会されるか (出力内容) を保証できないと、恐ろしいランタイムエラー
System.SObjectException:SObject row was retrieved via SOQL without querying the requested field:X.Y
(sObject 行が要求された項目 X、Y を照会せずに SOQL を介して取得されました) が発生します。結局、開発者が必要な項目を照会するためだけに、同じレコードに対して別のクエリを繰り返すことになります。または、コードパスの流れで、別のクエリで照会された Account レコードが共有関数に渡されるようにするかもしれません。どちらの方法も適切ではありません。
-
セキュリティの不整合: Salesforce では、すべての Apex コードが実行ユーザーのオブジェクトセキュリティに従うことを義務付けています。Apex はシステムレベルでも実行できるため、開発者の責任でクエリの実行前にセキュリティをチェックする必要があります。よい点は、これを行うのに多くの Apex コードは必要ないことです。ただし、見落とされがちで、単体テストでテストするのは簡単ではありません。これをセレクター内でのみ行うことで開発者の作業がかなり楽になります。
これから説明するセレクターパターンは上記の問題に役立ちます。
セレクターパターン
Martin Fowler は、セレクターパターンのベースであるマッパーパターンを次のように定義しています。
マッパーパターン - 「オブジェクト、データベース、およびマッパー自体の独立性を保ちつつ、オブジェクトとデータベース間でデータを移動するマッパーのレイヤー (473)。」— Fowler, Martin
このパターンではマッパーではなくセレクターという用語を使用します。セレクターのほうが、Salesforce でのマッパーパターン実装の主要な違いがよく反映されています。これは、Stephen Willcock が、このパターンの最初の Apex 実装を開発するときに気付いた違いです。この単元で紹介する fflib_SObjectSelector
基本クラスはこれに基づいています。
違いは、セレクターパターンでは、データベースの結果セットをデータオブジェクトやドメイン表現にさえ対応付けないという点です。Salesforce のコンテキストでは、セレクターパターンの主な役割は sObject レコードを提供し、データベースから照会されたデータをプラットフォームのネイティブな方法で表現することです。場合によっては、この単元で後ほど確認するように、他の Apex データ表現を提供できますが、この場合は基本的にデータの選択のみを行うため、それに合わせてこのパターンの名前が変更されました。
関心の分離の観点では、セレクターの関心は次の機能を提供することです。
-
可視性、再利用性、メンテナンス性 - セレクターは、データベースのクエリロジックを見つけやすくし、メンテナンスを容易にします。たとえば、クエリ条件の更新やスキーマ変更の反映時、より簡単でリスクの少ない方法でよく使用される項目を他のコードベースに追加できます。セレクターでは、動的クエリが作成されていても、項目名へのコンパイル時参照を使用する必要があります。そうすることで、項目が削除されたとき、プラットフォームはコード内に参照が存在していたら削除されないようにします。
-
照会されるデータの予測可能性 - セレクターが何をするかをメソッド名でわかるようにし、何を返すかも明確にします。一貫性のない項目が入力された sObject レコードを返すことが適切なモデルとは限りません。というのは、コール元またはコール元から結果のレコードデータが渡される先にとって、項目単位のデータは明確でないため、ランタイム例外の原因となるからです。セレクターパターンによって、照会されたレコードに照会された最小限の項目セットが含まれていることが保証されます。開発者はこれを「セレクターと組織の他の部分とのコントラクト」の一部と考えることができます。
-
セキュリティ - コール元が (システムレベルのシナリオで) 現在のユーザーコンテキストに適用される共有および権限を執行するセキュリティチェックをオプトインまたはオプトアウトできる手段を提供します。
-
プラットフォームの共鳴 - クエリを可能な限り最適にして、主に条件をセットで表現することで、コール元がセレクターメソッドをコールするときのコードでの一括処理化を促します。より大きなデータセットが含まれ、ヒープが懸念される場合に、照会される項目データの一貫性の必要性と最適な項目データのバランスをセレクターが取れるようにする必要があります。
デザインの考慮事項
セレクタークラスを記述するときには、設計面で次の点を考慮してください。
セレクタークラスの使用
上記の設計上の考慮事項を実現するにはどうすればよいでしょうか? 次の簡単な例では、ドメインクラスインスタンスをレコードで初期化します。このセレクターは、非静的メソッドを使用して機能を提供するため、インスタンスが作成されます。コール元がセレクターインスタンスを保持する場合、必要に応じて設定とキャッシュを実行するための通用範囲があります。
List<Opportunity> opportunities = new OpportunitiesSelector().selectById(opportunityIds));
次のサービスレイヤーコード (前の単元で見た覚えがあるかもしれません) は今回、セレクターを使用してレコードを照会し、ドメインクラスに渡します。ドメインクラスは照会されたレコードに対してドメインロジックを実行します。そのため、インライン SOQL ロジックは、サービスおよびドメインロジックから分離されています。
List<Opportunity> opportunityRecords = new OpportunitiesSelector().selectByIdWithProducts(opportunityIds); Opportunities opportunities = new Opportunities(opportunityRecords); opportunities.applyDiscount(discountPercentage, uow);
次の例では、Apex 一括処理の start メソッドのセレクターが、実行でのレコードの処理を駆動しています。
public Database.QueryLocator start(Database.BatchableContext bc) { return new InvoicesSelector().queryLocatorInvoicesToProcess(); }
セレクタークラスのテスト
当然、セレクタークラスコードは、ドメインまたはサービスレイヤーテストに関連するテストでも対象となります。ただし、より純粋な単体テストを実行することに関心があるなら、セレクタークラスは他のクラスとほぼ同じです。テストデータを作成し、メソッドをコールして結果を確認してください。
@IsTest private static void whenQueryOpportuntiesGetProducts() { // Given Set<Id> testRecordIds = setupTestOpportunities(); // When OpportunitiesSelector selector = new OpportunitiesSelector(); List<Opportunity> opportunities = selector.selectByIdWithProducts(testRecordIds); // Then System.assertEquals(10, opportunities.size()); System.assertEquals(5, opportunities[0].OpportunityLineItems.size()); }
セレクターに QueryLocator メソッドを含めた場合、iterator メソッドをコールして結果のレコードを取得し、セレクターテストで期待されるデータを確認できます。
Database.QueryLocator queryLocator = new ProductsSelector().queryLocatorById(new Set<Id> {product.Id}); Database.QueryLocatorIterator productsIterator = queryLocator.iterator(); Product2 queriedProduct = (Product2) productsIterator.next();
リソース
- GitHub: Apex Enterprise Patterns (Apex エンタープライズパターン)
- Wikipedia: 関心の分離
- ブログ投稿: Martin Fowler: Mapper Pattern (マッパーパターン)
- ブログ投稿: Apex Sharing and Applying to Apex Enterprise Patterns (Apex エンタープライズパターンに対する Apex 共有および適用)
- ブログ投稿: Martin Fowler's Enterprise Architecture Patterns (Martin Fowler のエンタープライズアーキテクチャパターン)