Become a member!

Chaves primárias compostas no Delphi MVC Framework ActiveRecord

🌐
Este artigo também está disponível em outros idiomas:
🇬🇧 English · 🇮🇹 Italiano · 🇪🇸 Español · 🇩🇪 Deutsch

Chaves primárias compostas no DMVCFramework ActiveRecord com Delphi

Por anos o ActiveRecord do DMVCFramework manteve as chaves primárias propositalmente simples: uma só coluna, sem exceções. A versão 3.5 remove esse limite.

Se você já projetou um banco de dados relacional de qualquer tamanho, já topou com esta tabela:

CREATE TABLE user_roles (
  user_id INTEGER NOT NULL,
  role_id INTEGER NOT NULL,
  PRIMARY KEY (user_id, role_id)
);

Uma tabela de junção. Sua identidade não é uma coluna, são duas. E por anos o TMVCActiveRecord impôs uma regra firme: exatamente uma coluna de chave primária por entidade. Adicione foPrimaryKey a um segundo campo e ele te barrava logo na inicialização, com um erro.

A regra mantinha o ORM simples. Mas deixava de fora um tipo de tabela muito comum: user_roles(user_id, role_id), um item de pedido com chave (order_id, line_no), um registro por tenant com chave (tenant, code). Tabelas assim estão em todo lugar.

Os contornos que todo mundo conhecia

É justo dizer que sempre houve uma saída. Você podia não declarar nenhuma chave primária, mapear dois campos normais e lê-los com RQL e Where<T> como qualquer outra coisa. Você só abria mão de endereçar uma linha pela sua chave: nada de GetByPK, então toda busca passava por um filtro explícito.

Ou você adicionava um id autoincremento surrogate como chave primária, com uma restrição UNIQUE nas colunas naturais. O ActiveRecord ficava feliz, você recuperava o CRUD por chave, e o banco continuava garantindo que a chave real permanecia única. A coluna id então passava o resto da vida sem que ninguém a consultasse, existindo apenas para o ORM parar de reclamar.

Ambos resolvem. Um monte de esquemas bons rodam exatamente assim hoje. Mas nenhum dos dois deixa você dizer o óbvio: que (user_id, role_id) é a chave, e fazer o ORM tratá-la como tal.

Esse é também o motivo pelo qual esse recurso demorou tanto. Os contornos eram bons o bastante, e um contorno bom o bastante é o inimigo natural da solução de verdade: enquanto a saída de emergência funciona, ninguém coloca no topo da lista a porta que falta. Por anos um id surrogate absorveu a demanda em silêncio, e eu deixei. Desta vez me cansei de explicar o contorno, então sentei e fiz o ActiveRecord mapear a chave natural diretamente.

A partir da 3.5, você não precisa mais escolher

Aqui está a mudança inteira no seu model:

[MVCTable('user_roles')]
TUserRole = class(TMVCActiveRecord)
private
  [MVCTableField('user_id', [foPrimaryKey])]
  fUserID: Integer;
  [MVCTableField('role_id', [foPrimaryKey])]
  fRoleID: Integer;
  // ...
end;

Nenhum atributo novo para memorizar. Você marca as duas colunas com foPrimaryKey, do mesmo jeito que já marca uma. Se você sabe colocar um foPrimaryKey, já sabe declarar uma chave composta: a curva de aprendizado é um degrau de altura zero. A partir daí o ActiveRecord trata (user_id, role_id) como a identidade da linha, e todo WHERE, INSERT, UPDATE e DELETE gerado cobre a chave inteira.

E, como um único valor já não consegue apontar para uma linha, os métodos por chave que você conhece ganharam contrapartes plurais:

lRole := TMVCActiveRecord.GetByPKs<TUserRole>([1, 42]);

Essa chamada, GetByPKs, é um método de classe, e aponta para algo que vale a pena saber: é tudo um mesmo engine, e você não está preso ao estilo Active Record estático. O DMVCFramework já traz um repositório pronto sobre as mesmas entidades, IMVCRepository<T>, na unit MVCFramework.Repository. A real vantagem dele sobre os métodos de classe é que é uma interface: diferente de uma chamada estática, você pode injetá-lo diretamente nos seus controllers e serviços através do container de injeção de dependências. Você o registra uma vez e deixa o [MVCInject] entregá-lo a quem precisar:

// Registre o repositório uma vez, no .dpr, antes de o servidor iniciar
Container.RegisterType(TMVCRepository<TUserRole>, IMVCRepository<TUserRole>,
  TRegistrationType.SingletonPerRequest);

// Depois injete onde precisar, um controller ou um serviço
type
  [MVCPath('/user-roles')]
  TUserRolesController = class(TMVCController)
  private
    fRepo: IMVCRepository<TUserRole>;
  public
    [MVCInject]
    constructor Create(UserRolesRepository: IMVCRepository<TUserRole>); reintroduce;
  end;

Mesmas entidades, mesmo suporte a chaves compostas por baixo, agora acessado por uma dependência que você pode trocar em um teste, em vez de uma chamada estática que não pode. Escrever seus próprios repositórios continua perfeitamente válido, a questão é que você raramente precisa.

Esses métodos plurais de array eram a parte óbvia. A pergunta mais difícil era como sequer dar nome a uma chave agora feita de vários valores. Então, ao lado dos métodos de array, existe agora uma forma diferente de endereçar uma linha: você define cada campo da chave pelo nome da propriedade, na ordem que preferir, e depois chama Load, um método totalmente novo que lê a chave direto da entidade em vez de uma lista de argumentos. No papel parece uma mudança pequena. Por baixo é uma verdadeira mudança de paradigma, porque pela primeira vez é a entidade que possui a própria chave, em vez de recebê-la como argumento posicional.

Essa é a essência, e é também onde termina a parte fácil. Marcar as duas colunas leva um minuto. As perguntas escondidas atrás desse minuto são o motivo pelo qual o artigo completo é tão longo:

  • Os métodos posicionais de array (GetByPKs, LoadByPKs) e o Load orientado à propriedade existem os dois por um motivo: qual armadilha um deles evita, e quando você ainda deve preferir o outro?
  • O que Load e Refresh fazem quando a linha simplesmente não está lá, e por que escolhi a resposta que parece menos conveniente?
  • As colunas de uma mesma chave podem ser de tipos diferentes? E quantas delas o banco pode preencher no seu lugar?
  • Com o controller auto-CRUD, como você monta a URL para endereçar uma linha cuja chave tem duas colunas? E como chaves do tipo string ou GUID são escritas nela?
  • Qual é a única quebra de compatibilidade da release inteira, e que tipo bem específico de código precisa mesmo se preocupar com ela?

Você encontra as respostas a essas, e a algumas perguntas em que ainda não pensou, no artigo completo no Patreon.

Um superpoder que poucos desenvolvedores conhecem

Este é um bom momento para apontar um pedaço do DMVCFramework que eu considero um superpoder discreto: o TMVCActiveRecordController. Todo mundo que começa a usar não larga mais, e ainda assim a maioria dos desenvolvedores com quem converso nem sabe que ele existe.

O que ele resolve é o código mais repetitivo de qualquer API baseada em dados. Para cada tabela, você escreveria à mão o mesmo controller: um GET para a lista, um GET por id, um POST para criar, um PUT para atualizar, um DELETE, mais paginação, filtragem e ordenação, multiplicado por cada entidade do esquema. É boilerplate que você já escreveu cem vezes e vai errar num detalhe na centésima primeira, sempre naquele único endpoint que escapou da revisão de código.

O controller substitui tudo isso por uma linha:

FMVC.AddController(TMVCActiveRecordController, '/api/entities');
FMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create(CON_DEF_NAME));

A partir daí, toda entidade ActiveRecord registrada é um recurso REST. Você tem CRUD completo, consultas RQL direto da URL para filtrar, ordenar e paginar os resultados, e uma descrição Swagger gerada para você. Sem controller por entidade, sem DTO por entidade, sem rota por entidade.

A parte que o torna seguro em uma aplicação de verdade é que ele não é um repasse burro que empurra linhas para dentro e para fora do banco pelas costas do ORM. O controller conduz suas entidades pelo ciclo de vida normal, então toda a business logic que você colocou na entidade continua valendo: a validação, os hooks OnBeforeInsert e OnBeforeUpdate, os campos calculados e somente leitura, as regras de serialização. Um valor que a entidade recusa é recusado por HTTP com a mesma firmeza com que seria a partir de código Delphi. Você está expondo o seu model, não passando por cima dele.

Combine isso com o novo gerador de entidades e a conta fica quase absurda. Você aponta o gerador para um banco existente e ele escreve as classes TMVCActiveRecord para você, tabela por tabela; você registra o controller uma vez, e um esquema com milhares de tabelas vira uma API RESTful no tempo de fazer um café. O limite é claro: uma tabela que é a raiz de um agregado, com filhos ou outras relações cuja consistência precisa ser mantida (pense num cabeçalho de nota fiscal com seus itens), não deveria ser exposta linha a linha desse jeito. Ela pertence atrás do seu Aggregate Root, como argumenta o Domain-Driven Design (o famoso livro de Eric Evans), para que a raiz possa garantir os invariantes do agregado. Esses casos ainda merecem um controller dedicado, escrito à mão. Mas a maioria dos esquemas é feita sobretudo de tabelas simples e independentes, e para cada uma delas esse caminho low-code te entrega uma API funcionando e validada com esforço praticamente zero.

E é exatamente aqui que as chaves compostas precisavam conquistar seu lugar. Um controller auto-CRUD é tão geral quanto as chaves que consegue endereçar, então um recurso que parasse nas chaves de coluna única teria deixado as tabelas de junção de fora justamente da parte do framework cujo trabalho é tratar toda entidade da mesma forma.

Leia o passo a passo completo

Escrevi tudo por extenso, com o código, o raciocínio por trás de cada escolha e os testes com que ele é lançado (SQLite, Firebird e PostgreSQL, Win32 e Win64), como um artigo de aprofundamento para os assinantes do DelphiMVCFramework no Patreon.

Se você constrói backends Delphi com DMVCFramework, é ali que estão os detalhes. A assinatura também abre o resto do material premium: artigos de aprofundamento como este, vídeos, os livros mais completos e um desconto na próxima edição do guia oficial do DMVCFramework. É também o que mantém o desenvolvimento do framework em andamento.

👉 Chaves primárias compostas no DMVCFramework ActiveRecord: artigo completo no Patreon

Comments

comments powered by Disqus