Skip to main content

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

学習の目的

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

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

メモ

日本語で受講されている方へ
このバッジの Trailhead ハンズオン Challenge は英語で受講していただく必要があります。英語の意味についてはかっこ内の翻訳をご参照ください。Trailhead Playground では (1) [Locale (地域)] を [United States (米国)] に、[Language (言語)] を [English (英語)] に切り替えて、(2) 英語の値のみをコピーして貼り付けてください。こちらの指示に従ってください。

翻訳版 Trailhead を活用する方法の詳細は、自分の言語の Trailhead バッジを参照してください。

一緒にトレイルを進みましょう

エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。

(この動画は 55:16 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)

セレクタークラスの実装

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

  • 整理機能が含まれている。CurrencyIsoCode 項目などの連動項目は、マルチ通貨機能が有効になっている場合にのみ表示されます。
  • システム管理者が FieldSet で定義した項目を (任意で) 追加できる。
  • ユーザーにオブジェクトの参照アクセス権がない場合に例外を投げることで、プラットフォームセキュリティを適用する。ユーザーが実行している操作の代わりにオブジェクトが間接的にアクセスされているため、コール元のコードがこの機能を迂回する場合は、コンストラクター引数を介して機能を無効にできます。

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

fflibSObjectSelector 基本クラスのメソッド

実装する必要がある抽象メソッドは次のとおりです。

  • abstract Schema.SObjectType getSObjectType();
  • abstract List<Schema.SObjectField> getSObjectFieldList();

Product2 オブジェクトのセレクタークラスの基本的な例を次に示します。selectSObjectsById() メソッドは基本クラスから直接コールできますが、Product2 レコードのリストを返すことを明確にするために selectById() が実装されることが一般的です。

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,
            Product2.DiscountingApproved__c};
    }
    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() メソッドで照会できる項目のリストが定義され、照会されたレコードに基本的な項目が含まれ、常に一貫してデータが入力されていることが保証されます。これによって、レコードに入力されているデータに一貫性がないためにコード実行パスが不安定になる問題を防止できます。

プロのヒント: ここでのトレードオフは、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 ステートメントの動的作成を、従来の文字列連結アプローチよりも堅牢でエラーが発生しにくくすることです。そのメソッド署名は流れるようなインターフェース設計モデルに従っています。

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

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 using the setCondition() method
        .setCondition('SystemModstamp = LAST_N_DAYS:30').

        //  set the number of records to limit the query to 
       .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() に指定された項目を無視するように指示します。次に、selectField() メソッドを使用して Opportunity オブジェクト (およびこの場合は関連する Account オブジェクトと User オブジェクト) の特定の項目を追加してクロスオブジェクトを形成します。クエリのベース SObject は Opportunity であるため、この場合、このメソッドは OpportunitiesSelector クラスにあります。

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

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 OpportunityInfo(Opportunity opportunity) {this.opportunity = opportunity; }
    public Id Id { get { return this.opportunity.Id; } }     
    public Decimal Amount { get { return this.opportunity.Amount; } }        
    public String Stage { get { return this.opportunity.StageName; } }       
    public String AccountName { get { return this.opportunity.Account.Name; } }      
    public String AccountNumber { get { return this.opportunity.Account.AccountNumber; } }       
    public String AccountOwner { get { return opportunity.Account.Owner.Name; } }         
}

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

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

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

FieldSet サポート

fflib_SObjectSelector 基本クラスのもう 1 つの機能は、所定の FieldSet で参照される項目を含めることです。これにより、セレクターは半動的になり、結果を、項目がクエリ済みであることが要求される Lightning Web ページや 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() 基本クラスのメソッドを利用しています。子オブジェクトセレクターのインスタンスを作成することで、これらのメソッドはセレクター項目を、最終的にクエリを実行することになる、親オブジェクトのカスタムセレクターメソッドで提供されるクエリファクトリーインスタンスに挿入します。

サブセレクトをベースクエリに追加するには、まずベース SObject のクエリファクトリーから始めます。次に、サブセレクトに含まれる SObject のクエリファクトリーをインスタンス化します。サブセレクト SObject のクエリファクトリーで addQueryFactorySubselect() メソッドをコールして、ベース SObject のクエリファクトリーをパラメーターとして渡すことができます。ベース SObject のクエリファクトリーで toSOQL() を実行すると、サブセレクトクエリが追加されます。

データ型が参照関係または主従関係の項目のリレーションクエリを介して SObject を追加するには、ベース SObject のクエリファクトリーから始めます。次に、ベースクエリに追加される項目が含まれる親 SObject のクエリファクトリーをインスタンス化します。その後、親 SObject のクエリファクトリーで configureQueryFactoryFields() メソッドをコールします。このとき、ベース SObject のクエリファクトリーと親項目のリレーション API 参照名をパラメーターとして渡します。ベース SObject のクエリファクトリーで toSOQL() メソッドを実行すると、親の SObject の項目が、パラメーターとして提供されたリレーション名を使用してベースクエリの SELECT 部分に追加されます。

以下の例では、どちらも 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;             
        }
    }
}

リソース

無料で学習を続けましょう!
続けるにはアカウントにサインアップしてください。
サインアップすると次のような機能が利用できるようになります。
  • 各自のキャリア目標に合わせてパーソナライズされたおすすめが表示される
  • ハンズオン Challenge やテストでスキルを練習できる
  • 進捗状況を追跡して上司と共有できる
  • メンターやキャリアチャンスと繋がることができる