Skip to main content

Aplicar principios de la Capa de selectores en Apex

Objetivos de aprendizaje

Después de completar esta unidad, podrá:

  • Crear una clase de Apex de selector y hacer un uso efectivo de ella.
  • Asegurarse de que los campos se consultan de manera coherente.
  • Implementar consultas de subselección y objetos cruzados con el patrón de selector.
  • Consultar campos dinámicamente desde un FieldSet además de los suyos.
  • Controlar cuándo se aplica la seguridad de la plataforma.
Nota

Nota

¿Es su idioma de aprendizaje español (LATAM)? En esta insignia, las validaciones de los retos prácticos de Trailhead funcionan en inglés. Entre paréntesis se incluyen las traducciones a modo de referencia. En su instancia de Trailhead Playground, asegúrese de (1) cambiar la configuración local a los Estados Unidos, (2) cambiar el idioma a inglés, y (3) copiar y pegar solo los valores en inglés. Siga las instrucciones que figuran aquí.

Consulte la insignia Trailhead en su idioma para obtener más información sobre cómo aprovechar la experiencia de Trailhead en otros idiomas.

Siga el proceso con Trail Together

¿Desea seguir el proceso con un experto a medida que realiza este paso? Mire este video que forma parte de la serie Trail Together.

(Este video comienza en el minuto 55:16, en caso de que desee rebobinar y mirar el comienzo del paso nuevamente).

Implementación de una clase de selector

Vamos a finalizar este módulo profundizando en la clase de selector y en cómo implementarla. Esta implementación de selector usa la clase base fflib_SObjectSelector para facilitar la generación y ejecución de consultas SOQL, y hacer que sean más coherentes y que su cumplimiento sea mayor con menos código reutilizado escrito por el desarrollador. Esto lo hace dinámicamente a la vez que sigue garantizando que se mantiene la integridad de la compilación y las referencias de los campos consultados. También proporciona funcionalidades de consulta comunes de utilidad.

  • Inclusión de la función de organización: campos dependientes, como el campo CurrencyIsoCode, que solo pueden verse cuando está activada la función de múltiples divisas.
  • Capacidad de incluir (opcionalmente) campos definidos por un FieldSet a través del administrador.
  • Aplicación de la seguridad de la plataforma lanzando una excepción si el usuario no tiene acceso de lectura en el objeto. Puede desactivar esta funcionalidad a través de un argumento del constructor si el código de llamada quiere omitirlo porque se accede al objeto indirectamente en nombre de una operación que está realizando el usuario.

A continuación se muestran los métodos de la clase base fflib_SObjectSelector, que es una clase base abstracta, lo que quiere decir que debe implementar al menos los métodos marcados como abstractos antes de poder ampliarla.

Métodos en la clase base fflibSObjectSelector.

Los métodos abstractos que se deben implementar son los siguientes.

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

El siguiente es un ejemplo básico de una clase de selector para el objeto Product2. Aunque se puede llamar al método selectSObjectsById() directamente desde la clase base, se suele implementar el método selectById() para aclarar que está mostrando una lista de registros de 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);
    }
}

Este ejemplo da como resultado la generación y ejecución de la siguiente consulta SOQL cuando se llama al método selectById(). También puede observar que se incorpora comportamiento de clase base común en la consulta SOQL para proporcionar una ordenación coherente. En este ejemplo, presenta el valor predeterminado del campo Nombre porque todavía no se especificó una alternativa.

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

La implementación del método getSObjectFieldList() definió una lista de campos para que la clase base realizara consultas en el método selectSObjectsById(), lo que garantiza que los registros consultados tengan los campos base y que siempre se completen de manera coherente. Esto evita posibles problemas con registros completados de manera incoherente que pueden generar rutas de ejecución de código más frágiles.

Sugerencia avanzada: El tamaño de la pila de Apex compensa la frecuencia con la que los diversos emisores de llamadas de los métodos de selector necesitan campos. La recomendación es usar esto para que solo se incluya un conjunto mínimo de campos que sean de utilidad la mayor parte del tiempo para la mayor parte de la lógica. Omita los campos de texto de gran tamaño que se usen poco y los campos de texto enriquecido a favor de proporcionarlos mediante métodos específicos que devuelvan listas de tipos de Apex personalizados, como se describe más adelante en esta unidad.

También puede reemplazar el método getOrderBy() para garantizar que todas las consultas generadas o ejecutadas por la clase base compartan los mismos criterios de ordenación, como se muestra en el ejemplo siguiente.

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

Cuando se reemplaza el método anterior, la consulta SOQL generada a partir de selectById() ahora se ve de la siguiente manera:

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

Implementación de métodos de selector personalizados

Hasta ahora implementamos los métodos abstractos en la clase base y vimos cómo afecta esto a la consulta realizada al llamar al método selectById() de la clase base. Ahora agregaremos algunos métodos de clase de selector que pueden realizar diferentes consultas para variar los criterios, los campos seleccionados y otros aspectos de las consultas que necesite realizar.

Para implementar un método de selector personalizado y seguir manteniendo la coherencia de los campos y la ordenación que expresa el selector, puede llamar a los métodos que ya se implementaron anteriormente. El ejemplo siguiente es un ejemplo de consulta SOQL dinámica básica que muestra cómo realizar esta acción mediante el uso de un formato de cadenas 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);
}

Enfoque de generador de consultas para generar consultas SOQL

El enfoque anterior para generar la consulta mediante el uso de String.format funciona, pero es más difícil de leer y mantener a medida que las consultas se vuelven más complejas.

La clase base fflib_SObjectSelector también ofrece una forma de generar consultas más orientada hacia el objeto mediante el uso de un enfoque de patrón de generador proporcionado por la clase fflib_QueryFactory. El objetivo de esta clase es lograr que la creación dinámica de declaraciones SOQL sea más robusta y tienda a presentar menos errores que los enfoques tradicionales de concatenación de cadenas. Las firmas de su método siguen el modelo de diseño de interfaz fluida.

Puede crear su propia instancia de fflib_QueryFactory y llamar a sus métodos para indicar qué objeto y qué campos quiere consultar. Sin embargo, la clase base de selector ofrece el método auxiliar newQueryFactory() para que lo haga por usted, aprovechando los métodos que implementó anteriormente. Luego, puede personalizar esa instancia de generador de consultas, como se muestra a continuación con criterios (cláusula where) antes de solicitar que el generador cree la consulta mediante el método toSOQL() y que, a continuación, la ejecute de manera tradicional.

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

Cuando se llama al método de selector personalizado con un parámetro de 10, se ejecuta la siguiente consulta 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

Consultas de selección de campo parcial y objetos cruzados

El siguiente ejemplo pasa un parámetro falso al método newQueryFactory() para indicar a la clase base que debe ignorar los campos especificados en getSObjectFieldList() al crear la instancia de generador de consultas. Luego, usamos los métodos selectField() para agregar campos específicos desde el objeto de oportunidad, y, en este caso, los objetos Cuenta y Usuario relacionados, a fin de formar una consulta de objetos cruzados. Dado que el SObject base de la consulta es la oportunidad, este método reside en la clase OpportunitiesSelector en este caso.

La otra diferencia en el siguiente ejemplo es, en lugar de devolver una List<Opportunity> solo con campos específicos completados y esperar que el emisor de la llamada sepa qué se completó, el método construye alrededor de cada registro una pequeña clase de Apex denominada OpportunityInfo para exponer explícitamente solo los valores de campo consultados. Este es un contrato mucho más seguro, autodocumentado y más estricto con el emisor del método de selector.

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

Sugerencia avanzada: También recomendamos que considere esta opción cuando use AggregateDatabaseResult para devolver una clase de Apex más personalizada con respecto a la información incluida en estos tipos de resultados. Evite la proliferación de clases internas al usar este enfoque. La selección de campos predeterminados del selector es realmente lo que cabría esperar que usara la mayor parte del tiempo y, de este modo, se devolverían sObjects reales. Considere también reutilizar estas clases entre métodos.

El código anterior genera la siguiente declaración SOQL:

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

Compatibilidad con FieldSet

Otra función de la clase base fflib_SObjectSelector es incluir campos a los que hace referencia un FieldSet determinado. Esto hace que el selector sea semidinámico, y le permite usar los resultados junto con páginas web Lightning y páginas de Visualforce que requieran que ya se hayan consultado los campos. Este ejemplo muestra cómo realizar esta acción mediante un parámetro del constructor usado para controlar la inclusión predeterminada de campos del FieldSet. También debe reemplazar el método getSObjectFieldSetList(). El resto del selector es igual.

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

Aquí tiene un breve ejemplo del uso de este nuevo parámetro del constructor de FieldSet. Aunque el código de ejemplo supone que el campo MyText__c existe y que se agregó al FieldSet, la clase base solo inyecta dinámicamente los campos que el administrador agregó a MyFieldSet después de que el parámetro true se pasa a su constructor.

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

Uso de FieldSets específicos en métodos de selector personalizados

Puede que su objeto tenga varios FieldSets, y que usted no quiera que todos los métodos de selector usen los expresados en el nivel de la clase de selector. En lugar de eso, tiene la opción de permitir que pasen como parámetros a sus métodos de selector y evitar registros, como se muestra en este ejemplo.

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

Avanzado: reutilización de listas de campos para consultas de objetos cruzados y subselección

También puede crear instancias de clases de selector que representen objetos secundarios al implementar métodos de selector personalizados que aprovechen consultas de campos de objetos cruzados y subselección.

El uso de selectores secundarios en este proceso garantiza que también esté aprovechando los campos de selector que definió para estos objetos, incluso si dichos registros se consultan como parte de una consulta de subselección u objetos cruzados.

El ejemplo siguiente aprovecha los métodos de clase base addQueryFactorySubselect() y configureQueryFactoryFields(). Al crear instancias de los selectores de objetos secundarios, estos métodos inyectan los campos de selector en la instancia de generador de consultas proporcionada por el método de selector personalizado de objeto principal que finalmente ejecutará la consulta.

Para agregar una subselección a su consulta base, empiece con el generador de consultas para el SObject base. Luego, cree una instancia del generador de consultas para SObject que sea parte de la subselección. Llame al método addQueryFactorySubselect() en el generador de consultas del SObject correspondiente a la subselección y pase el generador de consultas del SObject base como parámetro. Cuando ejecute el método toSOQL() en el generador de consultas del SObject base, se agregará la consulta de subselección.

Para agregar un SObject mediante una consulta de relación para un campo que sea el tipo de datos de Búsqueda o Principal-detalle, empiece con el generador de consultas para el SObject base. Luego, cree una instancia del generador de consultas para el SObject principal cuyos campos se agregarán a la consulta base. Luego, llame al método configureQueryFactoryFields() en el generador de consultas del SObject principal y pase el generador de consultas del SObject base y el nombre de API de relación del SObject base como parámetros. Cuando ejecute el método toSOQL() en el generador de consultas del SObject base, los campos del SObject principal se agregarán a la parte de SELECT de la consulta base con el nombre de relación suministrado como parámetro.

Aquí tiene un ejemplo que consulta tanto las Oportunidades como los Productos de oportunidad secundarios, así como información relacionada de los objetos Producto y Lista de precios que reutilizan las respectivas clases de selector secundarias.

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

El resultado de esta acción es la siguiente consulta SOQL. Tenga en cuenta que incluso la subselección reutiliza el orden predeterminado según lo especificado por OpportunityLineItemsSelector (esto no se muestra en los ejemplos de esta unidad).

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

Control de la aplicación de seguridad a nivel de objeto y campo

De manera predeterminada, la clase base fflib_SObjectSelector aplica la seguridad de objetos de Salesforce con una excepción si el usuario activo no tiene acceso de lectura al objeto. Sin embargo, no aplica seguridad a nivel de campo de manera predeterminada a menos que esté activado explícitamente, como se describe en esta sección.

Siguiendo una lógica similar a la de la clase base de capa de dominios mencionada en la unidad anterior, esta aplicación se realiza para todo tipo de acceso al objeto, ya sea a través de un controlador, un servicio o un dominio. De manera interna, puede que alguna lógica de servicio o dominio quiera acceder a un objeto en nombre de un usuario que no tenga el permiso requerido.

Sin embargo, si prefiere desactivar esta aplicación de seguridad de clase base predeterminada e implementar esto por sí mismo, puede usar parámetros de configuración del constructor de clase base. El siguiente ejemplo muestra cómo hacerlo en cada constructor de clase de selector. Puede crear su propia clase base y, a continuación, ampliar dicha clase para todas las clases de selector con el fin de evitar código reutilizado adicional y estandarizar siguiendo las reglas de su propia aplicación.

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

Si prefiere dejar que el emisor controle la aplicación, considere la opción de agregar un constructor sobrecargado como el siguiente, permitiendo así que el constructor predeterminado no tome de manera predeterminada ninguna seguridad y requiera que los emisores usen el otro constructor para solicitar que la aplicación se active de la manera requerida.

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

Control de la aplicación de seguridad de reglas de colaboración

Otra forma de aplicación de seguridad que los métodos de selector deben considerar es si se aplican reglas de colaboración. Según las consideraciones de diseño para la capa de servicios, o clases de controlador de Lightning o Visualforce, el uso de la palabra clave with sharing es una mejor práctica. Cualquier código de estas clases que llame a un método de selector también se ejecuta en este contexto. Si se siguen estas convenciones, el código de selector se ejecuta con reglas with sharing aplicadas de manera predeterminada.

Al igual que con los criterios de filtro expresados en la cláusula where al ejecutar consultas, la palabra clave sharing también afecta al conjunto de registros seleccionado. Para encapsular un requisito con el fin de que seleccione todos los registros independientemente de las reglas de colaboración, puede seguir un patrón de una elevación explícita (nombre del método) e interna mediante el uso de una clase interna privada anotada con la palabra clave without sharing requerida. Este enfoque encapsula este requisito y evita que el emisor tenga que expresar esta necesidad mediante la creación artificial de una clase para invocar este comportamiento.

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

Recursos

¡Siga aprendiendo gratis!
Regístrese para obtener una cuenta y continuar.
¿Qué hay para usted?
  • Consiga recomendaciones personalizadas para sus objetivos profesionales
  • Practique sus aptitudes con retos prácticos y pruebas
  • Siga y comparta su progreso con empleadores
  • Póngase en contacto para recibir asesoramiento y oportunidades laborales