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

Apex でのセレクタレイヤの原則の適用

学習の目的

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

  • セレクタ Apex クラスを作成して効果的に使用する。
  • 一貫した方法で項目がクエリされるようにする。
  • セレクタパターンを使用してサブセレクトおよびクロスオブジェクトクエリを実装する。
  • 独自の方法に加え FieldSet から動的に項目をクエリする。
  • プラットフォームのセキュリティ適用をいつ行うかを制御する。

セレクタクラスの実装

ここでは、このモジュールのまとめとして、セキュリティクラスの詳細とその実装方法を詳しく確認します。このセレクタ実装では、基本クラス fflib_SObjectSelector を使用して、SOQL クエリの作成と実行の簡易化、一貫性の向上、コンプライアンスの強化を図るとともに、開発者が記述する定型コードを減らします。これを動的に行いながらも、クエリされる項目のコンパイルと参照の完全性を確保します。また、次のように、よく使用される便利なクエリ機能も提供します。

  • 整理機能が含まれている。CurrencyIsoCode 項目などの連動項目は、マルチ通貨機能が有効になっている場合にのみ表示されます。

  • システム管理者が FieldSet で定義した項目を (任意で) 追加できる。

  • ユーザにオブジェクトの参照アクセス権がない場合に例外を投げることで、プラットフォームセキュリティを適用する。ユーザが実行している操作の代わりにオブジェクトが間接的にアクセスされているため、コール元のコードがこの機能を迂回する場合は、コンストラクタ引数を介して機能を無効にできます。

以下は fflib_SObjectSelector 基本クラスのメソッドです。これは抽象基本クラスであるため、拡張するには、その前に抽象としてマークされたメソッドを少なくとも実装します。これらのメソッドには A が付けられています。

[リリース] ボタン

以下は、Product2 オブジェクトの基本的な例です。selectSObjectsById メソッドは基本クラスから直接コールできますが、Product2 レコードのリストを返すことを明確にするためにオーバーロードバージョンが実装されます。

public class ProductsSelector extends fflib_SObjectSelector {

    public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField> {
            Product2.Description,
            Product2.Id,
            Product2.IsActive,
            Product2.Name,
            Product2.ProductCode };
    }

    public Schema.SObjectType getSObjectType() {
        return Product2.sObjectType;
    }

    public List<Product2> selectById(Set<ID> idSet) {
        return (List<Product2>) selectSObjectsById(idSet);
    }
}

この例では、selectById メソッドがコールされると、結果として次の SOQL が生成され実行されます。共通する基本クラスの動作が SOQL に挿入されて、一貫した順序が設定されることも確認できます。この例では、選択肢がまだ指定されていないため、デフォルトで Name 項目が並び替え基準になります。

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE id in :idSet
  ORDER BY Name ASC NULLS FIRST

getSObjectFieldList メソッドの実装によって、基本クラスが selectSObjectsById メソッドでクエリできる項目のリストが定義され、実行されたクエリによって 常に一貫してデータが入力される項目を含む sObject が出力されます。データの入力に一貫性のないレコードに伴う潜在的な問題を防止すると、より不安定なコード実行パスに対処できます。

プロのヒント: ここでのトレードオフは、Apex ヒープサイズと、セレクタメソッドのさまざまなコール元が項目を必要とする頻度のバランスです。推奨されるのは、バランスを取ってほとんどのロジックでほとんどの場合最も役に立つ項目のみが含まれるように使用することです。カスタム Apex 型のリストを返す専用メソッドで、ほとんど使用されない項目や大きなテキスト項目を優先的に提供することは避けます (詳細は後述)。

getOrderBy メソッドを上書きして、基本クラスで作成または実行されたすべてのクエリに同じ並び替え基準を共有させることもできます (次の例を参照)。

public override String getOrderBy() {
    return 'IsActive DESC, ProductCode';
}

上記のメソッドが上書きされると、selectById から生成された SOQL は次のようになります。

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE id in :idSet
  ORDER BY IsActive DESC NULLS FIRST , ProductCode ASC NULLS FIRST

カスタムセレクタメソッドの実装

ここまで、抽象メソッドを基本クラスに実装し、基本クラスの selectById メソッドをコールして実行されるクエリにどう影響するかを確認しました。次は、さまざまなクエリを実行する、セレクタクラスのメソッドをいくつか追加して、実行するクエリの条件、選択される項目、その他の要素を変化させます。

カスタムセレクタメソッドを実装しつつ、セレクタで表現される項目と並び順の一貫性を維持するには、すでに上記で実装されたメソッドをコールできます。次の例は、基本的な動的 SOQLの例で、簡単な文字列の書式設定を使用してこれを行う方法を示します。

public List<Opportunity> selectRecentlyUpdated(Integer recordLimit) {
    String query = String.format(
    'select {0} from {1} ' +
    'where SystemModstamp = LAST_N_DAYS:30 ' +
    'order by {2} limit {3}',
    new List<String> {
        getFieldListString(),
        getSObjectName(),
        getOrderBy(),
        String.valueOf(recordLimit)
      }
    );
    return (List<Opportunity>) Database.query(query);
}

SOQL クエリを作成のクエリファクトリアプローチ

String.format を使用する上記のクエリ作成アプローチはうまくいきましたが、クエリが複雑になると、読みづらく、メンテナンスが難しくなります。

fflib_SObjectSelector 基本クラスでは、よりオブジェクト指向のクエリ作成方法も提供しています。この方法では、fflib_QueryFactory クラスが提供するビルダーパターンアプローチを使用します。このクラスの目的は、SOQL ステートメントの動的作成を、従来の文字列連結アプローチよりも堅牢でエラーが発生しにくくすることです。そのメソッド署名は Fluent Design Model に従っています。

fflib_QueryFactory の独自のインスタンスを作成し、そのメソッドをコールしてクエリするオブジェクトと項目を指示できます。ただし、セレクタ基本クラスは、ヘルパーメソッド newQueryFactory を提供してユーザの代わりにこれを実行し、上記で実装したメソッドを利用します。そのインスタンスを条件 (WHERE 句) で下記のようにカスタマイズしてから、SOQL メソッドを使用してファクトリでクエリを作成して従来の方法で実行するように要求できます。

public List<Product2> selectRecentlyUpdated(Integer recordLimit) {   
    return (List<Product2>) Database.query(
        /**
          Query factory has been pre-initialised by calling
          getSObjectFieldList(), getOrderBy() for you.
        */
        newQueryFactory().
        /**
          Now focus on building the remainder of the
          query needed for this method.
        */
        setCondition('SystemModstamp = LAST_N_DAYS:30').
        setLimit(recordLimit).
        // Finally build the query to execute
        toSOQL());
}

カスタムセレクタメソッドが パラメータ 10 でコールされると、次の SOQL が実行されます。

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE SystemModstamp = LAST_N_DAYS:30
  ORDER BY IsActive DESC NULLS FIRST, ProductCode ASC NULLS FIRST
  LIMIT 10

項目の部分的な選択とクロスオブジェクトクエリ

次の例では、false パラメータを newQueryFactory メソッドに渡して、基本クラスに、クエリファクトリインスタンスの作成時に getSObjectFieldList に指定された項目を無視するように指示します。これにより、セレクタメソッドコードが (ここでは Opportunity オブジェクト、関連する Account および User オブジェクトから) 特定の項目を追加してクロスオブジェクトを形成できるようにします。この場合、このメソッドは OpportunitiesSelector クラスにあります。

次の例には他にも違いがあります。それは「特定の項目にのみデータが入力された Opportunity sObject を返して、どの項目にデータが入力されているかはコール元が調べることを期待する」のではないという点です。このメソッドは、各レコードについて小さな Apex 内部クラス OpportunityInfo を作成してクエリされた項目値のみを明示的に公開します。コール元にとっては、こちらの方がはるかに安全で、自己文書化され、緊密なコントラクトになります。

public List<OpportunityInfo> selectOpportunityInfo(Set<Id> idSet) {
    List<OpportunityInfo> opportunityInfos = new List<OpportunityInfo>();
    for(Opportunity opportunity : Database.query(
            newQueryFactory(false).
                selectField(Opportunity.Id).
                selectField(Opportunity.Amount).
                selectField(Opportunity.StageName).
                selectField('Account.Name').
                selectField('Account.AccountNumber').
                selectField('Account.Owner.Name').
                setCondition('id in :idSet').
                toSOQL()))
        opportunityInfos.add(new OpportunityInfo(opportunity));
    return opportunityInfos;
}

public class OpportunityInfo {       
    private Opportunity opportunity;
    public Id Id { get { return opportunity.Id; } }     
    public Decimal Amount { get { return opportunity.Amount; } }        
    public String Stage { get { return opportunity.StageName; } }       
    public String AccountName { get { return opportunity.Account.Name; } }      
    public String AccountNumber { get { return opportunity.Account.AccountNumber; } }       
    public String AccountOwner { get { return opportunity.Account.Owner.Name; } }
    private OpportunityInfo(Opportunity opportunity) { this.opportunity = opportunity; }         
}

プロのヒント: AggregateDatabaseResult を使用して、こうした種類の結果内に含まれる情報に合わせてさらに調整された Apex クラスを返す場合、このオプションの検討が必要になる場合もあります。このアプローチを使用して Apex 内部クラスを急増させることは避けてください。セレクタからのデフォルトの項目選択は、ほとんどの場合、期待した使用予定のものであり、実際の sObject が返されます。メソッド間でこれらのクラスを再利用することも検討してください。

上記のコードは次の SOQL ステートメントを生成します。

SELECT Id, StageName, Account.AccountNumber, Account.Name, Account.Owner.Name
  FROM Opportunity WHERE id in :idSet
  ORDER BY Name ASC NULLS FIRST

FieldSet サポート

fflib_SObjectSelector 基本クラスのもう 1 つの機能は、所定の FieldSet で参照される項目を含めることです。これにより、セレクタは半動的になり、結果を、項目がクエリ済みであることが要求される Visualforce ページとともに使用できるようになります。次の例では、デフォルトで FieldSet 項目が追加されることを制御するために使用されるコンストラクタパラメータによってこれを行う方法を示します。getSObjectFieldSetList メソッドの上書きも必要になります。残りのセレクタは同じです。

public class ProductSelector extends fflib_SObjectSelector {

    public ProductsSelector() {
        super(false);
    }

    public ProductsSelector(Boolean includeFieldSetFields) {
        super(includeFieldSetFields);
    }


    public override List<Schema.FieldSet> getSObjectFieldSetList() {
        return new List<Schema.FieldSet>
                { SObjectType.Product2.FieldSets.MyFieldSet };
    }

    // Reminder of the Selector methods are the same
    // ...
}

次の短い例では、この新しい FieldSet コンストラクタパラメータを使用しています。サンプルコードでは、MyText__c 項目が存在し、FieldSet に追加済みであると想定していますが、それは、そのコンストラクタに true パラメータが渡され、システム管理者がすでに MyFieldSet に追加した項目を基本クラスが動的にコンストラクタに挿入する場合のみです。

// Test data
Product2 product = new Product2();
product.Description = 'Something cool';
product.Name = 'CoolItem';
product.IsActive = true;
product.MyText__c = 'My Text Field';
insert product;                 

// Query (including FieldSet fields)
List<Product2> products =
  new ProductsSelector(true).selectById(new Set<Id> { product.Id });


// Assert (FieldSet has been pre-configured to include MyText__c here)
System.assertEquals('Something cool', products[0].Description);    
System.assertEquals('CoolItem', products[0].Name);     
System.assertEquals(true, products[0].IsActive);       
System.assertEquals('My Text Field', products[0].MyText__c);

カスタムセレクタメソッドでの特定の FieldSet の使用

オブジェクトに複数の FieldSet があり、セレクタクラスレベルで表現されたそれらの FieldSet をすべてのセレクタメソッドには利用させたくない場合があります。代わりに、それらの FieldSet をパラメータとしてセレクタメソッドに渡せるようにして、登録を回避することができます (例を参照)。

public List<Product2> selectById(Set<ID> idSet, Schema.FieldSet fieldSet) {
  return (List<Product2>) Database.query(
    newQueryFactory()
      .selectFieldSet(fieldSet)
      .setCondition('id in :idSet')
      .toSOQL()
  );
}

上級: クロスオブジェクトおよびサブセレクトクエリでの項目リストの再利用

クロスオブジェクト項目およびサブセレクトクエリを利用するカスタムセレクタメソッドの実装時に、子オブジェクトを表す、セレクタクラスのインスタンスを作成することもできます。

このプロセスで子セレクタを使用すると、これらのオブジェクト用に定義したセレクタ項目も利用することになります。それらのレコードがサブセレクトまたはクロスオブジェクトクエリの一部としてクエリされる場合でも同様です。

次の例では、addQueryFactorySubselect および configureQueryFactoryFields 基本クラスのメソッドを利用しています。子オブジェクトセレクタのインスタンスを作成することで、これらのメソッドはセレクタ項目を、最終的にクエリを実行することになる、親オブジェクトのカスタムセレクタメソッドで提供されるクエリファクトリインスタンスに挿入します。

以下の例では、どちらも Opportunities、子 Opportunity Products、および、それぞれの子セレクタクラスを再利用する Product および Pricebook オブジェクトからの関連情報をクエリします。

public List<Opportunity> selectByIdWithProducts(Set<ID> idSet) {

    // Query Factory for this Selector (Opportunity)
    fflib_QueryFactory opportunitiesQueryFactory = newQueryFactory();

    // Add a query sub-select via the Query Factory for the Opportunity Products
    fflib_QueryFactory lineItemsQueryFactory =
        new OpportunityLineItemsSelector().
            addQueryFactorySubselect(opportunitiesQueryFactory);

    // Add cross object query fields for Pricebook Entry, Products and Pricebook
    new PricebookEntriesSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry');
    new ProductsSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Product2');
    new PricebooksSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Pricebook2');

    // Set the condition and build the query
    return (List<Opportunity>) Database.query(
        opportunitiesQueryFactory.setCondition('id in :idSet').toSOQL());
}

この結果が次の SOQL クエリになります。サブセレクトでさえ、OpportunityLineItemsSelector (この単元の例では非表示) で指定されたとおりにデフォルトの並び替え基準を再利用しています。

SELECT
  AccountId, Amount, CloseDate, Description,
  DiscountType__c, ExpectedRevenue, Id, Name,
  Pricebook2Id, Probability, StageName, Type,  
  (SELECT
      Description, Id, ListPrice, OpportunityId,
      PricebookEntryId, Quantity, SortOrder,
      TotalPrice, UnitPrice, PricebookEntry.Id,
      PricebookEntry.IsActive, PricebookEntry.Name,
      PricebookEntry.Pricebook2Id, PricebookEntry.Product2Id,
      PricebookEntry.ProductCode, PricebookEntry.UnitPrice,
      PricebookEntry.UseStandardPrice,
      PricebookEntry.Pricebook2.Description,
      PricebookEntry.Pricebook2.Id,
      PricebookEntry.Pricebook2.IsActive,
      PricebookEntry.Pricebook2.IsStandard,
      PricebookEntry.Pricebook2.Name,
      PricebookEntry.Product2.Description,
      PricebookEntry.Product2.Id,
      PricebookEntry.Product2.IsActive,
      PricebookEntry.Product2.Name,
      PricebookEntry.Product2.ProductCode
     FROM OpportunityLineItems
     ORDER BY SortOrder ASC NULLS FIRST, PricebookEntry.Name ASC NULLS FIRST)
FROM Opportunity WHERE id in :idSet
ORDER BY Name ASC NULLS FIRST

オブジェクトおよび項目レベルセキュリティ適用の制御

デフォルトでは、fflib_SObjectSelector 基本クラスは、実行ユーザにオブジェクトの参照アクセス権がない場合に例外を投げることで、Salesforce オブジェクトにセキュリティを適用します。ただし、このセクションで説明するように、明示的に有効にしない限り、デフォルトで項目レベルセキュリティが適用されることはありません。

以前の単元で言及したドメインレイヤ基本クラスの類似ロジックのように、これはコントローラ、サービス、またはドメインを通じて、オブジェクトへのあらゆる種類のアクセスに適用されます。内部的には、一部のサービスまたはドメインロジックが、必要な権限のないユーザに代わってオブジェクトにアクセスする場合もあります。

ただし、このデフォルトの基本クラスセキュリティ適用を無効にし、自分で実装する場合は、基本クラスコンストラクタの設定パラメータを使用できます。次の例は、各セレクタクラスコンストラクタでこれを行う方法を示しています。独自の基本クラスを作成し、そのクラスをすべてのセレクタクラスについて拡張すると、定型コードの追加を避け、独自のアプリケーションルールに従って標準化できます。

public PricebookEntriesSelector() {
    super(false, // Do not include FieldSet fields
          false, // Do not enforce Object level security
          false); // Do not enforce Field level security
}

コール元で適用を行う場合は、次のようなオーバーロードコンストラクタの追加を検討します。これにより、デフォルトのコンストラクタがデフォルトでセキュリティなしになり、コール元には、他のコンストラクタを使用して、必要に応じてセキュリティ適用の有効化を要求することが求められます。

public PricebookEntriesSelector(Boolean enforceObjectAndFieldSecurity) {
    super(false, // Do not include FieldSet fields
      enforceObjectAndFieldSecurity, enforceObjectAndFieldSecurity);
}

共有ルールのセキュリティ適用の制御

セレクタメソッドで検討が必要なもう 1 つのセキュリティ適用の形式は、共有ルールを適用するかどうかです。サービスレイヤ、Visualforce、または Lightning コントローラクラスの設計上の考慮事項では、with sharing キーワードがベストプラクティスです。これらのクラスに含まれていて、セレクタメソッドをコールするコードは、このコンテキストでも実行されます。これらの規則に従う場合、セレクタコードは、デフォルトで with sharing ルールが適用されて実行されます。

クエリ実行時の WHERE 句で表現されるフィルタ条件と同様に、sharing キーワードは選択されるレコードセットにも影響を与えます。共有ルールに関係なく、すべてのレコードを選択するという要件をカプセル化する場合は、必要な without sharing キーワードでアノテーションが追加された非公開内部クラスを使用することで、明示的 (メソッド名) な内部昇格のパターンに従うことができます。このアプローチでは、この要件をカプセル化し、この動作を呼び出すクラスを人為的に作成してコール元がこの必要性を明示せずにすむようにします。

public class OpportunitiesSelector extends fflib_SObjectSelector {
    public List<Opportunity> selectById(Set<Id> idSet) {
        // This method simply runs in the sharing context of the caller
        // ...
        return opportunities;
    }

    public List<OpportunityInfo> selectOpportunityInfoAsSystem(Set<Id> idSet) {
        // Explicitly run the query in a 'without sharing' context
        return new SelectOpportunityInfo().selectOpportunityInfo(this, idSet);
    }

    private without sharing class SelectOpportunityInfo {
        public List<OpportunitiesSelector.OpportunityInfo> selectOpportunityInfo(OpportunitiesSelector selector, Set<Id> idSet) {
            // Execute the query as normal
            // ...
           return opportunityInfos;             
        }
    }
}

リソース