A verdade, antes de tudo, ou por que o sistema precisa ser projetado com base no dispositivo de banco de dados

Olá Habr!

Continuamos a explorar o tópico Java e Spring , inclusive no nível do banco de dados. Hoje, sugerimos a leitura sobre por que, ao projetar aplicativos grandes, é a estrutura do banco de dados, e não o código Java, que deve ter um significado decisivo sobre como isso é feito e quais são as exceções a essa regra.

Neste artigo bastante tardio, explicarei por que acredito que em quase todos os casos o modelo de dados no aplicativo deve ser projetado "com base no banco de dados" e não "com base nos recursos do Java" (ou em outra linguagem cliente com a qual você trabalha). Escolhendo a segunda abordagem, você embarca em uma longa jornada de dor e sofrimento assim que seu projeto começa a crescer.

Este artigo é baseado em uma pergunta feita no Stack Overflow.

Discussões interessantes sobre o reddit nas seções / r / java e / r / programação .

Geração de código


Estou surpreso por haver uma camada tão pequena de usuários que, depois de se familiarizar com o jOOQ, ficam indignados com o fato de que, ao trabalhar com o jOOQ, depende seriamente da geração do código-fonte. Ninguém te incomoda em usar o jOOQ como achar melhor e não o força a usar a geração de código. Porém, por padrão (como descrito no manual), trabalhar com o jOOQ acontece da seguinte maneira: você começa com o esquema do banco de dados (herdado), realiza a engenharia reversa com o gerador de código do jOOQ, para obter um conjunto de classes representando suas tabelas e, em seguida, escreva consultas de tipo seguro para estas tabelas:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^      
//     ,    
//   SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^  
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

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

Geração de código fonte


Várias filosofias, vantagens e desvantagens estão associadas a essas abordagens à geração de código - manual e automática - que não vou discutir em detalhes neste artigo. Mas, em geral, o ponto principal do código gerado é que ele nos permite reproduzir em Java a "verdade" que damos como certa, tanto dentro do nosso sistema quanto fora dele. Em certo sentido, os compiladores que geram código de código, máquina ou algum outro tipo de código baseado na fonte fazem a mesma coisa - obtemos uma representação da nossa "verdade" em outro idioma, independentemente de razões específicas.

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

  • Existe alguma verdade (interna ou externa) - por exemplo, especificação, modelo de dados, etc.
  • Precisamos de uma representação local dessa verdade em nossa linguagem de programação.

Além disso, a geração dessa representação é quase sempre aconselhável - para evitar redundância.

Provedores de Tipo e Processamento de Anotações


Nota: outra abordagem mais moderna e específica à geração de código para o jOOQ está associada ao uso de provedores de tipo, na forma em que eles são implementados em F # . Nesse caso, o código é gerado pelo compilador, na verdade no estágio de compilação. Na forma de fontes, esse código, em princípio, não existe. Existem ferramentas semelhantes, embora menos elegantes, em Java - são processadores de anotação como o Lombok .

Em certo sentido, as mesmas coisas acontecem aqui como no primeiro caso, com exceção de:

  • Você não vê o código gerado (talvez essa situação pareça a alguém não tão repulsivo?)
  • , , , «» . Lombok, “”. , .

?


Além da questão complicada de como é melhor iniciar a geração de código - manual ou automaticamente, é necessário mencionar que existem pessoas que acreditam que a geração de código não é necessária. A justificativa para esse ponto de vista, com o qual me deparei com mais frequência, é que fica difícil configurar o pipeline de montagem. Sim, muito difícil. Existem custos de infraestrutura adicionais. Se você está apenas começando a trabalhar com um determinado produto (seja jOOQ, JAXB, Hibernate etc.), leva tempo para configurar o ambiente de trabalho que você gostaria de gastar aprendendo a API e, em seguida, extrair valor dela.

Se a sobrecarga associada à compreensão do dispositivo do gerador for muito grande, a API realmente trabalhou um pouco na usabilidade do gerador de código (e, no futuro, a configuração do usuário é complicada). A facilidade de uso deve ser a maior prioridade para qualquer API. Mas este é apenas um argumento contra a geração de código. De resto, é completamente manual escrever uma representação local da verdade interna ou externa.

Muitos dirão que não têm tempo para fazer tudo isso. Eles têm prazos para seu super produto. Algum tempo depois, pentearemos os transportadores de montagem, chegará a tempo. Eu vou respondê-las:


Original , Alan O'Rourke, Audience Stack

Mas no Hibernate / JPA, é tão simples escrever código "para Java".

Realmente. Para o Hibernate e seus usuários, isso é uma bênção e uma maldição. No Hibernate, você pode simplesmente escrever algumas entidades, assim:

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

E quase tudo está pronto. Agora, o destino do Hibernate é gerar "detalhes" complexos de exatamente como essa entidade será definida no 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 direcionar o aplicativo. Uma oportunidade muito legal de começar rapidamente e experimentar coisas diferentes.

No entanto, permita. Eu estava enganando.

  • O Hibernate realmente aplica a definição dessa chave primária nomeada?
  • O Hibernate criará um índice em TITLE? "Tenho certeza de que precisaremos dele."
  • O Hibernate exatamente identifica essa chave na Especificação de identidade?

Provavelmente não. Se você estiver desenvolvendo seu projeto do zero, é sempre conveniente simplesmente excluir o banco de dados antigo e gerar um novo assim que você adicionar as anotações necessárias. Portanto, a entidade Livro eventualmente assumirá a forma:

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

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

Mas então você tem que pagar por isso


Cedo ou tarde, você precisa entrar em produção. Nesse momento, esse modelo deixará de funcionar. Porque:

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

A partir de agora, você precisará escrever scripts de migração DDL, por exemplo, usando o Flyway . E o que acontece então com suas entidades? Você pode adaptá-los manualmente (e, assim, dobrar sua carga de trabalho) ou pode pedir ao Hibernate para regenerá-los para você (quão grandes são as chances de que o gerado dessa maneira atenda às suas expectativas?) Você perde assim mesmo.

Assim, assim que você entrar em produção, precisará de patches quentes. E eles precisam ser colocados em produção muito rapidamente. Como você não preparou e organizou uma transmissão suave de suas migrações para produção, você está corrigindo tudo de maneira descontrolada. E então você não tem tempo para fazer tudo certo. E repreenda o Hibernate, porque qualquer um é sempre o culpado, mas não você ...

Em vez disso, desde o início, tudo poderia ser feito de uma maneira completamente diferente. Por exemplo, coloque rodas redondas em uma bicicleta.

Primeiro banco de dados


A verdadeira "verdade" no esquema do seu banco de dados e a "soberania" sobre ele estão dentro do banco de dados. Um esquema é definido apenas no próprio banco de dados e em nenhum outro lugar, e cada cliente possui uma cópia desse esquema; portanto, é totalmente aconselhável impor a conformidade com o esquema e sua integridade, para fazê-lo diretamente no banco de dados - onde as informações são armazenadas.
Esta é uma velha sabedoria desgastada mesmo. Chaves primárias e exclusivas são boas. Chaves estrangeiras são boas. Verificar restrições é bom. Declarações são boas.

Além disso, isso não é tudo. Por exemplo, usando o Oracle, você provavelmente deseja especificar:

  • Em que espaço de tabela está sua tabela?
  • Qual é o seu valor PCTFREE?
  • Qual é o tamanho do cache na sua sequência (atrás do identificador)

Talvez tudo isso não seja importante em pequenos sistemas, mas não é necessário aguardar a transição para a área de "big data" - é possível e muito mais cedo começar a se beneficiar da otimização do armazenamento de dados fornecido pelo fornecedor, como os mencionados acima. Nenhum dos ORMs que eu vi (incluindo o jOOQ) fornece acesso ao conjunto completo de opções de DDL que você pode querer usar no seu banco de dados. Os ORMs oferecem algumas ferramentas que ajudam a escrever DDL.

Mas, no final, um circuito bem projetado é escrito manualmente em DDL. Qualquer DDL gerada é apenas uma aproximação dela.

E o modelo do cliente?


Como mencionado acima, no cliente você precisará de uma cópia do esquema do banco de dados, a visualização do cliente. Escusado será dizer que essa visã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 meta informações através do SQL. Veja como obter todas as tabelas em diferentes dialetos do SQL do seu banco de dados:

	-- 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 similares, dependendo se você também precisa considerar representações, representações materializadas, funções com um valor de tabela) também são executadas usando uma chamada DatabaseMetaData.getTables()do JDBC ou usando o metódulo jOOQ.

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

  • Se você usar JDBC ou Spring, poderá criar um conjunto de constantes de sequência
  • Se estiver usando o JPA, você poderá gerar as próprias entidades
  • Se você estiver usando o jOOQ, poderá gerar o metamodelo jOOQ

Dependendo de quantos recursos são oferecidos pela API do cliente (por exemplo, jOOQ ou JPA), o metamodelo gerado pode ser verdadeiramente rico e completo. Tomemos, por exemplo , a possibilidade de junções implícitas que apareceram no jOOQ 3.11 , que se baseia nas meta-informações geradas sobre os relacionamentos de chaves estrangeiras entre suas tabelas.

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

ALTER TABLE book RENAME COLUMN title TO book_title;

Você realmente gostaria de fazer este trabalho duas vezes? Em nenhum caso. Apenas conserte o DDL, execute-o no seu pipeline de montagem e obtenha a entidade atualizada:

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

Ou uma classe jOOQ atualizada. A maioria das alterações de DDL também afeta a semântica, não apenas a sintaxe. Portanto, pode ser conveniente ver no código compilado qual código será (ou pode ser) afetado pelo incremento do seu banco de dados.

A única verdade


Independentemente da tecnologia usada, sempre existe um modelo que é a única fonte de verdade para algum subsistema - ou, pelo menos, devemos nos esforçar para evitar essa confusão empresarial, onde a “verdade” está em todo lugar e em lugar nenhum. Tudo pode ser muito mais simples. Se você estiver apenas trocando arquivos XML com outro sistema, use o XSD. Veja o metamodelo INFORMATION_SCHEMA do jOOQ no formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD é bem entendido
  • XSD XML
  • XSD
  • XSD Java XJC

O último ponto é importante. Ao se comunicar com um sistema externo usando mensagens XML, queremos ter certeza da validade de nossas mensagens. Isso é muito fácil de conseguir com JAXB, XJC e XSD. Seria completamente insano esperar que, ao abordar o design do “Java primeiro”, onde fazemos nossas mensagens na forma de objetos Java, elas possam, de alguma forma, ser claramente exibidas em XML e enviadas para outro sistema para consumo. O XML gerado dessa maneira seria de muito baixa qualidade, não documentado e seria difícil de desenvolver. Se houvesse um acordo sobre o nível de qualidade de serviço (SLA) em tal interface, nós o arruinaríamos imediatamente.

Honestamente, é exatamente isso que acontece o tempo todo, da API ao JSON, mas essa é outra história, eu juro na próxima vez ...

Bancos de dados: é a mesma coisa


Ao trabalhar com bancos de dados, você entende que eles são, em princípio, semelhantes. A base possui seus dados e deve gerenciar o esquema. Quaisquer modificações feitas no circuito devem ser implementadas diretamente no DDL para atualizar uma única fonte de verdade.

Quando uma atualização de origem ocorre, todos os clientes também devem atualizar suas cópias do modelo. Alguns clientes podem ser gravados em Java usando jOOQ e Hibernate ou JDBC (ou todos de uma vez). Outros clientes podem ser escritos em Perl (resta desejar-lhes boa sorte) e outros em C #. Isso não importa. O modelo principal está no banco de dados. Os modelos gerados usando ORMs, geralmente de baixa qualidade, são pouco documentados e difíceis de desenvolver.

Portanto, não cometa erros. Não cometa erros desde o início. Trabalhar a partir de um banco de dados. Crie um pipeline de implantação que possa ser automatizado. Ligue os geradores de código para facilitar a cópia do modelo de banco de dados e despejá-lo nos clientes. E pare de se preocupar com geradores de código. Eles são bons. Com eles você se tornará mais produtivo. Você só precisa gastar um pouco de tempo desde o início para configurá-los - e então terá anos de produtividade aumentada que formarão a história do seu projeto.

Até lá, obrigado.

Explicação


Para maior clareza: este artigo não recomenda que, de acordo com o modelo do seu banco de dados, você precise dobrar todo o sistema (por exemplo, área de assunto, lógica de negócios etc.). Neste artigo, digo que o código do cliente que interage com o banco de dados deve atuar com base no modelo de banco de dados, para que não reproduza o modelo de banco de dados no status "primeira classe". Essa lógica geralmente está localizada no nível de acesso a dados no seu cliente.

Em arquiteturas de duas camadas, que ainda são preservadas em alguns lugares, esse modelo de sistema pode ser o único possível. No entanto, na maioria dos sistemas, o nível de acesso a dados parece-me um "subsistema" que encapsula um modelo de banco de dados.

Exceções


Há exceções a qualquer regra, e eu já disse que uma abordagem com primazia de banco de dados e geração de código-fonte pode às vezes ser inadequada. Aqui estão algumas exceções (provavelmente existem outras):

  • Quando o circuito é desconhecido e deve ser aberto. Por exemplo, você é um provedor de uma ferramenta para ajudar os usuários a navegar em qualquer esquema. Ufa Não há geração de código. Mas ainda assim - o banco de dados está acima de tudo.
  • Quando um circuito deve ser gerado em tempo real para resolver um determinado problema. Este exemplo parece uma versão um pouco fantasiosa do padrão de valor do atributo da entidade , ou seja, você realmente não possui um esquema bem definido. Nesse caso, muitas vezes é impossível ter certeza de que um RDBMS é ideal para você.

Exceções são inerentemente excepcionais. Na maioria dos casos que envolvem o uso de um RDBMS, o esquema é conhecido antecipadamente, está localizado dentro do RDBMS e é a única fonte de "verdade", e todos os clientes precisam adquirir cópias dele derivadas. Idealmente, você precisa usar um gerador de código.

All Articles