artigo

modelo de dados no ServiceNow: a decisão que você paga pelo resto do projeto

Por que estender tabelas nativas, escolher o tipo de campo certo e evitar dot-walking descontrolado define se sua implementação envelhece bem.

6 min de leitura intermediário servicenow arquitetura modelo-de-dados

#o prédio novo do lado do prédio antigo

Imagina que você precisa de um escritório novo. De um lado da rua tem um prédio antigo, já com elevador funcionando, portaria 24h, gerador, internet cabeada e síndico que resolve qualquer chamado em uma hora. Do outro lado tem um terreno vazio.

Construir do zero no terreno vazio parece mais "seu", mais sob controle. Só que agora você precisa instalar elevador, contratar portaria, puxar internet e virar síndico também. Nada disso é impossível — é só trabalho que o prédio antigo já tinha resolvido.

No ServiceNow, esse prédio antigo se chama tabela nativa. E a decisão mais cara que você toma num projeto normalmente não é qual API usar, é se você vai morar no prédio que já existe ou construir um do zero sem perceber o tamanho da conta.

#herança sem cerimônia

O ServiceNow guarda registros de tabelas filhas e tabelas pai na mesma tabela física do banco, distinguindo cada linha por um campo interno (sys_class_name). Isso é Single Table Inheritance, e na prática significa uma coisa simples: quando você estende task, você não herda só os campos — herda Business Rules, SLAs, fluxos de aprovação, relatórios prontos e até o jeito que o registro se comporta no portal do usuário.

É diferente de herança de código, onde às vezes você herda algo que não queria e precisa sobrescrever. Aqui, o ganho é quase todo a favor: incident, change_request e sc_request são só task com uma roupa específica — campos de categoria, tipo de mudança ou dados de entrega por cima da mesma fundação de número, estado, prioridade e responsável.

O erro clássico é não confiar nisso. Alguém cria u_solicitacao_ferramenta do zero porque "é mais simples controlar os campos", e seis meses depois está reescrevendo aprovação, SLA, notificação e portal — tudo que sc_request já fazia de graça.

#quando o terreno vazio é a escolha certa

Estender por estender também é armadilha. Forçar uma entidade sem ciclo de vida — tipo uma tabela de "categorias de equipamento" — para dentro de task é tão ruim quanto duplicar sc_request. Categoria não tem estado, não tem responsável, não precisa de SLA.

perguntase a resposta for sim
é só uma lista de referência, sem fluxo de trabalho?tabela nova simples, sem estender nada
tem estados próprios que não cabem no vocabulário de task?tabela nova, ciclo de vida customizado
é um vínculo entre dois registros com atributos próprios?tabela de relacionamento explícita (mais sobre isso já já)
tem número, responsável, prazo e aprovação?quase certamente extensão de task ou sc_request

Toda tabela nova, mesmo a mais simples, merece um kit mínimo além dos campos técnicos que a plataforma já dá de graça (sys_id, sys_created_by, sys_created_on...): um número formatado e visível, uma descrição curta, um estado, e se fizer sentido, responsável e grupo. Sem isso, sua tabela "simples" vira uma lista de sys_ids que ninguém consegue ler em uma reunião.

#o campo errado é uma dívida que só aparece depois

Escolher o tipo de campo pela conveniência do momento é assinar uma dívida com juros. Ela não cobra nada no primeiro mês — cobra quando a tabela já tem 300 mil registros e alguém precisa migrar o dado para o tipo certo.

o dado é...usenão use
um valor fixo entre poucas opçõesChoice, com valor interno estável (numérico ou um slug tipo high)comparar ou gravar pelo label em script — o texto de exibição muda, o valor interno não deveria
ligação com outro registroReferenceString guardando o sys_id na mão — mata o dot-walking e a integridade referencial
sim/nãoBooleanChoice "Sim/Não" em String — você vai ter "sim", "Sim" e "SIM" convivendo
valor financeiroCurrency ou DecimalFloat — ponto flutuante não fecha centavo, e cliente financeiro percebe
nota, histórico, observaçãoJournal FieldString com texto concatenado manualmente — perde autoria e timestamp
dado sensívelcampo criptografado com contexto de encriptaçãoString — aparece em export e log sem controle nenhum

O padrão por trás de toda linha dessa tabela é o mesmo: o tipo certo carrega significado que o banco entende e a plataforma sabe validar. O tipo errado empurra esse significado para dentro de scripts espalhados, que vão divergir entre si com o tempo.

#dot-walking: ótimo até a nona iteração

Dot-walking — encadear campos de referência tipo account.owner.timezone — é provavelmente a feature mais usada sem que ninguém perceba que está usando. Ela é ótima em formulário, porque resolve em uma linha o que seria uma busca manual.

O problema aparece em script processando lista. Cada nível de dot-walk pode custar uma query ao banco, por registro. Um relatório noturno que processa 2 mil contratos e faz dot-walk de dois níveis pode gerar 4 mil queries que ninguém pediu.

javascript
// ❌ dot-walking dentro do loop: multiplica queries pelo total de registros
var gr = new GlideRecord("x_acme_contract");
gr.addQuery("active", true);
gr.query();
while (gr.next()) {
    var fuso = gr.account.owner.timezone.getDisplayValue(); // 2 queries por volta
    gs.info(gr.number + " -> " + fuso);
}
javascript
// �
 pré-carrega os donos de conta em um Map antes do loop principal
var gr = new GlideRecord("x_acme_contract");
gr.addQuery("active", true);
gr.query();

var contas = [], linhas = [];
while (gr.next()) {
    var accId = gr.getValue("account");
    if (accId && contas.indexOf(accId) === -1) contas.push(accId);
    linhas.push({ numero: gr.number.toString(), conta: accId });
}

var fusoPorConta = {};
if (contas.length > 0) {
    var ga = new GlideRecord("x_acme_account");
    ga.addQuery("sys_id", "IN", contas.join(","));
    ga.query();
    while (ga.next()) {
        fusoPorConta[ga.getUniqueValue()] = ga.owner.timezone.getDisplayValue();
    }
}

linhas.forEach(function(linha) {
    gs.info(linha.numero + " -> " + (fusoPorConta[linha.conta] || "sem fuso"));
});

Em formulário individual, um ou dois níveis de dot-walk são inofensivos — é uma consulta a mais, em um contexto onde o usuário já espera o carregamento do form. Em script de lista, ACL avaliada por registro ou Business Rule em massa, cada nível vira um multiplicador silencioso.

#a tentação da vírgula

Toda modelagem real tem relacionamento muitos-para-muitos: um contrato cobre vários serviços, um serviço aparece em vários contratos. E toda vez que esse caso aparece, alguém propõe a solução mais rápida: um campo String guardando os sys_ids separados por vírgula.

Funciona na demo. Quebra quando alguém pergunta "quais contratos cobrem o serviço de e-mail?" — porque essa pergunta exige ler e interpretar a string inteira de cada registro, sem índice, sem query eficiente, sem jeito de adicionar um atributo ao vínculo (tipo "esse serviço é prioritário neste contrato específico").

javascript
// tabela de relacionamento explícita: x_acme_contract_service
// campos: contract (Reference), service (Reference), priority (Choice)

var gr = new GlideRecord("x_acme_contract_service");
gr.addQuery("service", servicoSysId);
gr.query();
while (gr.next()) {
    gs.info("Contrato: " + gr.contract.getDisplayValue());
}

Uma tabela de relacionamento não é luxo arquitetural. É a diferença entre uma pergunta que o banco responde em milissegundos e uma pergunta que exige varrer cada registro na força bruta.

#e agora?

  • Na sua instância de dev, procure algum campo String armazenando valores separados por vírgula ou um sys_id "colado à mão" — são os dois sintomas mais fáceis de encontrar e mais caros de deixar para depois.
  • Antes de criar a próxima tabela custom, passe cinco minutos procurando se já existe uma tabela nativa parecida — a busca no dicionário de tabelas custa bem menos que migrar dados depois.
  • Se você ainda não leu, o artigo sobre precedência de campos complementa este aqui: modelo de dados certo e camada certa de validação costumam andar juntos.
LinkedIn X/Twitter WhatsApp ☕ me paga um café