
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:
- 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.
- 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.
- 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
BoCustomercarrega dezenas de colunas do SQLite (endereços completos, limites de crédito, contatos, classificações fiscais), oLuMyCustomerLookupcarrega 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 comoreadOnly="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
pKeydeve possuir obrigatoriamenteid="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 Engenharia | BusinessObject Completo (Bo) | ListObject / ListItem (Lo/Li) | LookupObject (Lu) |
|---|---|---|---|
| Finalidade Principal | Manipulaçã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 RAM | Alto (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 Escrita | Sim (saveAsync). | Coleção em lote (saveAsync). | Não (entidade puramente de leitura). |
| Controles de UI Típicos | Telas 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.
