LookupObject no Consumer Goods Cloud Modeler

Como estruturar entidades de consulta ultraleves, resolver dependências relacionais sem inflar a memória RAM e criar seletores modais de alta performance no banco SQLite local.

Na modelagem de dados para o varejo de bens de consumo embalados (CPG/FMCG), praticamente nenhuma entidade de negócio opera isolada:

  • Um Pedido de Venda (Order) depende de uma Conta (Customer/Account), vincula-se a uma Tabela de Preços (PriceList) e associa-se a uma Condição de Pagamento (PaymentTerm).
  • Uma Auditoria de Gôndola (DisplayAudit) aponta para um Tipo de Exibidor (DisplayType).
  • Uma Tarefa de Visita (VisitTask) referencia um Produto Específico (Product).

Em aplicações web conectadas ao Salesforce Core, resolver esses relacionamentos é direto via SOQL (Account.Name, Pricebook2.Description) ou componentes nativos como o lightning-record-picker no LWC.

No entanto, no Salesforce Consumer Goods Cloud (CGC) Modeler (Mv2), a aplicação opera sob o paradigma Offline-First. Todas as consultas rodam exclusivamente contra o banco de dados SQLite local (app.db3) instalado no tablet ou smartphone do promotor em campo.

Nesse ecossistema desconectado, carregar uma instância completa de um BusinessObject (BO) apenas para descobrir o nome de um cliente ou abrir um modal de seleção gera gargalos sérios:

  1. Consumo Excessivo de Memória: Um BusinessObject mantém em memória todo o seu ciclo transacional, dezenas de propriedades simples, métodos de gravação e flags de dirty tracking.
  2. Dependência Circular e Travamentos: Instanciar um BO a partir de outro BO gera cadeias de carregamento em cascata que congelam a inicialização de telas complexas.
  3. Lentidão em Telas de Seleção: Exibir um seletor que instancia objetos completos para cada linha torna a rolagem truncada e instável.

Para solucionar essa necessidade com baixo consumo de recursos e alta velocidade de resposta, o framework do Modeler disponibiliza o contrato LookupObject.

Identificado pela extensão .lookupobject.xml e pelo prefixo Lu, o LookupObject atua como uma projeção relacional de leitura ultraleve. Ele “espia” registros relacionados trazendo estritamente as colunas necessárias para identificação, busca e amarração de chaves estrangeiras (Foreign Keys).


1. O Papel do LookupObject na Arquitetura de 6 Camadas

O LookupObject atua como uma ponte relacional entre entidades no Modeler:

Topologia Relacional no Modeler:

 [ BusinessObject Mestre: BoOrder ]
   │
   ├──> SimpleProperty: customerPKey (Armazena a Foreign Key no SQLite)
   │
   └──> <LookupObjects>
          └──> LookupObject: LuMyCustomerLookup (.lookupobject.xml)
                 │
                 ├──> DataSource Dedicado: DsLuMyCustomerLookup_sf (.datasource.xml)
                 │      └──> Executa: SELECT Id, Name, Account_Number__c FROM Account
                 │
                 └──> Interface do Usuário: <LookupField> na UI
                        └──> Abre modal de seleção instantâneo sem instanciar BoCustomer!

Por Que Ele é Tão Mais Eficiente que um BusinessObject?

  • Sem Ciclo de Vida Pesado: O LookupObject não possui métodos de escrita (saveAsync) nem transações de gravação. É uma entidade estritamente de leitura.
  • Projeção Enxuta de Colunas: Enquanto um BoCustomer carrega dezenas de colunas do SQLite (endereços completos, limites de crédito, contatos, classificações fiscais), o LuMyCustomerLookup carrega apenas as colunas essenciais: chave primária (pKey), nome (name), código (customerNumber) e localização básica.
  • Redução Drástica de RAM: O footprint de memória de um LookupObject é até 90% menor do que o de um BusinessObject correspondente, permitindo que telas de seleção com milhares de registros abram sem qualquer engasgo.

2. Implementação Real no Modeler: LuMyCustomerLookup

Implementamos um contrato completo de LookupObject no projeto Modeler local (projeto_modeler/src/Order/), compilado e validado com sucesso via sf mdl build e sf modeler workspace validate.

Passo 1: O DataSource de Leitura (DsLuMyCustomerLookup_sf.datasource.xml)

Localizado em src/Order/DS/DsLuMyCustomerLookup_sf.datasource.xml:

<!-- Data: 2026-10-14; Objetivo da Customização: DataSource ultraleve para resolução de chave estrangeira de cliente em pedidos do CGC Modeler -->
<DataSource name="DsLuMyCustomerLookup" backendSystem="sf" businessObjectClass="LuMyCustomerLookup" external="false" editableEntity="Account" readOnly="true" schemaVersion="2.0">
  <Attributes>
    <Attribute name="pKey" table="Account" column="Id" />
    <Attribute name="name" table="Account" column="Name" />
    <Attribute name="customerNumber" table="Account" column="Account_Number__c" />
    <Attribute name="city" table="Account" column="ShippingCity" />
    <Attribute name="street" table="Account" column="ShippingStreet" />
  </Attributes>
  <Entities>
    <Entity name="Account" alias="" idAttribute="Id" />
  </Entities>
  <QueryCondition><![CDATA[
    Account.Id = #pKey#
  ]]></QueryCondition>
  <OrderCriteria />
  <Parameters>
    <Parameter name="pKey" type="TEXT" />
  </Parameters>
</DataSource>

Atenção ao atributo readOnly="true": DataSources de lookup devem ser marcados como readOnly="true". Isso sinaliza ao compilador e ao motor em C++ que a entidade não necessita de buffers de escrita ou tracking de modificações.


Passo 2: O Contrato do LookupObject (LuMyCustomerLookup.lookupobject.xml)

Localizado em src/Order/BO/LuMyCustomerLookup/LuMyCustomerLookup.lookupobject.xml:

<!-- Data: 2026-10-14; Objetivo da Customização: Contrato LookupObject para resolução ágil de cliente com projeção enxuta de memória RAM no SQLite local -->
<LookupObject name="LuMyCustomerLookup" generateLoadMethod="true" schemaVersion="1.1">
  <DataSource name="DsLuMyCustomerLookup" />
  <SimpleProperties>
    <SimpleProperty id="true" name="pKey" type="DomPKey" dataSourceProperty="pKey" />
    <SimpleProperty name="name" type="DomString" dataSourceProperty="name" />
    <SimpleProperty name="customerNumber" type="DomString" dataSourceProperty="customerNumber" />
    <SimpleProperty name="city" type="DomString" dataSourceProperty="city" />
    <SimpleProperty name="street" type="DomString" dataSourceProperty="street" />
  </SimpleProperties>
  <Methods>
    <Method name="afterLoadAsync" />
    <Method name="beforeLoadAsync" />
    <Method name="loadAsync" />
  </Methods>
</LookupObject>

Regra de Validação Crucial: A propriedade pKey deve possuir obrigatoriamente id="true". Omitir esse atributo impede que o Modeler resolva a amarração relacional entre a Foreign Key do BusinessObject pai e a chave primária do LookupObject.


Passo 3: Declarando o Relacionamento no BusinessObject Pai

No arquivo BoOrder.businessobject.xml, o relacionamento é amarrado na seção <LookupObjects>:

<BusinessObject name="BoOrder" schemaVersion="1.1" generateLoadMethod="true" generateCreateMethod="true">
  <DataSource name="DsBoOrder" />
  <SimpleProperties>
    <SimpleProperty id="true" name="pKey" type="DomPKey" dataSourceProperty="pKey" />
    <SimpleProperty name="orderId" type="DomString" dataSourceProperty="orderId" />
    <!-- Coluna física que persiste a Foreign Key no SQLite -->
    <SimpleProperty name="customerPKey" type="DomPKey" dataSourceProperty="customerPKey" />
    <SimpleProperty name="grossTotalValue" type="DomMoney" dataSourceProperty="grossTotalValue" />
  </SimpleProperties>

  <!-- SEÇÃO RELACIONAL: Mapeia o LookupObject à Foreign Key física -->
  <LookupObjects>
    <LookupObject name="luCustomer" objectClass="LuMyCustomerLookup" dataSourceProperty="customerPKey" />
  </LookupObjects>

  <Methods>
    <Method name="loadAsync" />
    <Method name="saveAsync" />
  </Methods>
</BusinessObject>

Quando o BoOrder é carregado no SQLite via loadAsync(), o framework inspeciona customerPKey. Se preenchida, o objeto luCustomer é hidratado sob demanda de forma instantânea. Na Business Logic (.bl.js):

/* Data: 2026-10-14; Objetivo da Customização: Acesso aos dados do cliente relacionado sem carregar BoCustomer */
var clienteNome = me.getLuCustomer().getName();
var codigoCliente = me.getLuCustomer().getCustomerNumber();

AppLog.info("Pedido vinculado ao cliente: " + clienteNome + " (" + codigoCliente + ")");


3. Exemplo Prático Customizado: Central de Plugins no Simulador

Para comprovar o funcionamento sem depender de telas nativas do sistema, implementamos a demonstração prática do LuMyCustomerLookup diretamente na tela customizada Central de Plugins (Application::MyMainCustomPluginsUI).

Como Foi Implementado no Modeler

1. Na Interface do Usuário (Application_MyMainCustomPluginsUI.userinterface.xml):

Criamos a aba Lookup Cliente 🔍 com o seletor <Lookup> e os campos de leitura derivados:

<!-- Data: 2026-10-14; Objetivo da Customização: Área visual de demonstração e teste do LookupObject LuMyCustomerLookup -->
<Area areaPattern="MultiArea" areaName="LookupDemo">
  <Area areaName="LookupDemo" areaPattern="GroupedElementsArea">
    <GroupElement name="LookupDemoGroup">
      <Bindings>
        <Resource target="Title" type="Label" id="LookupDemoTitle" defaultLabel="Demonstração Prática de LookupObject (LuMyCustomerLookup)" />
      </Bindings>
      <!-- Seletor modal que dispara a pesquisa -->
      <Lookup name="CustomerLookupField">
        <Bindings>
          <Resource target="Label" type="Label" defaultLabel="Selecionar Cliente (LuMyCustomerLookup)" id="CustLookupLabel" />
          <Binding target="Value" binding="ProcessContext::CustomerLookup.name" bindingMode="TWO_WAY" />
        </Bindings>
        <Events>
          <LookupEvent event="openCustomCustomerLookup" />
        </Events>
      </Lookup>
      <!-- Dados complementares resolvidos de forma ultraleve -->
      <InputArea name="CustomerPKey" disabled="true">
        <Bindings>
          <Resource target="Label" type="Label" id="CustPKeyLabel" defaultLabel="Chave Primária (PKey / Account Id)" />
          <Binding target="Value" binding="ProcessContext::CustomerLookup.pKey" bindingMode="ONE_WAY" />
        </Bindings>
      </InputArea>
      <InputArea name="CustomerNumber" disabled="true">
        <Bindings>
          <Resource target="Label" type="Label" id="CustNumberLabel" defaultLabel="Código ERP (Account_Number__c)" />
          <Binding target="Value" binding="ProcessContext::CustomerLookup.customerNumber" bindingMode="ONE_WAY" />
        </Bindings>
      </InputArea>
      <InputArea name="CustomerCity" disabled="true">
        <Bindings>
          <Resource target="Label" type="Label" id="CustCityLabel" defaultLabel="Cidade (ShippingCity)" />
          <Binding target="Value" binding="ProcessContext::CustomerLookup.city" bindingMode="ONE_WAY" />
        </Bindings>
      </InputArea>
      <InputArea name="CustomerStreet" disabled="true">
        <Bindings>
          <Resource target="Label" type="Label" id="CustStreetLabel" defaultLabel="Logradouro (ShippingStreet)" />
          <Binding target="Value" binding="ProcessContext::CustomerLookup.street" bindingMode="ONE_WAY" />
        </Bindings>
      </InputArea>
    </GroupElement>
  </Area>
</Area>

2. No Fluxo de Processo (Application_MyMainCustomPluginsProcess.processflow.xml):

No processo, declaramos CustomerLookup como LuMyCustomerLookup e orquestramos a busca e o carregamento:

<!-- Data: 2026-10-14; Objetivo da Customização: Ações para invocar o seletor e instanciar o LuMyCustomerLookup -->
<Action actionType="PROCESS" name="OpenCustomCustomerLookup" process="Customer::LookupProcess">
  <ReturnValues>
    <Return name="ProcessContext::SelectedCustomerPKey" value="customerPKey" />
  </ReturnValues>
  <TransitionTo action="CheckCustomerSelected" />
</Action>

<Action name="CheckCustomerSelected" actionType="DECISION" parameter="ProcessContext::SelectedCustomerPKey">
  <CaseEmpty action="ShowMainPluginsUI" />
  <CaseElse action="LoadCustomCustomerLookup" />
</Action>

<!-- Carga ultraleve sob demanda via LuMyCustomerLookup -->
<Action actionType="LOAD" name="LoadCustomCustomerLookup" type="LuMyCustomerLookup">
  <Parameters>
    <Input name="pKey" value="ProcessContext::SelectedCustomerPKey" />
  </Parameters>
  <Return name="ProcessContext::CustomerLookup" />
  <TransitionTo action="ShowMainPluginsUI" />
</Action>


4. Matriz de Decisão: Quando Usar Cada Contrato

Critério de EngenhariaBusinessObject Completo (Bo)ListObject / ListItem (Lo/Li)LookupObject (Lu)
Finalidade PrincipalManipulação do registro principal com regras de escrita.Exibição de grades, listas e históricos homogêneos.Resolução de chaves estrangeiras e seletores de busca.
Consumo de Memória RAMAlto (mantém dirty tracking e métodos transacionais).Moderado (coleção de itens de leitura).Mínimo (apenas as colunas necessárias para identificação).
Operação de EscritaSim (saveAsync).Coleção em lote (saveAsync).Não (entidade puramente de leitura).
Controles de UI TípicosTelas de detalhes e formulários de edição.<CockpitList>, <HierarchicalList>.<LookupField> e seletores modais.

Conclusão

Projetar aplicações para o ponto de venda no Consumer Goods Cloud exige disciplina de arquitetura para que a falta de sinal de rede não signifique lentidão no dispositivo móvel.

O contrato LookupObject é a ferramenta essencial do Modeler para resolver chaves estrangeiras de forma elegante, econômica e ultrarrápida. Substituindo BusinessObjects pesados por projeções enxutas no SQLite app.db3, seu aplicativo economiza ciclos de processador e garante uma navegação ágil para o promotor de vendas em campo.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *