Como contornar comportamentos não documentados do framework Mv2, implementar a Facade em ganchos assíncronos e dominar o controle transacional para nunca perder dados no aplicativo móvel.

Você aperta o botão de ação no aplicativo móvel do Salesforce Consumer Goods Cloud, o log cospe objectStatus: 1, nenhuma exceção estoura na tela e a promessa assíncrona é resolvida com louvor. Você respira aliviado. Meia hora depois, ao plugar o emulador e inspecionar o arquivo SQLite local (app.db3), vem o choque: a tabela de destino continua rigorosamente vazia. Nenhum byte foi gravado.

Essa cena é um clássico de quem desenvolve no ecossistema do CG Cloud Modeler (Mv2). Batemos a cabeça com isso incontáveis vezes.

O compilador do Modeler possui um comportamento silencioso e cruel com quem está acostumado com o Salesforce clássico ou frameworks web reativos: ele não autogera rotinas de gravação para Business Objects customizados. Se você não declarar a cadeia de métodos no XML e não invocar a Facade na camada de lógica de negócio, seu salvamento vira fumaça. Pior ainda: se você disparar essa gravação a partir de um evento avulso de interface fora de um fluxo de processo padrão, o SQLite executa um rollback implícito silencioso no encerramento da pilha de execução.

Neste artigo prático de engenharia de campo, vamos dissecar o mecanismo real de persistência offline do Modeler. Sem rodeios acadêmicos: direto nos contratos de metadados, na manipulação cirúrgica da Facade, no controle das flags de estado e na blindagem transacional que garante integridade absoluta no ponto de venda.


1. A armadilha dos métodos autogerados: o contrato no XML

No ecossistema Mv2, tudo começa pelo contrato declarativo do Business Object (.businessobject.xml). Em objetos concebidos exclusivamente para leitura, tags como generateLoadMethod="true" instruem o compilador a gerar o mapeamento SQL de consulta automaticamente.

Só que para escrita física no banco, essa mágica não existe.

Se você criar um Business Object customizado — por exemplo, uma entidade para armazenar marcações de geolocalização e auditoria em segundo plano (BoMyUserLocationTracking) — e omitir a declaração formal dos ganchos assíncronos de salvamento, o runtime simplesmente resolve bo.saveAsync() com uma promessa vazia. Nada quebra. Nenhum erro aparece no console. O SQLite simplesmente é ignorado.

Para forçar o compilador a criar os canais de persistência, abra o arquivo src/User/BO/BoMyUserLocationTracking/BoMyUserLocationTracking.businessobject.xml e configure a estrutura mandatória:

<?xml version="1.0" encoding="UTF-8"?>
<BusinessObject name="BoMyUserLocationTracking" schemaVersion="2.0">
  <DataSource name="DsBoMyUserLocationTracking" />
  <SimpleProperties>
    <!-- O atributo id="true" é mandatório para a governança automática da chave primária -->
    <SimpleProperty name="pKey" type="DomPKey" id="true" dataSourceProperty="pKey" />
    <SimpleProperty name="user" type="DomPKey" dataSourceProperty="user" />
    <SimpleProperty name="latitude" type="DomDecimal" dataSourceProperty="latitude" />
    <SimpleProperty name="longitude" type="DomDecimal" dataSourceProperty="longitude" />
    <SimpleProperty name="capturedDateTime" type="DomDateTime" dataSourceProperty="capturedDateTime" />
    <SimpleProperty name="sourceEvent" type="DomString" dataSourceProperty="sourceEvent" />
    <SimpleProperty name="accuracy" type="DomDecimal" dataSourceProperty="accuracy" />
  </SimpleProperties>
  
  <Methods>
    <!-- Ganchos de Inicialização do Ciclo de Vida -->
    <Method name="beforeCreateAsync" />
    <Method name="afterCreateAsync" />
    <Method name="createAsync" />

    <!-- Ganchos Físicos de Persistência no SQLite local (app.db3) -->
    <Method name="beforeSaveAsync" />
    <Method name="afterSaveAsync" />
    <Method name="saveAsync" />
  </Methods>
</BusinessObject>

Preste muita atenção neste detalhe: você não deve criar manualmente um arquivo saveAsync.bl.js. Quando você declara saveAsync juntamente com beforeSaveAsync e afterSaveAsync dentro da tag <Methods>, o compilador do Modeler assume a responsabilidade de encadear essas três etapas na sequência correta durante o sf modeler workspace build.


2. Gravando no SQLite na unha: a Facade no BeforeSaveAsync

Com os contratos declarados, a conexão entre o Business Object em memória e o arquivo app.db3 precisa ser executada fisicamente. No framework Mv2, essa conversa com o banco relacional local é de competência exclusiva de uma entidade: a Facade.

A chamada física ao banco não deve ficar espalhada em botões de tela ou manipuladores de eventos. O ponto arquitetural canônico para essa execução é o gancho assíncrono que precede a consolidação do registro: o BeforeSaveAsync.

Veja a implementação no arquivo src/User/BO/BoMyUserLocationTracking/Mv2/SaveAsync/BoMyUserLocationTracking.BeforeSaveAsync.bl.js:

"use strict";

/**
 * @function beforeSaveAsync
 * @this BoMyUserLocationTracking
 * @kind businessobject
 * @async
 * @namespace CUSTOM
 * @param {Object} context
 * @returns promise
 */
function beforeSaveAsync(context) {
    var me = this;

    /* 
     * Data: 2026-09-23
     * Objetivo da Customização: Validação de persistência física de Business Object no SQLite local (app.db3) via Facade.saveObjectAsync
     */
    
    // A Facade é a única responsável por disparar o comando SQL correspondente contra o app.db3
    var promise = Facade.saveObjectAsync(me).then(function() {
        AppLog.info("BeforeSaveAsync: Save Object executado com sucesso no SQLite local.", me);
        return context;
    }).catch(function(error) {
        AppLog.error("BeforeSaveAsync: Erro crítico ao gravar objeto no SQLite.", error);
        return context;
    });

    return promise;
}

Quando Facade.saveObjectAsync(me) é acionado, a camada interna do Modeler lê os metadados do DataSource associado (DsBoMyUserLocationTracking), cruza com os campos modificados na instância de me e constrói a query SQL (INSERT INTO ... ou UPDATE ...) diretamente na conexão aberta do SQLite.


3. Governança de PKeys e a Máquina de Estados: STATE.NEW | STATE.DIRTY

Em Salesforce na nuvem, novos registros recebem IDs de 18 dígitos calculados pelo banco de dados central da Salesforce. No ambiente offline do Consumer Goods Cloud Mobile, esse modelo não se sustenta: centenas de promotores estão em áreas sem sinal de operadora criando pedidos, registrando visitas e disparando pesquisas de gôndola simultaneamente.

Por isso, novos registros nascem com identificadores locais chamados DomPKey.

O erro clássico de sobrescrever a PKey

Nunca faça isso na inicialização:

// ❌ ANTI-PATTERN PERIGOSO: Destrói o rastreamento do Sync Engine
me.setPKey(PKey.next());

Se a propriedade pKey estiver mapeada com id="true" no XML do Business Object, a fábrica padrão (BoFactory.createObjectAsync) já gera uma chave temporária perfeitamente indexada e compatível com a árvore de dependências do aplicativo móvel. Quando você altera a PKey na mão, o mecanismo de sincronização (Sync Engine) perde a trilha de conversão que substitui a chave temporária pelo ID definitivo da nuvem durante o upload de dados.

A sinalização obrigatória de mutação

Para que a Facade saiba se deve emitir um INSERT ou um UPDATE, ela não advinha: ela avalia o status do objeto na máquina de estados do Mv2.

Se você acabou de instanciar o objeto na memória e preencheu seus atributos, você deve marcar expressamente que ele é novo e sofreu alterações pendentes de gravação:

// Notifica o framework: objeto novo com dados em memória prontos para gravação física<br>boLocation.setObjectStatus(STATE.NEW | STATE.DIRTY);

Sem essa flag composta (STATE.NEW | STATE.DIRTY), a Facade pode interpretar o objeto como limpo e ignorar os comandos de escrita na tabela local.


4. O temido “Silent Transaction Killer”: quando o SQLite faz Rollback sem avisar

Aqui reside o pesadelo de nove entre dez desenvolvedores do Modeler.

Quando você salva um Business Object dentro do fluxo de trabalho padrão de uma tela — isto é, em um arquivo de processo .processflow.xml onde uma transição de estado declara o atributo commit="true" —, o motor de processos do CG Cloud Mobile abre a transação SQLite antes da ação e consolida os dados automaticamente no final:

<!-- No ProcessFlow: commit explícito orquestrado pelo motor de telas -->
<Action name="SaveAndExit" actionType="PROCESS" process="ProcessBoLocation">
    <TransitionTo action="EndProcess" commit="true" />
</Action>

Tudo funciona redondo.

O desastre acontece quando a sua lógica de negócio precisa rodar fora de uma transição de processo. Exemplos comuns:

  • Um botão de ação customizado na interface (EventHandlers avulsos na UI).
  • Um serviço de rastreamento GPS ou leitura periódica de Bluetooth que roda em segundo plano.
  • Um handler de integração de scanner de código de barras ou câmera fotográfica.

Nesses cenários fora do fluxo de processo, se você instanciar o objeto e chamar bo.saveAsync(), o SQLite abre uma transação implícita e faz um ROLLBACK automático no encerramento da pilha de execução.

O console cospe logs verdes. O método retorna sucesso. Mas no disco físico, o SQLite simplesmente descartou a transação porque ninguém ordenou o commit.

A solução definitiva: transação manual com a Facade

Para neutralizar o descarte silencioso em chamadas avulsas, você é obrigado a controlar o ciclo transacional explicitamente. Veja como implementamos essa regra no arquivo src/User/BO/BoUser/Mv1/BoUser.MyCaptureUserLocation.bl.js:

"use strict";

/**
 * @function myCaptureUserLocation
 * @this BoUser
 * @kind businessobject
 * @async
 * @namespace CUSTOM
 * @param {String} sourceEvent
 * @returns promise
 */
function myCaptureUserLocation(sourceEvent) {
    var me = this;

    /* 
     * Data: 2026-09-23
     * Objetivo da Customização: Captura de coordenadas GPS do usuário, instância de BoMyUserLocationTracking e transação manual no SQLite (app.db3) via Facade.
     */

    var promise = when.resolve();

    promise = Utils.getCurrentPosition().then(function(position) {
        if (!Utils.isDefined(position)) {
            position = { latitude: 0, longitude: 0, accuracy: 0 };
        }

        var initValues = {
            user: me.getPKey(),
            latitude: Utils.isDefined(position.latitude) ? position.latitude : 0,
            longitude: Utils.isDefined(position.longitude) ? position.longitude : 0,
            capturedDateTime: Utils.convertFullDate2Ansi(Utils.createDateNow()),
            sourceEvent: sourceEvent || "Application_Generic",
            accuracy: Utils.isDefined(position.accuracy) ? position.accuracy : 0
        };

        return BoFactory.createObjectAsync("BoMyUserLocationTracking", {}).then(function(boLocation) {
            boLocation.setUser(me.getPKey());
            boLocation.setLatitude(initValues.latitude);
            boLocation.setLongitude(initValues.longitude);
            boLocation.setCapturedDateTime(initValues.capturedDateTime);
            boLocation.setSourceEvent(initValues.sourceEvent);
            boLocation.setAccuracy(initValues.accuracy);
            
            // Marcação explícita de estado
            boLocation.setObjectStatus(STATE.NEW | STATE.DIRTY);
            
            // 1. Abertura manual da transação no SQLite local
            Facade.startTransaction();
            
            // 2. Disparo da esteira de persistência do Business Object
            return boLocation.saveAsync().then(function() {
                
                // 3. MANDATÓRIO: Consolidação física dos dados no app.db3
                return Facade.commitTransactionAsync().then(function() {
                    AppLog.info("MyCaptureUserLocation: Registro consolidado com sucesso no SQLite!");
                });

            }).catch(function(err) {
                
                // 4. Rollback seguro em caso de falha durante o salvamento
                Facade.rollbackTransaction();
                AppLog.error("MyCaptureUserLocation: Erro no saveAsync, rollback realizado.", err);
                throw err;
            });
        });
    }).catch(function(error) {
        AppLog.error("Location Tracking Error: ", error);
    });

    return promise;
}

O trio Facade.startTransaction(), Facade.commitTransactionAsync() e Facade.rollbackTransaction() é a única garantia contra a perda de registros quando a gravação não ocorre dentro de um fluxo de transição de telas.


5. Matriz de comparação: ciclos transacionais no Mv2

Critério de AvaliaçãoExecução via Process Flow PadrãoExecução Avulsa (UI Events / Background Timers)
Abertura da TransaçãoGerenciada automaticamente pelo motor de processosManual via Facade.startTransaction()
Confirmação dos DadosConfigurada no XML com commit="true" na transiçãoManual via Facade.commitTransactionAsync()
Comportamento em FalhaRollback automático pelo runtime de navegaçãoManual via Facade.rollbackTransaction()
Risco de Rollback SilenciosoBaixo (ocorre apenas se o atributo commit for omitido)Crítico se a transação manual for esquecida
Casos Típicos de UsoCheck-in de Visita, Finalização de Pedido, InventárioRastreamento de localização, eventos de clique avulso, scanners

6. Evidências de execução real: Modeler e Org Salesforce de testes

Para validar cada linha deste contrato, compilamos o workspace com as ferramentas oficiais da CLI do Modeler (sf modeler workspace validate e sf modeler workspace build)

Validation status for Modeler workspace at path 'projeto_modeler'.... done
Validation successful
Build status for Modeler workspace at path 'projeto_modeler':... done
Build successful

Nenhum erro de schema XSD, nenhum conflito de DataSource e conformidade total com o padrão Mv2.


7. Checklist definitivo para não perder dados no app.db3

Antes de gerar a compilação final e distribuir sua release para os dispositivos móveis dos promotores de campo, faça uma checagem rigorosa:

  • Declaração no XML: O arquivo .businessobject.xml declara explicitamente os métodos beforeSaveAsync, afterSaveAsync e saveAsync na tag <Methods>.
  • PKey id=”true”: A propriedade de identificador principal possui id="true" e ninguém está injetando setPKey() na mão.
  • Gravação física: O arquivo BeforeSaveAsync.bl.js invoca Facade.saveObjectAsync(me) e retorna a promessa encadeada.
  • Flags de mutação: Objetos novos recém-populados recebem bo.setObjectStatus(STATE.NEW | STATE.DIRTY).
  • Proteção contra Silent Rollback: Todo salvamento disparado fora de um ProcessFlow com commit="true" está encapsulado por Facade.startTransaction() e Facade.commitTransactionAsync().
  • Mapeamento de Sync: O DataSource do BO possui correspondência no schema do banco local e na tabela de sincronização de subida para a nuvem.

No fim das contas, a camada offline do Consumer Goods Cloud Modeler não é um bicho de sete cabeças. Ela apenas exige que o engenheiro assuma o controle explícito do ciclo de vida que a maioria dos frameworks modernos esconde por trás de abstrações. Quando você trata o SQLite local com o rigor transacional que ele exige, o aplicativo de campo se torna à prova de qualquer apagão de conectividade.

Deixe um comentário

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