Boas práticas
Este documento foi criado com o objetivo de apresentar sugestões de boas práticas para assuntos que são particulares da plataforma de desenvolvimento do Engine e do Web Framework.
Geral
Não utilize APIs não documentadas
O runtime JavaScript Ije não possui um método eficiente de criar APIs privadas. Por esse motivo, métodos e propriedades são prefixados ou sufixados com “_” para indicar privacidade. No entanto, essa solução não pode ser aplicada aos módulos e classes, sem poluir artificialmente os caminhos de inclusão desses módulos. Também há propriedades legadas que nunca chegaram a adotar uma nomenclatura de privacidade.
Em razão disso, deve-se considerar que apenas os módulos, classes, propriedades e métodos documentados em https://api.nginstack.com são APIs públicas. APIs que não estejam documentadas no site devem ser consideradas privadas ou experimentais e não há nenhuma garantia de estabilidade sobre elas. Atualizações do sistema podem remover ou alterar o comportamento dessas APIs não documentadas sem nenhum tipo de aviso.
Vale ressaltar que a interface cliente do Web Framework é uma implementação interna do sistema e é gerada de forma dinâmica a partir do uso das APIs públicas e documentadas. Não devem ser realizadas customizações ou automações que dependam de detalhes internos de implementação da interface, como a hierarquia e identificadores de tags HTML, classes CSS e APIs JavaScript disponíveis no cliente. Essas estruturas são alteradas livremente na evolução da interface cliente do sistema e não há como preservar o comportamento das versões anteriores. Parte delas são geradas de forma automática pelas ferramentas utilizadas na construção do sistema, podendo ser modificadas a cada nova versão liberada.
Evite utilizar APIs legadas
Via de regra, a evolução do sistema tenta preservar ao máximo as APIs públicas existentes a fim de garantir que os códigos construídos sobre elas continuem a funcionar corretamente a cada atualização do sistema. Mesmo APIs marcadas como legadas são preservadas de forma indefinida, desde que o seu uso não comprometa o funcionamento e a segurança do sistema.
No entanto, é importante observar que apesar de serem mantidas em funcionamento, as APIs legadas deixam de receber evoluções, melhorias de desempenho e até correções que possam afetar o comportamento dos códigos existentes. Por esse motivo, é recomendado que em algum momento o seu uso seja revisto e que elas jamais sejam utilizadas em códigos novos. Insistir no uso delas pode perpetuar os problemas que levaram essas APIs a serem classificadas como legadas.
Importante: apesar do objeto connection
e sua classe
Connection
,
não serem marcados como APIs legadas, a maioria das suas propriedades e métodos são.
O connection
mistura informações de diversos domínios e fontes de dados, algumas obtidas
rapidamente a partir do cache local ou memória, outras por meio de custosas requisições ao Engine
servidor ou ao banco de dados. O seu uso é confuso e historicamente tem provocado problemas
graves de desempenho e estabilidade do sistema. Por esse motivo, todos os métodos e propriedades
que não dizem respeito ao Engine servidor associado à conexão são considerados legados e não
deveriam ser mais utilizados. Na maioria das situações, o uso do connection
deve ser substituído
pelas APIs
database
,
dbCache
,
classes
,
security
e
engine
.
Scripts de inicialização e configuração não devem acessar a base de dados
Scripts de inicialização, como os de Sessão, Engine e Web Framework, e os scripts de definição das classes de dados, como os arquivos x-config, x-model, x-class e x-view, não devem fazer consultas à base de dados, nem utilizar APIs que façam requisições ao Engine servidor.
Além de prejudicarem o desempenho do sistema, as requisições ao banco de dados e ao Engine servidor durante a execução dos scripts de inicialização e configuração impedem a operação off-line do sistema e podem sobrecarregar o Engine servidor com requisições recursivas na obtenção das configurações das classes de dados do sistema, podendo levar até o esgotamento do atendimento do servidor.
Observação: o objeto connection
não deveria ser utilizado em scripts de inicialização
e configuração, pois não é claro quais dos seus métodos e propriedades são resolvidos sem
requisições ao servidor. Em seu lugar, utilize as APIs
dbCache
,
classes
,
security
e
engine
.
Evite nomear uma função definida em uma expressão
Expressões de funções atribuídas a objetos ou variáveis tem o seu nome definido automaticamente pelo runtime JavaScript, não devendo ser nomeadas manualmente.
Errado:
const test = function Test() {
};
function Class() {}
Class.prototype.test = function Class_test() {
};
Correto:
const test = function () {
};
function Class() {}
Class.prototype.test = function () {
};
No exemplo acima, a primeira função terá o nome definido automaticamente pelo runtime JavaScript
como test
e a segunda como Class.test
. A nomeação manual é um trabalho supérfluo que gera
manutenção, pois a variável ou nome da classe podem mudar. Quando isso ocorre, o nome definido
manualmente pode ficar desatualizado, podendo confundir as análises do profiler ou da pilha de
chamadas de erro realizadas por um desenvolvedor que está esperando o comportamento padrão
do sistema.
Uma exceção importante a essa prática ocorre em módulos que exportam funções. Nesse caso, é uma boa prática nomear a função, pois o runtime JavaScript não tem como sugerir um nome adequado. Exemplo:
module.exports = function test() {
};
Banco de dados
Não concatene valores diretamente em uma expressão SQL
Todo valor concatenado em uma expressão SQL deve ser tratado a fim de prevenir ataques de injeção.
Errado:
const ds = database.query("SELECT * FROM iGroupUser WHERE iKey = " + this.chaveUsuario);
Correto:
const toSqlString = require('@nginstack/engine/lib/string/toSqlString');
const ds = database.query("SELECT * FROM iGroupUser WHERE iKey = " + toSqlString(this.chaveUsuario));
Mais detalhes no manual Prevenção de injeção de códigos.
Não altere o tipo do dado das colunas
De uma forma geral, a ampliação do tamanho de campos do tipo “varchar” e o aumento da precisão e escala dos tipos numéricos são as únicas alterações bem suportadas por todos os SGBDs.
As demais alterações de tipo de dado de uma coluna não são recomendadas pelos SGBDs suportados pelo Engine. O Oracle em particular não permite a alteração de tipo de dados em colunas que têm valores preenchidos e os demais SGBDs permitem a conversão de forma restrita, normalmente às custas de uma reescrita completa da tabela. Reescritas de tabelas podem ser extremamente demoradas e onerosas, bloqueando o uso do sistema durante todo o processo de conversão, que pode ser na ordem de horas.
Os processos de atualização do sistema se limitam a aplicar os comandos de alteração do tipo de dado e falharão caso o SGBD não suporte as modificações realizadas. Por esse motivo, caso a conversão seja estritamente necessária, ela deverá ser realizada por processos de manutenção que devem ser desenvolvidos para os casos específicos. É importante que o processo avalie a natureza da alteração e trate a possibilidade da conversão não poder ser realizada durante uma janela de manutenção da base de dados de produção. Também deve ser avaliado o custo de armazenagem requerido para a conversão, que pode duplicar temporariamente o tamanho das tabelas afetadas.
Utilize apenas a sintaxe SQL padrão e os tipos normalizados pelo Engine
As instruções SQL não devem utilizar funcionalidades específicas de um SGBD e devem utilizar os tipos de dados e funções normalizadas pelo Engine a fim de garantir a interoperabilidade entre os SGBDs. Mais detalhes no manual Bancos de dados.
Operações aritméticas com valores decimais
Os valores numéricos decimais no JavaScript são representados internamente por pontos flutuantes de precisão dupla. Essa é uma representação extremamente eficiente para cálculos computacionais, no entanto, por serem uma representação aproximada, não permite que todos os valores possam ser representados precisamente por ela. Cálculos triviais podem gerar resultados que em um primeiro momento podem parecer surpreendentes, como:
9646.55 - 9646.54 == 0.01; // => false
Esse resultado não é específico do Engine, nem é algo particular do JavaScript. Todas as linguagens
de programação que adotam essa representação apresentam esse comportamento. É importante observar
que essa não é a única forma de representar números decimais e para cálculos financeiros são
recomendadas APIs ou tipos alternativos que permitam aritmética decimal com precisão arbitrária, como
a classe
BigDecimal
disponibilizada pelo Engine.
Dado o contexto acima, seguem algumas práticas recomendadas na manipulação de valores numéricos.
Use a classe BigDecimal
Sempre que for possível, utilize a classe
BigDecimal
.
Ela permite cálculos aritméticos com precisão de até 38 dígitos. Exemplo:
new BigDecimal(9646.55).minus(9646.54).equals(0.01); // => true
Arredonde os resultados das operações aritméticas
Os resultados das operações aritméticas devem ser arredondados por meio da função
Math.decimalRound
para
uma quantidade de casas decimais que faça sentido para o domínio de negócio tratado. Por exemplo,
para valores monetários, possivelmente não fará sentido armazenar valores com mais de 2 casas decimais.
A função decimalRound
também permite indicar a estratégia de arredondamento, que pode
ser diferente de acordo com as regulamentações envolvidas. Por exemplo, no
convênio ICMS 85 / 01 é
determinado que os valores das vendas registradas em equipamentos fiscais sejam arredondados de
acordo com a norma ABNT NBR 5891, equivalente ao
RoundingMode.HALF_EVEN
,
exceto quando se tratar de comercialização de combustíveis, que requer que os valores sejam
truncados, equivalente ao modo
RoundingMode.DOWN
.
Outras regulamentações podem existir, não havendo uma solução única para todos os domínios de
negócio. O desenvolvedor deve estar atento às exigências legais envolvidas e usar o modo de
arredondamento adequado. Exemplo de uso:
Math.decimalRound(2.555, 2, RoundingMode.HALF_EVEN); // => 2.56
Math.decimalRound(2.555, 2, RoundingMode.DOWN); // => 2.55
Outro aspecto que deve ser levado é consideração é o limite de precisão dos valores gravados
no banco de dados. Apesar do tipo number
utilizar internamente uma representação de ponto
flutuante de precisão dupla, o Engine grava esses valores no banco de dados em campos de
tipo numérico com precisão definida em 38 e escala 10. Na prática, isso significa que no máximo
poderão ser armazenados valores com 28 dígitos inteiros e 10 casas decimais. Valores com
uma quantidade maior de casas decimais serão arredondados e aqueles com mais de 28 dígitos
inteiros não serão aceitos, sendo gerado um erro no momento da gravação. É importante que o
desenvolver tenha esses limites em mente e antecipe o arrendondamento para a camada de negócio,
evitando assim a divergência entre o que foi gravado e o que será lido posteriormente do banco
de dados.
Evite comparações exatas
Em vez de verificar se um número é zero, utilize
Math.isZero
.
Essa função verifica se valor é aproximadamente zero tolerando uma diferença de até 0.0000001
positiva ou negativa. Essa tolerância é suficiente para contornar as imprecisões introduzidas
pelo tipo number
, que normalmente ocorrem na ordem de 1E-012. Exemplo de uso:
(9646.55 - 9646.54 - 0.01) === 0; // => false
Math.isZero(9646.55 - 9646.54 - 0.01); // => true
Um erro similar é verificar se o valor está contido em intervalo sem levar em consideração
a imprecisão do tipo number
. No exemplo abaixo, a função doSomething()
não será chamada, apesar
do valor ser equivalente a 0.01
:
const value = 9646.55 - 9646.54; // => 0.00999999999839929
if (value >= 0.01 && value < 10) {
doSomething(value);
}
A lógica acima pode ser revista para utilizar a classe BigDecimal
para realizar as comparações de
forma precisa:
const value = new BigDecimal(9646.55 - 9646.54).toDecimalPlaces(2); // => 0.01
if (value.greaterThanOrEqualTo(0.01) && value.lessThan(10)) {
doSomething(value);
}
Uso de chaves
Chaves são um recurso limitado do sistema e não devem ser desperdiçadas. De uma forma geral:
- não devem ser criadas chaves que não serão de fato gravadas no banco de dados.
- abertura de operações nunca devem criar chaves, pois uma operação pode ser aberta diversas vezes
após a sua criação, sem que sejam efetivadas alterações. É importante que sejam criados testes de
integração que garantam que a abertura de operações não criem chaves. A API
session.limitKeyCreation
pode ser utilizada para limitar a criação de chaves durante a execução dos testes.
Evite comparações estritas de chaves numéricas
Chaves podem ser representadas por valores do tipo number
ou por instâncias da classe DBKey
. O
uso da comparação estrita impede que as duas representações distintas de uma mesma chave
sejam tratadas como equivalentes. Quando forem comparadas chaves com os valores dos campos
de um DataSet, deve ser dada preferência à comparação não estrita com o valor numérico da chave.
Errado:
const keys = [];
ds.indexFieldNames = 'iParent';
if (ds.find(parent)) {
while (!ds.eof && ds.getField('iParent') === parent) {
keys.push(ds.getField('iKey'));
ds.next();
}
}
Correto:
const keys = [];
ds.indexFieldNames = 'iParent';
if (ds.find(parent)) {
while (!ds.eof && ds.num('iParent') == parent) {
keys.push(ds.num('iKey'));
ds.next();
}
}
Evite instâncias persistentes de DBKey
No runtime JavaScript Ije, os campos dos registros armazenados no cache local podem ser consultados
de forma simples como se fossem propriedades de uma chave armazenada em uma variável ou propriedade
do tipo number
. Exemplo:
const userKey = -1898186559;
userKey.istatus.iname; // => 'Ativo'
Essa funcionalidade, no entanto, não é possível de ser implementada no runtime V8, pois nele
não é permitido estender o comportamento do tipo primitivo number
. Nesse runtime, não
existe a propriedade “istatus” em valores do tipo number
e o código acima retornará
um erro ao tentar ler a propriedade “iname” do valor undefined
retornado pela
propriedade “istatus”. Para obter o valor de um campo a partir de uma chave literal, é
necessário o uso da classe DBKey
, conforme exemplo abaixo:
const userKey = -1898186559;
DBKey.from(userKey).str('iStatus.iName'); // => 'Ativo'
Naturalmente pode surgir a ideia de migrar as APIs existentes para armazenarem chaves como
DBKey
em vez number
, tornando os códigos compatíveis com os dois runtimes. No entanto, essa
estratégia é arriscada e pode quebrar os códigos existentes que precisam comparar valores de
chaves. A linguagem Javascript não permite sobrecarregar o operador ==
e a comparação de duas
instâncias de DBKey
sempre retorna false
, por especificação da linguagem, tornando obrigatório
o uso do método DBKey.equals
nesse caso.
Historicamente o sistema foi construído representando chaves como valores do tipo number
, sem um
tipo específico que distinguisse as chaves de outros valores numéricos. Os bancos de dados, e por
consequência os DataSets, também não distinguem as chaves dos demais números inteiros. A ausência
dessa distinção torna muito arriscado um esforço de revisão da manipulação de chaves a fim de
garantir que elas sempre são representadas por instâncias de DBKey
e comparadas por meio do
método equals
. Por esse motivo, optou-se por continuar utilizando o tipo number
como
representação natural das chaves e utilizar o tipo DBKey
apenas quando for necessário
realizar uma operação de leitura de campos a partir de um valor do tipo number
.
Levando em consideração o contexto acima, seguem recomendações práticas de uso da classe DBKey
:
-
Preferencialmente, utilize a classe
DBKey
apenas para fazer operações de lookups ou comparações de chaves:const statusName = DBKey.from(this.userKey).str('iStatus.iName'); const isSameUser = DBKey.equals(this.userKey, userKey);
-
Ao manipular DataSets, utilize os métodos de leitura de campo que suportam expressões lookup, evitando a criação desnecessária de instâncias de
DBKey
:const statusName = ds.str('iUser.iStatus.iName');
-
Apenas em APIs onde se requer o uso frequente da classe
DBKey
deve ser avaliada a criação de propriedades desse tipo, sempre como alternativas às existentes de representação numérica:/** * Chave do usuário. * @type {number} */ UserProfile.prototype.userKey = null; /** * Chave do usuário representada por uma instância de `DBKey`. * @type {DBKey} */ UserProfile.prototype.userDBKey = null;
-
Em funções que comparam chaves, utilize a comparação não estrita caso esteja comparando o valor recebido com uma chave numérica ou utilize o método
equals()
caso seja uma instância deDBKey
.UserProfile.prototype.checkUser = function (userKey) { if (this.userKey == userKey) { /*...*/ } // or if (this.userDBKey.equals(userKey)) { /*...*/} // or if (DBKey.equals(userKey, anotherUserKey)) { /*...*/} }
Evite criar chaves ao sincronizar dataSets
Ao sincronizar dataSets, tenha cuidado em garantir que o dataSet a ser atualizado não está
com a propriedade insertWithKey
ativa, pois o append()
irá gerar uma chave que será logo em
seguida sobreposta pela chave da origem dos dados.
Errado:
for (origem.first(); !origem.eof; origem.next()) {
if (!destino.findKey(origem.chave)) {
destino.append();
}
destino.copyRecord(origem);
destino.post();
}
Correto:
const bkpInsertWithKey = destino.insertWithKey;
destino.insertWithKey = false;
try {
for (origem.first(); !origem.eof; origem.next()) {
if (!destino.findKey(origem.chave)) {
destino.append();
}
destino.copyRecord(origem);
destino.post();
}
} finally {
destino.insertWithKey = bkpInsertWithKey;
}
Não dependa da ordem das chaves
As chaves geradas pelo sistema não seguem necessariamente uma ordem crescente. As chaves são reservadas aos Engines por meio de blocos que podem ser consumidos em momentos distintos. Portanto, ao ver que um registro tem um chave maior que a de outro, não presuma que ele foi criado depois dele, nem crie lógicas que dependam dessa ordem.
Profiler
Evite instrumentações manuais que podem ser automatizadas
O Engine pode gerar automaticamente instrumentação de todas as funções JavaScript por meio da opção “Automatic Profiler Active”, portanto a instrução manual dos métodos de uma classe, das funções e dos módulos é um trabalho manual desnecessário que é melhor realizado pelo próprio Engine.
A instrução manual do Profiler também é utilizada como uma ferramenta de observabilidade em servidores de produção, sendo muito comum a execução de servidores de produção com a opção “Profiler Enabled” ativa, mas com a opção “Automatic Profiler Active” desativada. Para que o Profiler possa ser utilizado dessa forma, somente devem ser instrumentados os códigos que sejam realmente relevantes de serem observados no ambiente de produção, onde o benefício da observação supera o custo da instrumentação do código.
O overhead do profiler é da ordem de milissegundos, sendo desprezível para operações de alto custo, como consultas ao banco de dados, escrita de arquivos, e operações de I/O de uma forma geral. No entanto, o profiler pode gerar um custo significativo de desempenho se a instrumentação manual for mal utilizada em funções de baixo custo, mas que são executadas milhares de vezes.
Mais detalhes no tópico Instrumentação do código fonte do manual Análise de desempenho.
Sempre instrumente códigos com blocos try-finally
O método startOperation
deve ser imediatamente seguido por um bloco try-finally que garanta
a execução do endOperation
. No primeiro exemplo, um erro lançado pelo código instrumentado
ou um return
antecipado impedirá a finalização da instrumentação, gerando o desalinhamento
das medições. Esse desalinhamento pode provocar estouros de memória em servidores de produção
que tenham o profiler ativo.
Errado:
profiler.startOperation('code');
// code
profiler.endOperation();
Correto:
profiler.startOperation('code');
try {
// code
} finally {
profiler.endOperation();
}
Scheduler
Trate os erros dos scripts agendados
Ao criar um script que vai ser executado pelo Scheduler é importante estar atento ao seguinte fato: se a execução do script falhar com um erro, ocorrerá uma nova tentativa de execução após alguns minutos, independentemente da periodicidade da tarefa. A configuração de agendamento definida pelo usuário ou desenvolvedor é utilizada apenas quando o script é finalizado com sucesso.
O script é reexecutado para tratar eventuais erros transientes, como falhas de rede ou a indisponibilidade temporária da base de dados. No entanto, scripts que tenham um erro de implementação podem ser executados indefinidamente em intervalos de minutos. Essas execuções podem ser prejudiciais ao servidor, caso a execução consuma muita memória ou CPU, ou pode provocar o desperdício de recursos, como as chaves do sistema, caso o script crie registros.
Por esse motivo, é recomendado que o script tenha um bloco de try/catch englobando toda a lógica do script, permitindo que um eventual erro seja tratado adequadamente. Exemplo:
try {
// Scheduled script code
} catch (e) {
handleError(e);
}
O tratamento do erro irá variar de acordo com a importância de notificar o usuário da falha de
execução. Rotinas executadas com frequência ou que não sejam críticas podem simplesmente logar
a falha nos arquivos de log do Engine utilizando a classe Logger
. Outras podem tentar
enviar um e-mail para o administrador do sistema. Implementações mais sofisticadas podem gravar
o erro em uma tabela de eventos, permitindo que os usuários consultem o status de execução
dos scripts agendados. É importante apenas que a função de tratamento de erro não gere um novo
erro, caso contrário o script continuará a ter a sua execução considerada como falha e será
reexecutado novamente.
Importante: scripts de execução única que tenham o erro capturado e silenciado serão removidos do Scheduler e jamais serão executados novamente. Portanto, o tratamento de erro para esses scripts deve considerar que as informações contidas na tarefa agendada serão perdidas caso o erro seja simplesmente silenciado.