DataSet
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.getCachedDataSetretorna 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.getTablee restringi-la pelo métodosetView, descrito em Visão de classes . Ele substitui o método legadoconnection.cloneLocalCacheByClass, que tem o mesmo comportamento. -
Tabelas do cache local: o método
dbCache.getTableretorna 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
getCachedDataSetougetTableretorna 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.queryexecuta 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.getChildrenretorna 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étodoclauseWhereOfKeysdequeryUtilities: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.
Navegação por cursor
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 propriedadeindexFieldNames. Para pesquisar mais de um campo, indexe todos eles e informe um array de valores na mesma ordem.findNearest(values): semelhante afind, 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 olocatepercorre 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 porANDeORe 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, oufindsobre um índice sem filtros, que é compartilhado e reaproveitado por todo o sistema. Se for necessário percorrer vários registros com o mesmo valor, usefindseguido denextenquanto 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
numberou comoDBKey. 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
protectimpede 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.getTablepara uma tabela cujos registros não são sincronizados com o cache local é somente leitura.