A verdade em primeiro lugar, ou por que o sistema deve ser projetado a partir da estrutura do banco de dados

Olá, Habr!

Continuamos a explorar o tema Java e Spring, incluindo a nível de bancos de dados. Hoje, convidamos você a ler sobre por que, ao projetar grandes aplicativos, a estrutura do banco de dados, e não o código Java, deve ter um papel determinante, como isso é feito e quais são as exceções a essa regra.

Neste artigo bastante tardio, explicarei por que acredito que, em praticamente todos os casos, o modelo de dados em um aplicativo deve ser projetado "a partir do banco de dados", e não "a partir das capacidades do Java" (ou de outra linguagem de cliente com a qual você esteja trabalhando). Ao escolher a segunda abordagem, você embarca em um longo caminho de dor e sofrimento assim que seu projeto começa a crescer.

O artigo é baseado em uma pergunta, feita no Stack Overflow.

Discussões interessantes no reddit nas seções /r/java e /r/programming.

Geração de código

Fiquei bastante surpreso ao descobrir que há um pequeno grupo de usuários que, ao conhecer o jOOQ, se indignam com o fato de que, ao trabalhar com jOOQ, se confia seriamente na geração de código fonte. Ninguém o impede de usar o jOOQ da maneira que considerar necessária e ninguém o força a usar a geração de código. Mas, por padrão (como descrito no guia), trabalhar com jOOQ acontece assim: você começa com um esquema de banco de dados (herdado), realiza sua engenharia reversa usando o gerador de código jOOQ para obter um conjunto de classes representando suas tabelas e, em seguida, escreve consultas seguras para o tipo para essas tabelas:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ As informações de tipos são derivadas de
//   código gerado, ao qual se refere a condição de SELECT abaixo
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ nomes gerados
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

O código é gerado fora da compilação manualmente ou manualmente a cada compilação. Por exemplo, tal regeneração pode ocorrer logo após a migração do banco de dados Flyway, que também pode ser realizada manual ou automaticamente..

Geração de código fonte

Com essas abordagens para a geração de código – manuais e automáticas – são associadas diferentes filosofias, vantagens e desvantagens, que não pretendo discutir detalhadamente neste artigo. Mas, em geral, toda a essência do código gerado é que ele permite reproduzir em Java a "verdade" que aceitamos como um dado na nossa realidade, seja dentro do nosso sistema ou fora dele. De certa forma, a mesma coisa é feita pelos compiladores, que geram bytecode, código de máquina ou qualquer outro tipo de código com base nos códigos-fonte – obtemos uma representação da nossa "verdade" em outra linguagem, independentemente das razões específicas.

Existem muitos desses geradores de código. Por exemplo, XJC pode gerar código Java com base em arquivos XSD ou WSDL. O princípio é sempre o mesmo:

  • Há uma certa verdade (interna ou externa) – por exemplo, uma especificação, um modelo de dados, etc.
  • Precisamos de uma representação local dessa verdade em nossa linguagem de programação.

Além disso, gerar tal representação quase sempre é apropriado – para evitar redundâncias.

Provedores de tipos e processamento de anotações

Nota: outra abordagem mais moderna e específica para a geração de código para jOOQ está relacionada ao uso de provedores de tipos, na forma como são implementados em F#. Nesse caso, o código é gerado pelo compilador, na verdade, na etapa de compilação. Na forma de códigos-fonte, tal código praticamente não existe. Em Java, existem ferramentas semelhantes, embora não tão elegantes – são os processadores de anotações, por exemplo, Lombok.

De certa forma, aqui ocorrem as mesmas coisas que no primeiro caso, exceto que:

  • Você não vê o código gerado (talvez essa situação não pareça tão desagradável para alguns?).
  • Você deve garantir que os tipos possam ser fornecidos, ou seja, a "verdade" deve estar sempre acessível. Isso é fácil no caso do Lombok, que anota a "verdade". É um pouco mais complicado com os modelos de bancos de dados, cujo funcionamento depende de uma conexão ativa e constantemente disponível.

Qual é o problema com a geração de código?

Além da questão astuta sobre como é melhor iniciar a geração de código – manualmente ou automaticamente, é preciso mencionar que existem pessoas que acreditam que a geração de código não é necessária. O principal argumento que encontrei para essa opinião é que isso torna difícil configurar o pipeline de construção. Sim, é realmente complicado. Surgem custos adicionais de infraestrutura. Se você está apenas começando a trabalhar com um determinado produto (seja jOOQ, JAXB, Hibernate etc.), o tempo gasto para configurar o ambiente de trabalho é o que você gostaria de dedicar ao estudo da API em si, para depois extrair valor dela.

Se os custos relacionados a entender como funciona o gerador forem muito altos, então, de fato, houve problemas de usabilidade na API para o gerador de código (e, no futuro, descobre-se que a configuração do usuário é complicada). A facilidade de uso deve ser a prioridade máxima para qualquer API desse tipo. Mas esse é apenas um argumento contra a geração de código. No restante, é preciso escrever manualmente a representação local da verdade interna ou externa.

Muitos dirão que não têm tempo para lidar com isso tudo. Eles têm prazos apertados para o seu Super-Produto. Um dia, depois ajustaremos os pipelines de construção, teremos tempo. Eu respondo a eles:

A verdade em primeiro lugar, ou por que o sistema deve ser projetado a partir da estrutura do banco de dados
Original, Alan O’Rourke, Audience Stack

Mas no Hibernate/JPA é tão fácil escrever código "para Java".

De fato. Para o Hibernate e seus usuários, isso é ao mesmo tempo uma bênção e uma maldição. No Hibernate é possível escrever facilmente algumas entidades, assim:

	@Entity
class Book {
  @Id
  int id;
  String title;
}

E quase tudo está pronto. Agora, a tarefa do Hibernate é gerar os "detalhes" complexos de como exatamente essa entidade será definida em DDL do seu "dialeto" SQL:

	CREATE TABLE book (
  id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  title VARCHAR(50),
 
  CONSTRAINT pk_book PRIMARY KEY (id)
);
 
CREATE INDEX i_book_title ON book (title);

… e começamos a executar o aplicativo. É realmente uma ótima oportunidade para começar rapidamente e experimentar diferentes coisas.

No entanto, espere. Eu exagerei.

  • E o Hibernate realmente aplicará a definição dessa chave primária nomeada?
  • E o Hibernate criará um índice em TITLE? – eu sei que vamos precisar dele.
  • O Hibernate realmente tornará essa chave identificadora na Especificação de Identidade?

Provavelmente, não. Se você está desenvolvendo seu projeto do zero, é sempre conveniente simplesmente descartar o banco de dados antigo e gerar um novo assim que adicionar as anotações necessárias. Assim, a entidade Book acabará assim:

	@Entity
@Table(name = "book", indexes = {
  @Index(name = "i_book_title", columnList = "title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
  String title;
}

Legal. Regerar. Novamente, neste caso, será muito fácil no início.

Mas, posteriormente, você terá que pagar por isso.

Mais cedo ou mais tarde, você terá que ir para a produção. É nesse momento que esse modelo começará a falhar. Porque:

Na produção, não será mais possível descartar o banco de dados antigo, se necessário, e começar tudo do zero. Seu banco de dados se tornará legado.

De agora em diante, você terá que escrever scripts de migração DDL, por exemplo, usando Flyway.. E o que acontecerá com suas entidades nesse caso? Você poderá ou adaptá-las manualmente (e assim dobrar sua carga de trabalho) ou pedir ao Hibernate para gerá-las novamente para você (quais são as chances de que o gerado assim atenda às suas expectativas?) Você, de qualquer forma, perde.

Assim, assim que você for para a produção, precisará de hotfixes. E eles precisam ser colocados na produção muito rapidamente. Como você não se preparou e não organizou uma continuidade suave de suas migrações para a produção, você fará tudo apressadamente. E então não conseguirá fazer tudo corretamente. E você culpa o Hibernate, pois sempre há alguém a culpar, menos você...

Em vez disso, desde o início, poderia ter sido feito de forma completamente diferente. Por exemplo, colocar rodas redondas em uma bicicleta.

Primeiro o banco de dados.

A verdadeira "verdade" no esquema do seu banco de dados e a "soberania" sobre ele está dentro do banco de dados. O esquema é definido apenas no próprio banco de dados e em nenhum outro lugar, e cada cliente tem uma cópia desse esquema, portanto, é completamente razoável impor a conformidade com o esquema e sua integridade, fazendo isso diretamente no banco de dados - lá onde a informação é armazenada.
É uma sabedoria antiga e até mesmo batida. Chaves primárias e únicas são boas. Chaves estrangeiras são boas. Verificação de restrições é boa. Afirmar é bom.

Além disso, isso ainda não é tudo. Por exemplo, ao usar Oracle, você provavelmente vai querer especificar:

  • Em qual tablespace sua tabela está localizada
  • Qual é o valor de PCTFREE dela
  • Qual é o tamanho do cache na sua sequência (após o identificador)

Talvez tudo isso não importe em sistemas pequenos, mas não é necessário esperar atingir a área de "big data" - você pode começar a aproveitar os benefícios das otimizações de armazenamento oferecidas pelo fornecedor muito antes disso, como as mencionadas acima. Nenhuma das ORM que eu já vi (incluindo jOOQ) fornece acesso ao conjunto completo de opções DDL que você pode querer utilizar no seu banco de dados. As ORM oferecem algumas ferramentas que ajudam a escrever DDL.

Mas, no final das contas, um esquema bem projetado é escrito manualmente em DDL. Qualquer DDL gerado é apenas uma aproximação dele.

E quanto ao modelo do cliente?

Como mencionado anteriormente, no cliente você precisará de uma cópia do esquema do seu banco de dados, uma representação do cliente. É desnecessário mencionar que essa representação do cliente deve ser sincronizada com o modelo real. Qual a melhor maneira de conseguir isso? Usando um gerador de código.

Todos os bancos de dados fornecem suas metainformações via SQL. Veja como obter de seu banco de dados todas as tabelas em diferentes dialetos de SQL:

	-- H2, HSQLDB, MySQL, PostgreSQL, SQL Server
SELECT table_schema, table_name
FROM information_schema.tables
 
-- DB2
SELECT tabschema, tabname
FROM syscat.tables
 
-- Oracle
SELECT owner, table_name
FROM all_tables
 
-- SQLite
SELECT name
FROM sqlite_master
 
-- Teradata
SELECT databasename, tablename
FROM dbc.tables

Essas consultas (ou semelhantes, dependendo se também é necessário considerar views, views materializadas e funções com valor de tabela) também são realizadas através da chamada DatabaseMetaData.getTables() do JDBC, ou através do módulo de metadados do jOOQ.

A partir dos resultados dessas consultas, é relativamente fácil gerar qualquer representação do modelo do seu banco de dados no cliente, independentemente da tecnologia utilizada.

  • Se você usa JDBC ou Spring, pode criar um conjunto de constantes de string
  • Se usar JPA, pode gerar as entidades por conta própria
  • Se usar jOOQ, pode gerar a metamodelo do jOOQ

Dependendo do volume de recursos oferecido pela sua API cliente (como jOOQ ou JPA), o meta-modelo gerado pode ser realmente rico e completo. Vamos considerar, por exemplo, a possibilidade de junções implícitas, que apareceu no jOOQ 3.11, que se baseia nas metainformações geradas sobre as relações de chaves estrangeiras que existem entre suas tabelas.

Agora, qualquer alteração no banco de dados levará automaticamente à atualização do código cliente. Imagine, por exemplo:

ALTER TABLE book RENAME COLUMN title TO book_title;

Você realmente gostaria de fazer esse trabalho duas vezes? De maneira nenhuma. Basta registrar o DDL, executá-lo pelo seu pipeline de construção e obter a entidade atualizada:

@Entity
@Table(name = "book", indexes = {
 
  // Você já pensou sobre isso?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Ou a classe jOOQ atualizada. A maioria das mudanças no DDL também reflete na semântica, e não apenas na sintaxe. Portanto, pode ser útil olhar para o código compilado e ver qual código será (ou pode ser) afetado pela alteração do seu banco de dados.

A única verdade

Independentemente da tecnologia que você estiver usando, sempre há um único modelo que é a única fonte de verdade para algum subsistema – ou, no mínimo, devemos nos esforçar para isso e evitar aquela confusão corporativa, onde a 'verdade' está em todo lugar e em nenhum lugar ao mesmo tempo. Tudo pode ser muito mais simples. Se você apenas estiver trocando arquivos XML com algum outro sistema, basta usar XSD. Veja o meta-modelo INFORMATION_SCHEMA do jOOQ em formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD é bem compreendido
  • XSD descreve muito bem o conteúdo XML e permite validação em todas as linguagens cliente
  • XSD é bem versionado e possui uma boa compatibilidade retroativa
  • XSD pode ser traduzido para código Java usando XJC

O último ponto é importante. Ao nos comunicarmos com um sistema externo através de mensagens XML, queremos ter certeza da validade de nossas mensagens. Isso é muito fácil de alcançar com JAXB, XJC e XSD. Seria um completo absurdo esperar que, ao adotar uma abordagem de design "Java primeiro", onde criamos nossas mensagens como objetos Java, pudéssemos de alguma forma mapeá-las para XML e enviá-las para consumo em outro sistema. XML gerado dessa forma teria qualidade muito ruim, seria não documentado e difícil de evoluir. Se houvesse um acordo de nível de serviço (SLA) para tal interface, estaríamos imediatamente em apuros.

Para ser sincero, isso é exatamente o que acontece constantemente com APIs em JSON, mas isso é outra história, da próxima vez eu reclamo...

Bancos de dados: é tudo a mesma coisa

Ao trabalhar com bancos de dados, você percebe que todos eles, em essência, são semelhantes. O banco possui seus próprios dados e deve gerenciar o esquema. Quaisquer modificações feitas no esquema devem ser implementadas diretamente no DDL, para que a única fonte da verdade seja atualizada.

Quando a atualização da fonte ocorrer, todos os clientes também devem atualizar suas cópias do modelo. Alguns clientes podem ser escritos em Java utilizando jOOQ e Hibernate ou JDBC (ou todos ao mesmo tempo). Outros clientes podem ser escritos em Perl (desejo sorte a eles), e outros em C#. Isso não é importante. O modelo principal está no banco de dados. Os modelos gerados por ORM geralmente são de qualidade ruim, mal documentados e difíceis de evoluir.

Portanto, não cometa erros. Desde o início, não cometa erros. Trabalhe a partir do banco de dados. Construa um pipeline de implantação que possa ser automatizado. Inclua geradores de código para facilitar a cópia do modelo do seu banco de dados e sua implementação nos clientes. E pare de se preocupar com geradores de código. Eles são bons. Com eles, você será mais produtivo. Basta dedicar um pouco de tempo no início para configurá-los – e depois você terá anos de produtividade elevada, que se transformarão na história do seu projeto.

Não me agradeça ainda, depois.

Explicação

Para maior clareza: Este artigo de forma alguma defende que todo o sistema (ou seja, o domínio, a lógica de negócios, etc.) deve se curvar ao modelo do seu banco de dados. Aqui, estou dizendo que o código do cliente que interage com o banco de dados deve agir com base no modelo do banco de dados, de modo que não reproduza a modelo do banco de dados como sendo de 'primeira classe'. Essa lógica geralmente reside no nível de acesso a dados no cliente.

Em arquiteturas de duas camadas, que ainda existem em alguns lugares, esse modelo de sistema pode ser a única possibilidade. No entanto, na maioria dos sistemas, o nível de acesso a dados me parece uma 'sub-sistema', encapsulando o modelo do banco de dados.

Exceções

Toda regra tem suas exceções, e já mencionei que a abordagem de prioridade para o banco de dados e a geração do código-fonte pode, às vezes, ser inadequada. Aqui estão algumas dessas exceções (provavelmente existem outras):

  • Quando o esquema é desconhecido e precisa ser descoberto. Por exemplo, você é um fornecedor de ferramentas que ajuda os usuários a navegar em qualquer esquema. Uf. Aqui não tem como escapar da geração de código. Mas, ainda assim, o banco de dados deve vir em primeiro lugar.
  • Quando o esquema deve ser gerado em tempo real para resolver uma tarefa específica. Este exemplo parece uma versão um pouco extravagante do padrão entity attribute value, ou seja, você realmente não tem um esquema bem definido. Nesse caso, muitas vezes nem é possível ter certeza de que um SGBD será adequado.

Por natureza, exceções são excepcionais. Na maioria dos casos relacionados ao uso de SGBDs, o esquema é conhecido de antemão, está dentro do SGBD e é a única fonte de 'verdade', e todos os clientes têm que obter cópias derivadas dele. Idealmente, neste caso, deve-se utilizar um gerador de código.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster