DataSet

Uso da classe DataSet para ler, navegar, alterar, gravar, pesquisar e filtrar registros, com as práticas recomendadas para cada operação.

A classe DataSet é a API base para a leitura e a manipulação de registros no sistema. Ela representa um conjunto de registros com campos tipados, sobre o qual o código navega, lê valores, insere, altera e remove registros, e por meio do qual essas alterações são gravadas no banco de dados.

Este manual apresenta os conceitos e as práticas recomendadas de uso. A descrição completa de cada método e propriedade está na documentação da API .

Obtendo um DataSet

Na maioria dos casos, um DataSet é obtido de uma das seguintes formas:

  • Classes do cache local: o método classes.getCachedDataSet retorna os registros de uma classe de dados e das suas classes filhas, a partir de uma tabela do cache local . Quando é informado um usuário, são retornados apenas os registros que ele tem permissão de visualizar. Essa é a forma mais comum de obter um DataSet, pois o código normalmente trabalha com os registros de uma classe de dados, e não com todos os registros de uma tabela.

    const classes = require('@nginstack/engine/context/classes.js');
    const session = require('@nginstack/engine/context/session.js');
    const ClassKeys = require('@nginstack/engine/keys/Classes.js');
    
    const users = classes.getCachedDataSet(ClassKeys.USERS, session.userKey);
    

    O método equivale a obter a tabela da classe por dbCache.getTable e restringi-la pelo método setView, descrito em Visão de classes . Ele substitui o método legado connection.cloneLocalCacheByClass, que tem o mesmo comportamento.

  • Tabelas do cache local: o método dbCache.getTable retorna um DataSet com todos os registros de uma tabela do cache local, de todas as classes que a compartilham, sem considerar as permissões do usuário. Seu uso é indicado quando o código realmente precisa de todos os registros da tabela, como em rotinas de manutenção, ou apenas pesquisa registros pela chave.

    const dbCache = require('@nginstack/engine/context/dbCache.js');
    
    const groupUsers = dbCache.getTable('iGroupUser');
    

    Cada chamada de getCachedDataSet ou getTable retorna um novo DataSet, com posição, índice e filtros próprios, mas os registros são os mesmos do cache local, compartilhados por todo o Engine. A leitura não acessa o banco de dados.

  • Consultas ao banco de dados: o método Database.prototype.query executa uma expressão SQL e retorna o resultado em um DataSet.

    const database = require('@nginstack/engine/context/database.js');
    const toSqlString = require('@nginstack/engine/lib/string/toSqlString.js');
    
    const ds = database.query(
      'SELECT iKey, iName FROM iGroupUser WHERE iName LIKE ' + toSqlString('A%')
    );
    

    Os valores informados na expressão SQL devem ser convertidos por funções como toSqlString, e nunca concatenados diretamente. Veja a boa prática Não concatene valores diretamente em uma expressão SQL .

    Assim como no cache local, uma consulta normalmente deve se restringir às classes de dados com que o código trabalha e às permissões do usuário. O método classes.getChildren retorna as chaves de uma classe e das suas classes filhas e, quando é informado um usuário, apenas as das classes que ele tem permissão de visualizar. A cláusula SQL com essas chaves é montada pelo método clauseWhereOfKeys de queryUtilities:

    const classes = require('@nginstack/engine/context/classes.js');
    const database = require('@nginstack/engine/context/database.js');
    const queryUtilities = require('@nginstack/engine/context/queryUtilities.js');
    const session = require('@nginstack/engine/context/session.js');
    const ClassKeys = require('@nginstack/engine/keys/Classes.js');
    
    const classKeys = classes.getChildren(ClassKeys.USERS, session.userKey);
    if (classKeys !== '-1') {
      const users = database.query(
        'SELECT iKey, iVersion, iClass, iName FROM iGroupUser ' +
          queryUtilities.clauseWhereOfKeys('WHERE', 'iClass', classKeys)
      );
      // ...
    }
    

Um DataSet também pode ser criado diretamente, quando é necessário montar um conjunto temporário de registros que não está associado a uma tabela:

const DataSet = require('@nginstack/engine/lib/dataset/DataSet.js');

const ds = new DataSet();
ds.fieldDefs.add('code', 'int32');
ds.fieldDefs.add('name', 'string', 100);
ds.fieldDefs.add('amount', 'number');
ds.create();

Os tipos de campos disponíveis são string, memo, int32, int64, number, date, datetime e boolean.

Visão de classes

Uma tabela armazena os registros de todas as classes da hierarquia que a utiliza, como descrito em Classes de dados . Por isso, um DataSet de uma tabela raramente deve ser utilizado sem restrições: o código normalmente trabalha com os registros de uma classe e, quando atua em nome de um usuário, apenas com os registros que ele tem permissão de visualizar.

Essa restrição é definida pelo método setView, que restringe o DataSet aos registros de uma classe e das suas classes filhas e, quando é informado um usuário, aplica as suas permissões de visão:

const groupUsers = dbCache.getTable('iGroupUser');
groupUsers.setView(ClassKeys.USERS, session.userKey);

O método classes.getCachedDataSet já retorna o DataSet com a visão definida, por isso a visão costuma estar presente de forma implícita na maioria dos DataSets do cache local. Nas consultas ao banco de dados, a restrição deve ser feita na própria expressão SQL, filtrando o campo de classe pelas classes que o usuário visualiza, como no exemplo de Obtendo um DataSet . Assim, o banco de dados não retorna registros que seriam descartados em seguida.

A visão funciona como a base dos demais filtros do DataSet: os filtros da propriedade filter e o range são aplicados sobre os registros da visão, e os índices criados pelo DataSet também a consideram. Por ser uma restrição de segurança, a visão não é removida quando os filtros são alterados nem desabilitada pela propriedade filtered.

O DataSet adota o modelo de navegação por cursor: em cada momento, ele está posicionado em um único registro, o registro corrente. As leituras e as alterações de campos sempre se referem ao registro corrente e os métodos de navegação mudam qual registro ocupa essa posição.

Membro Descrição
first() Posiciona o cursor no primeiro registro.
next() Avança para o próximo registro.
prior() Volta para o registro anterior.
last() Posiciona o cursor no último registro.
eof Indica que o cursor passou do último registro ou que o DataSet está vazio.
bof Indica que o cursor passou do primeiro registro ou que o DataSet está vazio.
isEmpty Indica que o DataSet não possui registros.
recordCount Quantidade de registros do DataSet, considerando os filtros e o range ativos.

A forma recomendada de percorrer todos os registros é o laço com first, eof e next:

for (ds.first(); !ds.eof; ds.next()) {
  total += ds.num('amount');
}

O método iterate percorre os registros chamando uma função para cada um e, ao final, restaura o registro corrente anterior. Ele é útil quando a posição do DataSet não deve ser alterada, mas a função informada não pode mudar a posição do DataSet, remover registros ou alterar campos que afetem os filtros ou a ordem do índice.

Quando for necessário percorrer os mesmos registros sem perder a posição de um DataSet que está em uso, como o de uma grade, use newSharedDataSet. Ele cria um DataSet com os mesmos dados, mas com cursor, índice, filtro e range próprios.

Leitura de campos

Os valores dos campos do registro corrente devem ser lidos pelos métodos tipados:

Método Retorno Valor incompatível com o tipo
str(expr) string Convertido em string.
num(expr) number Gera um erro. Retorna 0 quando o campo é nulo.
bool(expr) boolean Convertido como em Boolean(value).
date(expr) Date ou null Gera um erro. Retorna null quando o campo é nulo.
dbkey(expr) DBKey ou null Gera um erro se não for um inteiro. Retorna null quando o campo é nulo.
const name = ds.str('iName');
const classKey = ds.dbkey('iClass');

Esses métodos garantem o tipo do valor retornado, por isso são preferíveis aos métodos val e getField, cujo tipo de retorno varia conforme o valor do campo.

Os campos de um DataSet também podem ser lidos ou alterados como propriedades, como em ds.iName. Essa forma não é mais recomendada, pois ela não garante o tipo do valor retornado e dificulta o uso de ferramentas de verificação de tipos e do IntelliSense. Por serem propriedades dinâmicas, essas ferramentas não conseguem identificar essas propriedades, nem deduzir o tipo delas.

Expressões de campos do cache local

Quando um campo contém a chave de um registro do cache local, os métodos tipados aceitam uma expressão de campos separados por ponto, que lê um campo do registro referenciado:

const groupUsers = dbCache.getTable('iGroupUser');
if (groupUsers.findKey(session.userKey)) {
  groupUsers.str('iClass.NOME'); // => nome da classe do usuário
}

Se um dos campos intermediários da expressão for nulo, a avaliação é interrompida e é retornado o valor correspondente a nulo do método utilizado, sem a necessidade de verificar cada campo.

Alteração de registros

Membro Descrição
append() Inicia a inserção de um novo registro em branco, que passa a ser o registro corrente.
setField(field, value) Atribui um valor a um campo do registro corrente.
edit() Coloca o registro corrente em edição.
post() Confirma a inserção ou a edição do registro corrente.
cancel() Descarta a inserção ou a edição do registro corrente.
del() Remove o registro corrente e posiciona o cursor no registro seguinte.
state Estado do DataSet: navegação, inserção ou edição.
ds.append();
ds.setField('code', 1);
ds.setField('name', 'Maria');
ds.post();

Não é necessário chamar edit antes de alterar um registro existente: ao atribuir um valor diferente do atual com setField, o DataSet entra em edição automaticamente. A inserção ou a edição também é confirmada automaticamente quando o DataSet é navegado para outro registro. Mesmo assim, chame post ao terminar de alterar um registro, deixando claro o momento em que a alteração é confirmada.

Como del já avança para o registro seguinte, um laço que remove registros não deve chamar next após a remoção:

ds.first();
while (!ds.eof) {
  if (ds.num('amount') < 0) {
    ds.del();
  } else {
    ds.next();
  }
}

Gravação no banco de dados

As alterações confirmadas por post e del ficam registradas apenas no DataSet. Elas são gravadas no banco de dados pelo método applyUpdates:

const ds = database.query('SELECT iKey, iVersion, iName FROM iGroupUser WHERE ...');
for (ds.first(); !ds.eof; ds.next()) {
  ds.setField('iName', ds.str('iName').trim());
  ds.post();
}
ds.applyUpdates();

Para que as alterações de um DataSet obtido por consulta possam ser gravadas, a consulta deve ser feita sobre uma única tabela definida no modelo de dados e retornar os campos de chave e de versão, iKey e iVersion ou, nas tabelas que seguem o padrão antigo de nomes, CHAVE e VERSAO. A versão permite detectar se o registro foi alterado por outra transação após a leitura, como descrito em Chaves e versões .

O applyUpdates grava todas as alterações pendentes em uma única transação: ou todas são gravadas, ou nenhuma é. Os registros gravados na mesma transação recebem o mesmo número de versão. O método database.applyUpdates recebe um array de DataSets e grava as alterações de todos eles em uma única transação.

Quando a propriedade automaticApplyUpdates está habilitada, cada registro é gravado no banco de dados assim que a sua alteração é confirmada. Ela já vem habilitada nos DataSets obtidos a partir do cache local, como os de dbCache.getTable e classes.getCachedDataSet, e desabilitada nos resultados de consultas e nos DataSets criados diretamente.

A gravação registro a registro é adequada para alterações pontuais. Ao alterar muitos registros, desabilite a gravação automática e grave as alterações de uma só vez, o que reduz a quantidade de transações no banco de dados e garante que as alterações sejam gravadas em conjunto:

const ds = dbCache.getTable('iGroupUser');
ds.automaticApplyUpdates = false;
// ... altera vários registros
ds.applyUpdates();

As alterações gravadas no banco de dados são propagadas para o cache local pela sincronização do Engine. Quando o código precisar ler os registros alterados logo após a gravação, informe true no parâmetro waitDBCacheSync de applyUpdates, que aguarda essa sincronização.

Inserção automática de chaves

Os registros do sistema são identificados por uma chave única , gravada no campo iKey ou CHAVE. Quando a propriedade insertWithKey está habilitada, o DataSet gera automaticamente uma nova chave em cada registro inserido por append.

A inserção automática de chaves já vem habilitada nos DataSets obtidos a partir do cache local e desabilitada nos resultados de consultas e nos DataSets criados diretamente. Ao inserir registros em um DataSet obtido por consulta, habilite-a ou informe a chave de cada registro:

const ds = database.query('SELECT * FROM iGroupUser WHERE 1 = 0');
ds.insertWithKey = true;
ds.append();
ds.setField('iName', 'Novo grupo');
// ... demais campos
ds.post();
ds.applyUpdates();

Ao sincronizar um DataSet a partir de outro, os registros copiados já possuem as suas chaves e uma nova chave não deve ser gerada. Veja a boa prática Evite criar chaves ao sincronizar DataSets .

Pesquisa de registros

As pesquisas posicionam o cursor no registro encontrado e, em geral, retornam um booleano indicando se o registro foi encontrado.

  • findKey(key): pesquisa um registro pela chave. Nos DataSets do cache local, não depende de índices do DataSet e é a forma mais rápida de localizar um registro.
  • find(values): pesquisa os valores nos campos do índice corrente, definido pela propriedade indexFieldNames. Para pesquisar mais de um campo, indexe todos eles e informe um array de valores na mesma ordem.
  • findNearest(values): semelhante a find, mas posiciona o cursor no primeiro registro igual ou maior que os valores informados, ou no último registro se não houver nenhum maior. Este método sempre posiciona o cursor em um registro e, por esse motivo, ele não retorna um booleano indicando o sucesso da pesquisa.
  • locate(fields, values): pesquisa campos que não fazem parte do índice corrente. Em geral o locate percorre todos os registros do DataSet. No entanto, caso exista um índice que contenha todos os campos informados, ele pode utilizar esse índice para otimizar a pesquisa. Essa otimização ocorre mesmo quando o índice encontrado não é o índice corrente do DataSet.
const groupUsers = dbCache.getTable('iGroupUser');
groupUsers.indexFieldNames = 'iClass;iName';
if (groupUsers.find([ClassKeys.USERS, 'maria'])) {
  // ...
}

Prefira findKey e find a locate. O índice utilizado por find é criado uma única vez nas tabelas do cache local e é compartilhado por todos os DataSets da mesma tabela, inclusive os de outros processos e usuários, enquanto o locate pode percorrer todos os registros a cada chamada. Use o locate apenas quando não puder alterar o índice corrente do DataSet, como em um DataSet exibido em uma grade.

Filtros e range

Os filtros e o range restringem os registros visíveis no DataSet: a navegação, o recordCount e as pesquisas passam a considerar apenas os registros que os satisfazem. Eles são aplicados sobre a visão de classes , que normalmente já foi definida quando o DataSet é obtido por classes.getCachedDataSet. Assim, não é necessário filtrar a classe dos registros novamente.

Filtros

A propriedade filter recebe uma expressão ou um array de expressões, combinadas por AND. O tipo de cada expressão é indicado por um prefixo:

  • Sem prefixo, ou simple:: condições simples, em que cada condição compara um campo com um valor, combinadas por AND e OR e agrupadas por parênteses. São avaliadas pelo próprio Engine e devem ser a primeira opção.

    ds.filter = 'iKey IN (-1898186559, -1898186558)';
    ds.filter = '(iValue < 10 OR iValue > 100) AND iDate >= "2026-01-01"';
    
  • class:: chaves de classes, separadas por vírgula. Mantém visíveis os registros das classes informadas, sem incluir as classes filhas.

  • js:: expressão JavaScript avaliada para cada registro. É o filtro mais caro, pois executa JavaScript em todos os registros da tabela, e deve ser usado apenas quando as condições simples não forem suficientes.

A propriedade filtered permite desabilitar temporariamente os filtros de filter, mantendo apenas a visão de classes, caso ela tenha sido definida.

Range

O método setRange restringe o DataSet a uma faixa de valores dos campos do índice corrente:

details.indexFieldNames = 'iClient';
details.setRange(clientKey, clientKey);

O range não cria um índice: ele apenas limita a navegação a um trecho do índice já existente, inclusive de um índice com filtros. Por isso, é uma restrição barata, que pode ser alterada a cada mudança de registro sem custo de construção. O uso típico é o de uma grade de detalhe, cujo range é alterado sempre que muda o registro da grade principal. Para remover o range, use resetRange.

Quando usar cada abordagem

Cada combinação de visão, de campos indexados e de filtros de um DataSet requer a construção de um índice na tabela do banco de dados local do Engine. Esse índice é construído sob demanda na primeira utilização e passa a ser atualizado em todas as gravações na tabela, até ser descartado por falta de uso. Na arquitetura do sistema, os filtros têm um custo de construção e de manutenção que não existe quando os registros são apenas lidos e pesquisados com findKey e find. Por esse motivo, o uso de filtros deve ser feito com critério e abaixo estão algumas recomendações:

  • Para ler registros em bibliotecas e regras de negócio, use a pesquisa: findKey, ou find sobre um índice sem filtros, que é compartilhado e reaproveitado por todo o sistema. Se for necessário percorrer vários registros com o mesmo valor, use find seguido de next enquanto o valor se mantiver, ou um range.
  • Use filtros quando os dados precisarem ser apresentados ou entregues já filtrados, como em um DataSet exibido em uma grade ou retornado por uma API, e quando a mesma restrição for reutilizada em muitas leituras.
  • Use o range para restrições que mudam com frequência sobre um mesmo índice, como os registros de detalhe de um registro principal.

Evite alterar o índice e os filtros de um DataSet repetidamente em sequência, pois cada combinação diferente pode criar um índice. Defina o índice e os filtros uma vez e use pesquisas e ranges para variar os registros lidos.

Cuidados comuns

  • Comparação de chaves: os valores das chaves podem ser lidos como number ou como DBKey. Veja a boa prática Evite comparações estritas de chaves numéricas .
  • Registro corrente após o post: se a alteração deixar de satisfazer os filtros do DataSet, o registro alterado deixa de ser visível e não será o registro corrente. Veja a boa prática Atenção no acesso ao registro corrente no evento “afterPost” .
  • DataSets protegidos: o método protect impede a alteração de campos, a inserção, a remoção ou a navegação em um DataSet. Ao receber um DataSet de outro componente, não presuma que ele possa ser navegado ou alterado.
  • Tabelas fora do cache local: o DataSet retornado por dbCache.getTable para uma tabela cujos registros não são sincronizados com o cache local é somente leitura.