Verdade primeiro, ou por que o sistema precisa ser projetado com base no dispositivo de banco de dados

Oi, Habr!

Continuamos pesquisando o tema Java и Primavera, inclusive no nível do banco de dados. Hoje convidamos você a ler sobre por que, ao projetar grandes aplicações, é a estrutura do banco de dados, e não o código Java, que deve ser de importância decisiva, como isso é feito e quais exceções existem a esta regra.

Neste artigo bastante tardio, explicarei por que acredito que, em quase todos os casos, o modelo de dados em uma aplicação deve ser projetado "a partir do banco de dados" em vez de "a partir dos recursos do Java" (ou de qualquer linguagem cliente que você esteja usando). trabalhando com). Ao adotar a segunda abordagem, você estará se preparando para um longo caminho de dor e sofrimento quando seu projeto começar a crescer.

O artigo foi escrito com base uma questão, fornecido no Stack Overflow.

Discussões interessantes no reddit em seções /r/java и /r/programação.

Geração de código

Fiquei surpreso ao ver que existe um segmento tão pequeno de usuários que, tendo se familiarizado com o jOOQ, ficam indignados com o fato de o jOOQ depender seriamente da geração de código-fonte para operar. Ninguém está impedindo você de usar o jOOQ como achar melhor ou forçando você a usar a geração de código. Mas a maneira padrão (conforme descrito no manual) de trabalhar com jOOQ é começar com um esquema de banco de dados (legado), fazer engenharia reversa usando o gerador de código jOOQ para obter um conjunto de classes que representam suas tabelas e, em seguida, escrever o tipo -consultas seguras 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 em cada montagem. Por exemplo, tal regeneração pode ocorrer imediatamente após Migração de banco de dados Flyway, que também pode ser feita manualmente ou automaticamente.

Geração de código fonte

Existem várias filosofias, vantagens e desvantagens associadas a essas abordagens de geração de código – manual e automática – que não irei discutir em detalhes neste artigo. Mas, em geral, o objetivo do código gerado é que ele nos permite reproduzir em Java aquela “verdade” que consideramos garantida, seja dentro ou fora do nosso sistema. De certa forma, é isso que os compiladores fazem quando geram bytecode, código de máquina ou alguma outra forma de código-fonte - obtemos uma representação de nossa "verdade" em outra linguagem, independentemente dos motivos específicos.

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

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

Além disso, é quase sempre aconselhável gerar tal representação para evitar redundância.

Provedores de tipo e processamento de anotações

Nota: outra abordagem mais moderna e específica para gerar código para jOOQ é usar provedores de tipos, como eles são implementados em F#. Nesse caso, o código é gerado pelo compilador, na verdade, na fase de compilação. Em princípio, tal código não existe na forma fonte. Java tem ferramentas semelhantes, embora não tão elegantes - processadores de anotação, por exemplo, Lombok.

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

  • Você não vê o código gerado (talvez esta situação pareça menos repulsiva para alguém?)
  • Você deve garantir que os tipos possam ser fornecidos, ou seja, "true" deve estar sempre disponível. Isto é fácil no caso de Lombok, que anota “verdade”. É um pouco mais complicado com modelos de banco de dados que dependem de uma conexão ativa constantemente disponível.

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

Além da complicada questão de qual a melhor forma de executar a geração de código - manual ou automaticamente, também devemos mencionar que há pessoas que acreditam que a geração de código não é necessária. A justificativa para esse ponto de vista, que encontrei com mais frequência, é que fica difícil montar um pipeline de construção. Sim, é muito difícil. Surgem custos adicionais de infraestrutura. Se você está apenas começando com um produto específico (seja jOOQ, ou JAXB, ou Hibernate, etc.), configurar um ambiente de produção leva um tempo que você preferiria gastar aprendendo a própria API para poder extrair valor dela .

Se os custos associados à compreensão da estrutura do gerador forem muito altos, então, de fato, a API fez um trabalho ruim na usabilidade do gerador de código (e mais tarde descobriu-se que a personalização do usuário nele também é complexa). A usabilidade deve ser a maior prioridade para qualquer API desse tipo. Mas este é apenas um argumento contra a geração de código. Caso contrário, é absolutamente 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 estão ficando sem prazos para seu Superproduto. Algum dia arrumaremos as esteiras de montagem, teremos tempo. Vou respondê-los:

Verdade primeiro, ou por que o sistema precisa ser projetado com base no dispositivo de banco de dados
Original, Alan O'Rourke, Pilha de Público

Mas no Hibernate/JPA é muito fácil escrever código 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, como esta:

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

E quase tudo está pronto. Agora cabe ao Hibernate gerar os "detalhes" complexos de como exatamente 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 comece a executar o aplicativo. Uma oportunidade muito legal para começar rapidamente e experimentar coisas diferentes.

No entanto, por favor, permita-me. Eu estava mentindo.

  • O Hibernate realmente imporá a definição desta chave primária nomeada?
  • O Hibernate criará um índice em TITLE? – Tenho certeza de que precisaremos disso.
  • O Hibernate fará exatamente essa identificação de chave 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 Livro acabará por 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 será muito fácil no início.

Mas você terá que pagar por isso mais tarde

Mais cedo ou mais tarde você terá que entrar em produção. É quando esse modelo vai parar de funcionar. Porque:

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

De agora em diante e para sempre você terá que escrever Scripts de migração DDL, por exemplo, usando Flyway. O que acontecerá com suas entidades neste caso? Você pode adaptá-los manualmente (e assim duplicar sua carga de trabalho) ou pode dizer ao Hibernate para regenerá-los para você (qual a probabilidade de aqueles gerados dessa forma atenderem às suas expectativas?) De qualquer forma, você perde.

Assim que entrar em produção, você precisará de hot patches. E precisam ser colocados em produção muito rapidamente. Como você não se preparou e não organizou um pipeline tranquilo de suas migrações para produção, você corrige tudo descontroladamente. E aí você não tem mais tempo para fazer tudo certo. E você critica o Hibernate, porque a culpa é sempre de outra pessoa, mas não de você...

Em vez disso, as coisas poderiam ter sido feitas de forma completamente diferente desde o início. Por exemplo, coloque rodas redondas em uma bicicleta.

Banco de dados primeiro

A verdadeira "verdade" no esquema do seu banco de dados e a "soberania" sobre ele estão dentro do banco de dados. O esquema é definido apenas no próprio banco de dados e em nenhum outro lugar, e cada cliente possui uma cópia desse esquema, portanto, faz todo o sentido garantir a conformidade com o esquema e sua integridade, para fazê-lo corretamente no banco de dados - onde as informações estão armazenado.
Esta é uma sabedoria antiga e até mesmo banal. Chaves primárias e exclusivas são boas. Chaves estrangeiras são boas. Verificar as restrições é bom. Asserções - Multar.

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

  • Em que tablespace está sua mesa?
  • Qual é o seu valor PCTFREE?
  • Qual é o tamanho do cache na sua sequência (atrás do id)

Isso pode não ser importante em sistemas pequenos, mas você não precisa esperar até passar para o domínio do big data – você pode começar a se beneficiar das otimizações de armazenamento fornecidas pelo fornecedor, como as mencionadas acima, muito mais cedo. Nenhum dos ORMs que vi (incluindo o jOOQ) fornece acesso ao conjunto completo de opções DDL que você pode querer usar em seu banco de dados. ORMs oferecem algumas ferramentas que ajudam você a escrever DDL.

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

E o modelo do cliente?

Conforme mencionado acima, no cliente você precisará de uma cópia do esquema do seu banco de dados, a visualização do cliente. Desnecessário mencionar que esta visão do cliente deve estar 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 todas as tabelas do seu banco de dados em diferentes dialetos 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 similares, dependendo se você também deve considerar visualizações, visualizações materializadas, funções com valor de tabela) também são executadas chamando DatabaseMetaData.getTables() do JDBC ou usando o metamódulo jOOQ.

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

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

Dependendo de quanta funcionalidade é oferecida pela API do seu cliente (por exemplo, jOOQ ou JPA), o metamodelo gerado pode ser realmente rico e completo. Tomemos, por exemplo, a possibilidade de junções implícitas, introduzido no jOOQ 3.11, que depende de metainformações geradas sobre os relacionamentos de chave estrangeira que existem entre suas tabelas.

Agora, qualquer incremento do banco de dados atualizará automaticamente o código do cliente. Imagine por exemplo:

ALTER TABLE book RENAME COLUMN title TO book_title;

Você realmente gostaria de fazer esse trabalho duas vezes? Em nenhum caso. Basta confirmar o DDL, executá-lo no pipeline de construção e obter 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 a classe jOOQ atualizada. A maioria das alterações de DDL também afetam a semântica, não apenas a sintaxe. Portanto, pode ser útil examinar o código compilado para ver qual código será (ou poderá) ser afetado pelo incremento do seu banco de dados.

A única verdade

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

  • XSD é bem compreendido
  • O XSD tokeniza muito bem o conteúdo XML e permite a validação em todas as linguagens do cliente
  • O XSD tem boa versão e possui compatibilidade retroativa avançada
  • XSD pode ser traduzido em código Java usando XJC

O último ponto é importante. Ao nos comunicarmos com um sistema externo usando mensagens XML, queremos ter certeza de que nossas mensagens são válidas. Isso é muito fácil de conseguir usando JAXB, XJC e XSD. Seria uma loucura pensar que, com uma abordagem de design "Java primeiro", onde transformamos nossas mensagens em objetos Java, elas poderiam de alguma forma ser mapeadas de forma coerente para XML e enviadas para outro sistema para consumo. O XML gerado dessa forma seria de qualidade muito baixa, não documentado e difícil de desenvolver. Se houvesse um acordo de nível de serviço (SLA) para tal interface, nós estragaríamos tudo imediatamente.

Honestamente, isso é o que acontece o tempo todo com APIs JSON, mas isso é outra história, discutirei na próxima vez...

Bancos de dados: são a mesma coisa

Ao trabalhar com bancos de dados, você percebe que todos eles são basicamente semelhantes. A base possui seus 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 ocorrer uma atualização de origem, todos os clientes também deverão atualizar suas cópias do modelo. Alguns clientes podem ser escritos em Java usando jOOQ e Hibernate ou JDBC (ou ambos). Outros clientes podem ser escritos em Perl (desejamos apenas boa sorte), enquanto outros podem ser escritos em C#. Não importa. O modelo principal está no banco de dados. Os modelos gerados usando ORMs são geralmente de baixa qualidade, mal documentados e difíceis de desenvolver.

Portanto, não cometa erros. Não cometa erros desde o início. Trabalhe a partir do banco de dados. Crie um pipeline de implantação que possa ser automatizado. Habilite geradores de código para facilitar a cópia do modelo de banco de dados e o despejo 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 configurando-os desde o início - e então anos de aumento de produtividade o aguardam, que farão a história do seu projeto.

Não me agradeça ainda, mais tarde.

Esclarecimento

Para ser claro: este artigo não defende de forma alguma que você precise dobrar todo o sistema (ou seja, domínio, lógica de negócios, etc., etc.) para se adequar ao seu modelo de banco de dados. O que estou dizendo neste artigo é que o código cliente que interage com o banco de dados deve atuar com base no modelo de banco de dados, para que ele próprio não reproduza o modelo de banco de dados em status de "primeira classe". Essa lógica geralmente está localizada na camada de acesso a dados do seu cliente.

Em arquiteturas de dois níveis, que ainda são preservadas em alguns lugares, tal modelo de sistema pode ser o único possível. Entretanto, na maioria dos sistemas a camada de acesso a dados me parece ser um “subsistema” que encapsula o modelo de banco de dados.

Exceções

Há exceções para todas as regras, e eu já disse que a abordagem que prioriza o banco de dados e a geração do código-fonte às vezes pode ser inadequada. Aqui estão algumas dessas exceções (provavelmente existem outras):

  • Quando o esquema é desconhecido e precisa ser descoberto. Por exemplo, você é fornecedor de uma ferramenta que ajuda os usuários a navegar em qualquer diagrama. Eca. Não há geração de código aqui. Mesmo assim, o banco de dados vem em primeiro lugar.
  • Quando um circuito deve ser gerado instantaneamente para resolver algum problema. Este exemplo parece uma versão um pouco fantasiosa do padrão valor do atributo da entidade, ou seja, você realmente não tem um esquema claramente definido. Nesse caso, muitas vezes você nem pode ter certeza de que um RDBMS será adequado para você.

As exceções são por natureza excepcionais. Na maioria dos casos que envolvem o uso de um RDBMS, o esquema é conhecido antecipadamente, reside no RDBMS e é a única fonte de “verdade”, e todos os clientes têm que adquirir cópias dele derivadas. Idealmente, você precisa usar um gerador de código.

Fonte: habr.com

Adicionar um comentário