Skip to main content

Appliquer les principes de la couche Sélecteur dans Apex

Objectifs de formation

Une fois cette unité terminée, vous pourrez :

  • Créer une classe Apex Sélecteur et l’utiliser efficacement
  • Vous assurer que les champs sont systématiquement interrogés
  • Implémenter des requêtes de sous-sélection et inter-objets avec le modèle de sélecteur
  • Interroger de manière dynamique les champs d’un FieldSet en plus des vôtres
  • Contrôler à quel moment l’application de la sécurité de la plate-forme s’effectue
Remarque

Remarque

Vous souhaitez apprendre en français ? Dans ce badge, les validations de défi pratique Trailhead se font en anglais. Les traductions sont fournies entre parenthèses à titre de référence. Dans votre Trailhead Playground, veillez (1) à définir les États-Unis comme région, (2) à sélectionner l’anglais comme langue, et (3) à copier et coller uniquement les valeurs en anglais. Suivez les instructions ici.

Consultez le badge Trailhead dans votre langue pour découvrir comment profiter de l’expérience Trailhead traduite.

Vidéo de démonstration Trail Together

Vous souhaitez être guidé pas à pas par un expert pendant que vous travaillez sur cette étape ? Regardez cette vidéo qui fait partie de la série Trail Together.

(Ce clip commence à 55 min 16 s, au cas où vous voudriez revenir en arrière et regarder à nouveau le début de l’étape.)

Implémentation d’une classe Sélecteur

Nous allons conclure ce module en explorant en détail la classe Sélecteur et la manière de l’implémenter. Cette implémentation de sélecteur utilise la classe de base fflib_SObjectSelector pour rendre la construction et l’exécution de requêtes SOQL plus facile, cohérente et conforme. Elle permet également au développeur d’avoir moins de code standard à écrire. Elle effectue le tout de manière dynamique, en veillant à ce que la compilation et l’intégrité de référence des champs interrogés soient préservées. Elle fournit également des fonctionnalités de requête communes bien pratiques.

  • L’intégration des champs dépendants, une fonctionnalité Organisation qui regroupe des champs tels que CurrencyIsoCode, s’affichant uniquement lorsque la fonctionnalité Devises multiples est activée.
  • La possibilité d’inclure (éventuellement) des champs définis par un FieldSet via l’administrateur.
  • L’application de la sécurité de la plate-forme en renvoyant une exception si l’utilisateur ne dispose pas d’un accès en lecture à l’objet. Vous pouvez désactiver cette fonctionnalité via un argument de constructeur si le code appelant veut la contourner. Cela peut arriver en cas d’accès indirect à l’objet pour le compte d’une opération effectuée par l’utilisateur.

L’exemple ci-dessous présente les méthodes de la classe de base fflib_SObjectSelector, qui est une classe de base abstraite. Cela signifie que vous devez implémenter au moins les méthodes marquées comme abstraites avant de pouvoir l’étendre.

Méthodes sur la classe de base fflibSObjectSelector.

Les méthodes abstraites qui doivent être implémentées sont les suivantes.

  • Méthode abstraite Schema.SObjectType getSObjectType();
  • Méthode abstraite List<Schema.SObjectField> getSObjectFieldList();

Voici un exemple de base d’une classe Sélecteur pour l’objet Product2. Bien que la méthode selectSObjectsById() puisse être appelée directement à partir de la classe de base, la méthode selectById() est implémentée de manière systématique pour préciser qu’elle renvoie une liste d’enregistrements 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,
            Product2.DiscountingApproved__c};
    }
    public Schema.SObjectType getSObjectType() {
        return Product2.sObjectType;
    }
    public List<Product2> selectById(Set<ID> idSet) {
        return (List<Product2>) selectSObjectsById(idSet);
    }
}

Cet exemple entraîne la génération et l’exécution du code SOQL suivant lors de l’appel de la méthode selectById(). Vous pouvez également constater qu’un comportement de classe de base commun est injecté dans SOQL afin que l’ordre soit cohérent. Dans cet exemple, le champ Name est utilisé par défaut car une alternative n’a pas encore été indiquée.

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

L’implémentation de la méthode getSObjectFieldList() a permis de définir une liste de champs pour la classe de base à interroger dans la méthode selectSObjectsById(), garantissant ainsi que les enregistrements interrogés contiennent les champs de base et sont toujours remplis de manière cohérente. Cela prévient des problèmes potentiels dus à la présence d’enregistrements remplis de manière incohérente, qui peuvent rendre les chemins d’exécution du code plus fragiles.

Conseil : ici, il faut faire un compromis entre la taille des segments d’Apex et la fréquence à laquelle les champs sont requis par les divers appelants des méthodes de sélecteur. Nous vous recommandons de procéder en intégrant uniquement un ensemble restreint de champs que la grande majorité de votre logique utilisera la plupart du temps. Omettez donc les champs de texte peu utilisés ou volumineux, ainsi que les champs de texte enrichi, et donnez plutôt accès à ceux-ci via des méthodes dédiées, renvoyant des listes de types Apex personnalisés, comme décrit plus loin dans cette unité.

Vous pouvez également remplacer la méthode getOrderBy() pour vous assurer que toutes les requêtes construites ou exécutées par la classe de base partagent les mêmes critères de classement, comme l’illustre l’exemple ci-dessous.

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

Lorsque la méthode présentée ci-dessus est remplacée, le code SOQL généré à partir de selectById() ressemble alors à ceci :

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

Implémentation de méthodes Sélecteur personnalisées

Jusqu’à présent, nous avons implémenté des méthodes abstraites sur la classe de base. Nous avons vu l’incidence que cela avait sur la requête effectuée en appelant la méthode selectById() de la classe de base. Nous allons maintenant ajouter quelques méthodes de classe Sélecteur pouvant exécuter différentes requêtes pour faire varier les critères, les champs sélectionnés ainsi que d’autres aspects des requêtes que vous devez effectuer.

Pour implémenter une méthode de sélecteur personnalisée tout en respectant la cohérence des champs et de l’ordre exprimés par le sélecteur, vous pouvez appeler les méthodes déjà implémentées ci-dessus. L’exemple suivant fait appel à du code SOQL dynamique élémentaire pour montrer comment procéder en utilisant un formatage de chaîne simple.

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);
}

Construction de requêtes SOQL : approche des requêtes standard

L’approche ci-dessus consistant à construire la requête à l’aide de String.format fonctionne, mais devient plus difficile à lire et à gérer lorsque les requêtes se complexifient.

La classe de base fflib_SObjectSelector propose également une manière davantage orientée objet de construire des requêtes, en utilisant l’approche du modèle de montage fournie par la classe fflib_QueryFactory. Cette classe a pour objectif de rendre la création dynamique d’instructions SOQL plus robuste et moins sujette aux erreurs que les approches traditionnelles de concaténation de chaînes. Ses signatures de méthode suivent le modèle de conception d’interface Fluent.

Vous pouvez créer votre propre instance de fflib_QueryFactory et appeler ses méthodes pour indiquer l’objet et les champs que vous souhaitez interroger. La classe de base Sélecteur dispose cependant de la méthode d’assistance newQueryFactory() qui peut le faire à votre place à l’aide des méthodes que vous avez implémentées ci-dessus. Vous pouvez ensuite personnaliser cette instance de fabrique de requêtes en spécifiant des critères (clause where), comme indiqué ci-dessous, avant de demander que la requête soit construite par la fabrique via la méthode toSOQL(), puis exécutée de manière traditionnelle.

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()
    );
}

Lorsque la méthode Sélecteur personnalisée est appelée avec un paramètre de 10, le code SOQL suivant est exécuté.

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

Sélection partielle de champs et requêtes inter-objets

L’exemple suivant transmet un paramètre false à la méthode newQueryFactory() pour indiquer à la classe de base d’ignorer les champs spécifiés dans getSObjectFieldList() lors de la création de l’instance de fabrique de requêtes. Nous utilisons ensuite les méthodes selectField() afin d’ajouter des champs spécifiques à partir de l’objet d’opportunité, ainsi que, dans ce cas précis, les objets associés Compte et Utilisateur, pour former une requête inter-objets. Étant donné que le SObject de base de la requête est l’opportunité, dans ce cas précis, la méthode est intégrée à la classe OpportunitiesSelector.

L’autre différence dans l’exemple suivant est que, plutôt que de renvoyer une List<Opportunity> avec seulement des champs spécifiques remplis et de partir du principe que l’appelant sait ce qui a été rempli, la méthode construit autour de chaque enregistrement une petite classe Apex nommée OpportunityInfo afin d’exposer uniquement de manière explicite les valeurs de champ demandées. Il s’agit d’une manière beaucoup plus sûre et plus stricte de réaliser des opérations avec l’appelant de la méthode de sélecteur, qui a aussi l’avantage d’être auto-documentée.

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; } }         
}

Conseil professionnel : vous pouvez également envisager d’avoir recours à cette solution lorsque vous utilisez AggregateDatabaseResult, afin de renvoyer une classe Apex plus adaptée aux informations contenues dans ces types de résultats. Cette méthode vous permet d’éviter de multiplier les classes internes. La sélection de champ par défaut du sélecteur correspond à ce que vous devez utiliser la plupart du temps et renvoie donc des sObjects authentiques. Pensez également à utiliser ces classes avec plusieurs méthodes.

Le code ci-dessus génère l’instruction SOQL suivante :

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

Prise en charge de FieldSet

L’une des autres caractéristiques de la classe de base fflib_SObjectSelector est qu’elle inclut les champs référencés par un FieldSet donné. Cela rend le sélecteur semi-dynamique et vous permet d’utiliser les résultats avec une page Web Lightning ou une page Visualforce nécessitant l’interrogation préalable des champs. Cet exemple montre comment cela s’effectue à l’aide d’un paramètre de constructeur utilisé pour contrôler l’inclusion par défaut des champs Fieldset. Vous devez également remplacer la méthode getSObjectFieldSetList(). Le reste du sélecteur est identique.

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
    // ...}

Voici un court exemple mettant en jeu ce nouveau paramètre de constructeur Fieldset. Bien que le code donné en exemple suppose que le champ MyText__c existe et a été ajouté au Fieldset, ce n’est que lorsque le paramètre true est transmis à son constructeur que la classe de base injecte de manière dynamique les champs ajoutés par l’administrateur au 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);

Utilisation de FieldSets spécifiques dans des méthodes Sélecteur personnalisées

Vous pouvez avoir plusieurs FieldSets sur votre objet et ne pas souhaiter que les méthodes de sélecteur utilisent toutes celles exprimées au niveau de la classe Sélecteur. À la place, vous pouvez choisir d’autoriser leur transmission en tant que paramètres à vos méthodes de sélecteur et d’éviter l’enregistrement, comme le montre cet exemple.

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

Avancé : réutilisation de listes de champs pour les requêtes inter-objets et de sous-sélection

Vous pouvez également créer des instances de classes Sélecteur représentant des objets enfant lors de l’implémentation de méthodes de sélecteur personnalisées exploitant des requêtes de champ et de sous-sélection inter-objets.

L’utilisation de sélecteurs enfant dans ce processus garantit que vous exploitez également les champs de sélecteur que vous avez définis pour ces objets, même lorsque ces enregistrements sont interrogés lors d’une requête de sous-sélection ou inter-objets.

L’exemple ci-dessous tire parti des méthodes des classes de base addQueryFactorySubselect() et configureQueryFactoryFields(). En créant des instances de sélecteurs d’objet enfant, ces méthodes injectent les champs de sélecteur dans l’instance de fabrique de requêtes fournie par la méthode de sélecteur personnalisée de l’objet parent qui exécutera par la suite la requête.

Pour ajouter une sous-sélection à votre requête de base, vous devez commencer par spécifier la fabrique de requêtes correspondant au SObject de base. Vous instanciez ensuite la fabrique de requêtes pour le SObject qui fait partie de la sous-sélection. Vous appelez la méthode addQueryFactorySubselect() sur la fabrique de requêtes du SObject de la sous-sélection et vous transmettez la fabrique de requêtes du SObject de base comme paramètre. Lorsque vous exécutez la méthode toSOQL() sur la fabrique de requêtes du SObject de base, la requête de sous-sélection est ajoutée.

Pour ajouter un SObject via une requête de relation pour un champ ayant pour type de données Référence ou Principal-Détails, vous devez commencer par spécifier la fabrique de requêtes correspondant au SObject de base. Vous instanciez ensuite la fabrique de requêtes pour le SObject parent dont les champs seront ajoutés à la requête de base. Il vous faut ensuite appeler la méthode configureQueryFactoryFields() sur la fabrique de requêtes du SObject parent. Pour ce faire, vous transmettez la fabrique de requêtes du SObject de base et le nom d’API de relation du champ parent en tant que paramètres. Lorsque vous exécutez la méthode toSOQL() sur la fabrique de requêtes du SObject de base, les champs SObject du parent sont ajoutés à la partie SELECT de la requête de base en utilisant comme paramètre le nom de relation fourni.

Dans cet exemple, des requêtes sont effectuées à la fois sur Opportunities, sur l’enfant Opportunity Products et sur des informations connexes issues des objets Product et Pricebook en réutilisant les classes Sélecteur enfant correspondantes.

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());
}

Cela engendre la requête SOQL suivante. Notez que même la sous-sélection réutilise l’ordre par défaut tel qu’indiqué par OpportunityLineItemsSelector (non présenté dans les exemples de cette unité).

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

Contrôle de l’application de la sécurité au niveau des objets et des champs

Par défaut, la classe de base fflib_SObjectSelector applique la sécurité de l’objet Salesforce en renvoyant une exception si l’utilisateur actif ne dispose pas d’un accès en lecture à l’objet. Cependant, elle n’applique pas par défaut la sécurité au niveau du champ sauf si elle est explicitement activée, comme décrit dans cette section.

Dans une logique semblable à celle employée dans la classe de base de la couche Domaine mentionnée dans l’unité précédente, cette application s’effectue sur tous les types d’accès à l’objet, que cela soit via un contrôleur, un service ou un domaine. En interne, une logique de service ou de domaine peut tenter d’accéder à un objet pour le compte d’un utilisateur ne disposant pas de l’autorisation requise.

Toutefois, si vous préférez désactiver cette application par défaut de la sécurité de la classe de base et l’implémenter vous-même, vous pouvez utiliser les paramètres de configuration du constructeur de la classe de base. L’exemple suivant montre comment procéder dans chaque constructeur de classe Sélecteur. Vous pouvez créer votre propre classe de base, puis étendre cette classe à toutes les classes Sélecteur pour éviter l’utilisation supplémentaire de code générique et standardiser vos propres règles d’application.

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

Si vous préférez laisser l’appelant être à l’origine de l’application, envisagez d’ajouter un constructeur surchargé semblable à celui que nous vous présentons ci-après. Ainsi, le constructeur n’appliquera par défaut aucune sécurité et cela obligera les appelants à utiliser l’autre constructeur pour demander l’activation de l’application si nécessaire.

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

Contrôle de l’application de la sécurité des règles de partage

Les méthodes de sélecteur doivent également prendre en compte une autre forme d’application de la sécurité : l’application des règles de partage. D’après les considérations de conception de la couche Service ou des classes de contrôleur Visualforce ou Lightning, l’utilisation du mot-clé with sharing constitue une bonne pratique. Tout code dans ces classes appelant une méthode de sélecteur s’exécute également dans ce contexte. Si ces conventions sont suivies, le code de sélecteur s’exécute en appliquant les règles with sharing par défaut.

Comme pour les critères de filtre exprimés dans la clause where lors de l’exécution de requêtes, le mot-clé sharing influe également sur le jeu d’enregistrements sélectionné. Pour encapsuler une obligation de sélection de l’ensemble des enregistrements, et ce, quelles que soient les règles de partage, vous pouvez suivre le modèle d’une élévation explicite (nom de méthode) et interne en utilisant une classe interne privée annotée avec le mot-clé requis without sharing. Cette approche encapsule cette obligation et évite à l’appelant d’exprimer ce besoin en créant artificiellement une classe pour invoquer ce comportement.

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;             
        }
    }
}

Ressources

Formez-vous gratuitement !
Créez un compte pour continuer.
Qu’est-ce que vous y gagnez ?
  • Obtenez des recommandations personnalisées pour vos objectifs de carrière
  • Mettez en pratique vos compétences grâce à des défis pratiques et à des questionnaires
  • Suivez et partagez vos progrès avec des employeurs
  • Découvrez des opportunités de mentorat et de carrière