Skip to main content

Aplicar os princípios da camada de seletor no Apex

Objetivos de aprendizagem

Após concluir esta unidade, você estará apto a:

  • Criar uma classe do Apex de seletor e usá-la de forma eficiente.
  • Fazer com que os campos sejam consultados consistentemente.
  • Implementar consultas de subseleção e entre objetos com o padrão Selector.
  • Consultar campos dinamicamente de um FieldSet além do seu.
  • Controlar quando a imposição de segurança de plataforma é aplicada.
Nota

Nota

Deseja aprender em português (Brasil)? Nesse emblema, as validações dos desafios práticos do Trailhead funcionam em inglês. As traduções são fornecidas entre parênteses como referência No Trailhead Playground, (1) mude a localidade para Estados Unidos, (2) mude o idioma para inglês e (3) copie e cole apenas os valores em inglês. Siga as instruções aqui.

Consulte o emblema Trailhead no seu idioma para saber mais sobre como aproveitar a experiência de Trailhead em outros idiomas.

Acompanhar com o Trail Together

Deseja acompanhar um especialista enquanto trabalha nesta etapa? Veja este vídeo que faz parte da série Trail Together.

(Este clipe começa na marca dos 55:16 minutos, caso você queira retroceder e ver o início da etapa novamente.)

Como implementar uma classe de seletor

Vamos concluir este módulo com um exame mais detalhado da classe de seletor e de como implementá-la. Essa implementação de seletor usa a classe base fflib_SObjectSelector para facilitar a criação e a execução de consultas SOQL, torná-las mais consistentes e mais compatíveis, com menos código clichê escrito pelo desenvolvedor. Isso é feito dinamicamente, mantendo, ao mesmo tempo, a integridade de referência e compilação dos campos consultados. Ela também oferece funções de consulta comuns úteis.

  • Inclusão do recurso de organização: campos dependentes, como o campo CurrencyIsoCode, que só ficam visíveis quando o recurso de configuração de várias moedas está habilitado.
  • Capacidade do Administrador de incluir (opcionalmente) campos definidos por um FieldSet.
  • Imposição de segurança de plataforma com a geração de exceções se o usuário não tem acesso de leitura para o objeto. É possível desativar esse recurso com um argumento construtor se o código chamador quiser ignorá-lo porque o objeto é acessado indiretamente em nome de uma operação feita pelo usuário.

Abaixo vemos os métodos na classe base fflib_SObjectSelector, que é uma classe base abstrata, o que significa que você deve implementar, no mínimo, os métodos marcados como abstratos antes de estendê-la.

Métodos na classe base fflibSObjectSelector.

A seguir estão os métodos abstratos que devem ser implementados.

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

A seguir está um exemplo básico de uma classe de seletor para o objeto Product2. Embora o método selectSObjectsById() possa ser chamado diretamente da classe base, o método selectByld() é rotineiramente implementado para esclarecer que ele está retornando uma lista de registros 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);
    }
}

Esse exemplo faz com que o SOQL a seguir seja gerado e executado quando o método selectById() é chamado. Você também pode ver comportamento de classe base comum injetado no SOQL para oferecer ordenação consistente. Este exemplo usa o padrão do campo Nome porque nenhuma alternativa foi especificada.

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

A implementação do método getSObjectFieldList() definiu uma lista de campos para a classe base consultar no método selectSObjectsById(), fazendo, dessa forma, com que os registros consultados tenham a presença dos campos base e sejam sempre preenchidos consistentemente. Isso previne possíveis problemas com registros preenchidos inconsistentemente que podem causar caminhos de execução de código mais frágeis.

Dica de profissional: a troca aqui é entre o tamanho de heap do Apex e a frequência com que os campos são exigidos pelos vários chamadores dos métodos de seletor. A recomendação é usá-la para incluir somente um conjunto mínimo de campos que vai ser útil na maior parte do tempo para a maior parte da sua lógica. Omita campos pouco usados, campos de texto grandes e campos de rich text; tente oferecê-los por métodos dedicados que retornam listas de tipos de Apex personalizados, conforme descrito mais adiante nesta unidade.

Você também pode substituir o método getOrderBy() para fazer com que todas as consultas criadas ou executadas pela classe base compartilhem os mesmos critérios de ordenação, como mostrado no exemplo abaixo.

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

Quando o método acima é substituído, o SOQL gerado por selectById() fica assim:

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

Como implementar métodos de seletor personalizados

Por enquanto, implementamos os métodos abstratos na classe base e vimos como ela afeta a consulta feita pela chamada do método selectById() da classe base. Agora adicionamos alguns métodos da classe de seletor que podem fazer consultas diferentes para variar os critérios, campos selecionados e outros aspectos das consultas que você precisa escolher.

Para implementar um método de seletor personalizado e manter a consistência dos campos e da ordenação definida pelo seletor, você poderá chamar os métodos que já foram implementados acima. O exemplo a seguir é um exemplo de SOQL dinâmico que mostra como fazer isso usando formatação de sequência de caracteres simples.

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

Abordagem de fábrica de consulta para a criação de consultas SOQL

A abordagem acima para criar a consulta usando String.format funciona, mas é mais difícil de ler e manter quando as consultas ficam mais complexas.

A classe base fflib_SObjectSelectortambém oferece uma forma de criar consultas mais voltada para objetos usando um padrão de criação fornecido pela classe fflib_QueryFactory. O objetivo dessa classe é fortalecer a criação dinâmica de instruções SOQL e deixá-la menos suscetível a erros do que as abordagens tradicionais de concatenação de sequência de caracteres. Suas assinaturas de método seguem o modelo de design de interface fluente.

Você pode criar sua própria instância de fflib_QueryFactory e chamar seus métodos para indicar quais objetos e campos deseja consultar. No entanto, a classe base de seletor oferece o método auxiliar newQueryFactory() para fazer isso por você, aproveitando-se dos métodos que foram implementados acima. Você pode, então, personalizar essa instância, como mostrado abaixo, com critérios (cláusula where) antes de solicitar que a consulta seja criada pela fábrica, por meio do método toSOQL(), e executada da maneira 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()
    );
}

Quando o método seletor personalizado é chamado com um parâmetro de 10, o SOQL a seguir é executado.

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 seleção de campo parcial e entre objetos

O próximo exemplo transmite um parâmetro falso ao método newQueryFactory() para mandar a classe base ignorar os campos especificados em getSObjectFieldList() ao criar a instância de fábrica de consulta. Depois usamos os métodos selectField() para adicionar campos específicos do objeto oportunidade (neste caso, os objetos Conta e Usuário relacionados) para formar uma consulta entre objetos. Como o SObject base da consulta é a oportunidade, esse método reside na classe OpportunitiesSelector, neste caso.

A outra diferença no exemplo a seguir é que, em vez de retornar List<Opportunities> com apenas campos específicos preenchidos e esperar que o chamador saiba o que foi preenchido, o método constrói em torno de cada registro uma pequena classe do Apex chamada OpportunityInfo para expor explicitamente apenas os valores de campo consultados. Isso é bem mais seguro e documentado para o chamador do método selecionado.

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

Dica de profissional: talvez seja melhor considerar essa opção ao usar AggregateDatabaseResult para retornar uma classe do Apex mais adequada para a informação contida nesses tipos de resultados. Evite a proliferação de classes internas usando essa abordagem. A seleção de campo padrão do seletor é realmente o que você deve usar na maioria das vezes e ela retorna sObjects reais. Considere também reutilizar essas classes entre métodos.

O código acima gera a seguinte instrução 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 Support

Outro recurso da classe base fflib_SObjectSelector é incluir campos referenciados por determinado FieldSet. Isso torna o seletor semidinâmico e permite a você usar os resultados junto com uma página Web do Lightning ou uma página do Visualforce que exige a consulta prévia dos campos. Esse exemplo mostra como isso é feito por meio de um parâmetro construtor usado para controlar a inclusão padrão de campos Fieldset. Você também precisa substituir o método getSObjectFieldSetList(). O resto do seletor é o mesmo.

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

Veja um exemplo pequeno que usa esse novo parâmetro construtor Fieldset. Embora o código de exemplo assuma que o campo MyText__c existe e foi adicionado a Fieldset, só quando o verdadeiro parâmetro é transmitido ao construtor é que a classe base injeta dinamicamente campos que o Administrador adicionou a 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);

Como usar FieldSets específicos em métodos de seletor personalizados

Talvez você tenha vários FieldSets em seu objeto e não queira que todos os métodos de seletor usem os que foram expressados no nível de classe do seletor. Você pode, em vez disso, permitir que eles sejam transmitidos como parâmetros para seus métodos de seletor e evitar seu registro, como mostra este exemplo.

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

Avançado: Como reutilizar listas de campos para consultas de subseleção e entre objetos

Você também pode criar instâncias de classes de seletor representando objetos filhos ao implementar métodos de seletor personalizados que aproveitam consultas de subseleção e entre objetos.

O uso de seletores filhos neste processo faz com que você aproveite também os campos de seletor definidos para esses objetos, mesmo quando os registros são consultados como parte de uma consulta de subseleção ou entre objetos.

O exemplo abaixo aproveita os métodos de classe base addQueryFactorySubselect() e configureQueryFactoryFields(). Ao criar instâncias dos seletores de objetos filhos, esses métodos injetam os campos de seletor na instância de fábrica de consulta fornecida pelo método seletor personalizado do objeto pai que vai executar a consulta.

Para adicionar uma subseleção à sua consulta base, comece com uma fábrica de consulta para o SObject base. Em seguida, instancia a fábrica de consulta para o SObject que faz parte da subseleção. Você chama o método addQueryFactorySubselect() na query factory do SObject da subseleção e passa a fábrica de consulta do SObject base como o parâmetro. Quando você executa o método toSOQL() na fábrica de consulta do SObject base, a consulta de subseleção será adicionada.

Para adicionar um SObject via consulta de relacionamento para um campo que é Lookup ou um tipo de dados entre mestre e detalhes, você começa com a fábrica de consulta para o SObject base. Em seguida, instancia a fábrica de consulta para o SObject pai cujos campos serão adicionados à consulta base. Em seguida, você chama o método configureQueryFactoryFields() na fábrica de consulta do SObject pai; passando a fábrica de consulta do SObject base e o nome da API de relacionamento do campo pai como os parâmetros. Quando você executa o método toSOQL() na fábrica de consulta do SObject base, os campos do SObject pai serão adicionados à parte SELECT da consulta base usando o nome do relacionamento fornecido como parâmetro.

Veja um exemplo que consulta Oportunidades, a filha, Produtos de oportunidade, e as informações relacionadas dos objetos Produto e Catálogo de preços reutilizando as respectivas classes de seletor filhas.

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

Isso resulta na consulta SOQL a seguir. Observe que mesmo a subseleção reutiliza o Ordenar por padrão especificado por OpportunityLineItemsSelector (não mostrado nos exemplos desta unidade).

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

Como controlar imposição de segurança em nível de objeto e campo

Por padrão, a classe base fflib_SObjectSelector impõe a segurança do objeto Salesforce com o lançamento de exceções se o usuário em execução não tem acesso de leitura para o objeto. No entanto, ela não imporá segurança em nível de campo por padrão se não estiver habilitado explicitamente conforme descrito nesta seção.

Seguindo lógica semelhante na classe base da camada de domínio mencionada na unidade anterior, essa imposição é aplicada a todos os tipos de acesso ao objeto, seja por um controlador, por um serviço ou por um domínio. Internamente, algum serviço ou lógica de domínio poderá querer acessar um objeto em nome de um usuário que não tenha a permissão necessária.

No entanto, se você preferir desativar essa imposição de segurança da classe base padrão e implementá-la por conta própria, poderá usar os parâmetros de configuração no construtor da classe base. O exemplo a seguir mostra como fazer isso em cada construtor de classe de seletor. Você pode criar sua própria classe base e estendê-la para todas as classes de seletor a fim de evitar mais código clichê e padronizar suas próprias regras de uso.

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

Se preferir deixar o chamador gerar a imposição, considere adicionar um construtor sobrecarregado, como o encontrado abaixo, permitindo que o construtor padrão use o padrão de “sem segurança” e exija que os chamadores usem o outro construtor para solicitar que a imposição seja ativada conforme a necessidade.

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

Como controlar a imposição de segurança de regra de compartilhamento

Outro tipo de imposição de segurança que os métodos de seletor precisam levar em conta é a aplicação de regras de compartilhamento. De acordo com as considerações de design da camada de serviço ou das classes de controlador do Visualforce ou do Lightning, a palavra-chave with sharing é a melhor prática. Todos os códigos nessas classes que chamarem um método de seletor serão executados também nesse contexto. Se essas convenções forem seguidas, o código de seletor será executado segundo as regras with sharing aplicadas por padrão.

Assim como com os critérios de filtro expressos na cláusula where na execução de consultas, a palavra-chave compartilhamento também afeta o conjunto de registros selecionado. Para encapsular um requisito para selecionar todos os registros independentemente das regras de compartilhamento, você pode seguir um padrão de elevação interna e explícita (nome do método) usando uma classe interna privada anotada com a palavra-chave sem compartilhamento obrigatória. Essa abordagem encapsula o requisito e evita que o chamador tenha que expressar essa necessidade criando uma classe artificialmente para invocar esse comportamento.

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

Continue a aprender de graça!
Inscreva-se em uma conta para continuar.
O que você ganha com isso?
  • Receba recomendações personalizadas para suas metas de carreira
  • Pratique suas habilidades com desafios práticos e testes
  • Monitore e compartilhe seu progresso com os empregadores
  • Conecte-se a orientação e oportunidades de carreira