進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

セレクタレイヤの原則について

学習の目的

この単元を完了すると、次のことができるようになります。

  • セレクタレイヤを使用することの利点を説明する。
  • セレクタレイヤの機能について説明する。
  • アプリケーションアーキテクチャおよびプラットフォームにサービスレイヤがどのように適合するか理解する。

概要

このモジュールではここまで、適切な関心の分離を行うことで、大規模で複雑なエンタープライズレベルのコードベースを、明確かつ堅牢で自己文書化された、リファクタリングや機能の進化による変更に適応可能なものにすることに重点を置いてきました。引き続きこの方向性で進めましょう。

この単元ではセレクタを紹介します。このレイヤのコードは、カスタムオブジェクトからの情報をクエリしてドメインレイヤとサービスレイヤのコードに渡すロジックをカプセル化します。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)。」Martin Fowler、『エンタープライズアプリケーションアーキテクチャパターン』

このモジュールではマッパーではなくセレクタという用語を使用します。セレクタの方が、Force.com でのマッパーパターン実装の主要な違いがよく反映されています。これは、Stephen Willockが、このパターンの最初の Apex 実装を開発するときに気付いた違いです。この単元で紹介する fflib_SObjectSelector 基本クラスはこれに基づいています。

違いは、セレクタパターンでは、データベースの結果セットをデータオブジェクトやドメイン表現にさえ対応付けないという点です。Force.com のコンテキストでは、セレクタパターンの主な役割は sObject オブジェクトを提供し、データベースからクエリされたデータをプラットフォームのネイティブな方法で表現することです。場合によっては、この単元で後ほど確認するように、他の Apex データ表現を提供できますが、この場合は基本的にデータの選択のみを行うため、それに合わせてこのパターンの名前が変更されました。

SOC の観点では、セレクタの関心は次の機能を提供することです。

  • 可視性、再利用性、メンテナンス性 - セレクタは、データベースのクエリロジックを見つけやすくし、メンテナンスを容易にします。たとえば、クエリ条件の更新やスキーマ変更の反映時、より簡単でリスクの少ない方法でよく使用される項目を他のコードベースに追加できます。セレクタでは、動的クエリが作成されていても、項目名へのコンパイル時参照を使用する必要があります。そうすることで、項目が削除されたとき、プラットフォームはコード内に参照が存在していたら削除されないようにします。

  • クエリされるデータの予測可能性 - セレクタが何をするかをメソッド名でわかるようにし、何を返すかも明確にします。部分的に入力された sObject を返すことが適切なモデルとは限りません。というのは、コール元またはコール元から結果のレコードデータが渡される先にとって、項目単位のデータは明確でないため、ランタイム例外の原因となるからです。

  • セキュリティ - コール元が (システムレベルのシナリオで) 現在のユーザコンテキストに適用される共有および権限を執行するセキュリティチェックをオプトインまたはオプトアウトできる手段を提供します。

  • プラットフォームの共鳴 - クエリを可能な限り最適にして、主に条件をリストで表現することで、コール元がセレクタメソッドをコールするときのコードでの一括処理化を促します。より大きなデータセットが含まれ、ヒープが懸念される場合に、クエリされる項目データの一貫性の必要性と最適な項目データのバランスをセレクタが取れるようにする必要があります。

デザインの考慮事項

セレクタクラスを記述するときには、設計面で次の点を考慮してください。

  • 命名 - セレクタ機能をオブジェクト種別ごとにグループ化します。ドメインパターンの単元で説明した命名規則に従う場合、オブジェクトの複数形の名前 (ProductsSelector や OpportunitiesSelector など) を使用することで、セレクタコードとドメインコードを一緒にグループ化できます。これは厳格なルールではなく、ベストプラクティスです。たとえば、アプリケーションで作成する特定のエンジンまたはコアモジュールのためにクエリ機能をカプセル化するセレクタクラスを作成することができます。

  • メソッド - メソッドを静的またはインスタンスのスコープにできます。後者の場合は、基本クラスと共通の動作の継承が許可されます (下記を参照)。メソッド名 (通常はプレフィックスが select) が情報と関連する子情報を示すことが理想的です。たとえば、selectById や selectByIdWithProductLines などです。

  • メソッドパラメータ - 常に、クエリが使用する条件すべてのリストにします。サービスおよびドメインクラスロジックのルールでは一括処理化が鍵となるため、セレクタクラスのコントラクトのこの部分も従います。

  • sObject リストを返す - ほとんどの場合、セレクタメソッドは sObject リストを返す必要があります。Apex では確定的な対応付けとセットがサポートされるようになりましたが、どちらも並び替え順序の反映には使用できません。解決策として、selectByIdMap および selectByIdList という 2 つのメソッドバリアントを提供するか、selectById のみにしてコール元が対応付けで返されたリストを必要に応じてラップできるようにします。次に例を示します。

  Map<Id, Account> accountsById =
    new Map<Id, Account>( accountsSelector.selectByIds(accountIds) );
  • QueryLocators を返す - Apex 一括処理を実装する場合、start メソッドにインラインでクエリを実装したくなるかもしれませんが、これはカプセル化と再利用のルールに反します。代わりに、セレクタメソッドで QueryLocator を返すことを検討してください。次に例を示します。
  accountSelector.selectByIdsWithQueryLocator(accountIds)
  • カスタム論理レコードセットを返す - 場合によっては、sObject インスタンスを介して Account、Opportunity、MyObject__c の物理表現を返すことが、クエリされた項目を反映する最適な方法ではないことがあります。数項目のみを (ヒープやビューステートの問題を回避する目的などで) クエリする場合、さまざまなリレーションから項目を選択する場合、または集計クエリを実行する場合、カスタム Apex クラス (Java では POJO と呼ばれる) を使用してクエリ結果のラッパーを実装し、クエリ結果の項目をクエリされた具体的な情報のより論理的な表現としてさらに明示的に公開することを検討してください。

  • セキュリティ - セレクタ Apex クラスでの with sharing または without sharing キーワードの使用を避け、コール元のコンテキストでこのコンテキストが継承されるようにします。通常、これはサービスクラスであり、その設計ガイドラインによると、with sharing を指定する必要があります。SOQL クエリがユーザの表示対象範囲外のレコードにアクセスする必要がある場合、こうしたコードは、実行コンテキストを明示的に昇格して、できる限り短時間でアクセスできるようにする必要があります。適切なアプローチは、without sharing 修飾子を適用して非公開 Apex 内部クラスを使用することです。次の単元では、実例を挙げてこれを説明します。

セレクタクラスの使用

これを実際に行うにはどうすればよいでしょうか? 次の簡単な例では、ドメインクラスインスタンスをレコードで初期化します。このセレクタは、非静的メソッドを使用して機能を提供するため、インスタンスが作成されます。コール元がセレクタインスタンスを保持する場合、必要に応じて設定とキャッシュを実行するためのスコープがあります。

List<Opportunity> opportunities =
   new OpportunitiesSelector().selectById(opportunityIds));

次のサービスレイヤコード (前の単元で見た覚えがあるかもしれません) は今回、セレクタを使用してレコードをクエリし、ドメインクラスに渡します。ドメインクラスはクエリされたレコードに対してドメインロジックを実行します。そのため、SOQL ロジックは、サービスおよびドメインロジックから分離されています。

Opportunities opportunities = new Opportunities(
    new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));
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();

リソース