Primeiro a verdade, ou por que hai que deseñar un sistema baseándose no deseño da base de datos

Ola Habr!

Seguimos investigando o tema Java и Primavera, incluso a nivel de base de datos. Hoxe invitámosvos a ler sobre por que, á hora de deseñar grandes aplicacións, é a estrutura da base de datos, e non o código Java, a que debería ter unha importancia decisiva, como se fai e que excepcións hai a esta regra.

Neste artigo bastante tardío, explicarei por que creo que en case todos os casos, o modelo de datos dunha aplicación debería deseñarse "a partir da base de datos" en lugar de "a partir das capacidades de Java" (ou calquera linguaxe do cliente que estea). traballando con). Ao tomar o segundo enfoque, estás preparando un longo camiño de dor e sufrimento unha vez que o teu proxecto comeza a crecer.

O artigo foi escrito en base a unha pregunta, dado en Stack Overflow.

Discusións interesantes sobre reddit en seccións /r/java и /r/programación.

Xeración de código

Como me sorprendeu que haxa un segmento tan pequeno de usuarios que, despois de familiarizarse con jOOQ, están indignados polo feito de que jOOQ confíe seriamente na xeración de código fonte para funcionar. Ninguén che impide usar jOOQ como creas oportuno nin te obriga a usar a xeración de código. Pero a forma predeterminada (tal e como se describe no manual) de traballar con jOOQ é que comeza cun esquema de base de datos (herdado), realice enxeñaría inversa usando o xerador de código jOOQ para obter así un conxunto de clases que representen as súas táboas e, a continuación, escriba o tipo. - Consultas seguras a estas táboas:

	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 xérase manualmente fóra da asemblea ou manualmente en cada conxunto. Por exemplo, tal rexeneración pode suceder inmediatamente despois Migración da base de datos Flyway, que tamén se pode facer manual ou automaticamente.

Xeración de código fonte

Hai varias filosofías, vantaxes e desvantaxes asociadas a estes enfoques para a xeración de código -manual e automática- que non vou comentar en detalle neste artigo. Pero, en xeral, todo o punto do código xerado é que nos permite reproducir en Java esa “verdade” que damos por descontada, ben dentro do noso sistema ou fóra del. En certo sentido, isto é o que fan os compiladores cando xeran código de bytes, código de máquina ou algunha outra forma de código fonte: obtemos unha representación da nosa "verdade" noutro idioma, independentemente das razóns específicas.

Hai moitos xeradores de código deste tipo. Por exemplo, XJC pode xerar código Java baseado en ficheiros XSD ou WSDL. O principio é sempre o mesmo:

  • Hai algunha verdade (interna ou externa) - por exemplo, unha especificación, un modelo de datos, etc.
  • Necesitamos unha representación local desta verdade na nosa linguaxe de programación.

Ademais, case sempre é recomendable xerar tal representación para evitar a redundancia.

Provedores de tipos e procesamento de anotacións

Nota: outro enfoque máis moderno e específico para xerar código para jOOQ é o uso de provedores de tipos, como están implementados en F#. Neste caso, o código é xerado polo compilador, realmente na fase de compilación. En principio, tal código non existe en forma de orixe. Java ten ferramentas similares, aínda que non tan elegantes: procesadores de anotación, por exemplo, Lombok.

En certo sentido, aquí suceden as mesmas cousas que no primeiro caso, coa excepción de:

  • Non ves o código xerado (quizais esta situación lle pareza menos repulsiva?)
  • Debe asegurarse de que se poidan proporcionar tipos, é dicir, "true" sempre debe estar dispoñible. Isto é doado no caso de Lombok, que anota "verdade". É un pouco máis complicado cos modelos de bases de datos que dependen dunha conexión en directo dispoñible constantemente.

Cal é o problema coa xeración de código?

Ademais da complicada cuestión de como executar mellor a xeración de código, de forma manual ou automática, tamén temos que mencionar que hai persoas que cren que a xeración de código non é necesaria en absoluto. A xustificación deste punto de vista, que atopei con máis frecuencia, é que entón é difícil configurar unha canalización de construción. Si, é moi difícil. Xorden custos adicionais de infraestrutura. Se estás comezando cun produto en particular (xa sexa jOOQ, JAXB, ou Hibernate, etc.), a configuración dun ambiente de produción leva un tempo que preferirías dedicar a aprender a propia API para poder extraerlle valor. .

Se os custos asociados á comprensión da estrutura do xerador son demasiado altos, entón, de feito, a API fixo un mal traballo na usabilidade do xerador de código (e máis tarde resulta que a personalización do usuario tamén é difícil). A usabilidade debe ser a máxima prioridade para calquera API deste tipo. Pero este é só un argumento contra a xeración de código. En caso contrario, escribir unha representación local da verdade interna ou externa é absolutamente manual.

Moitos dirán que non teñen tempo para facer todo isto. Están esgotando os prazos para o seu Superproduto. Algún día adecentaremos as cintas de montaxe, teremos tempo. Vou responderlles:

Primeiro a verdade, ou por que hai que deseñar un sistema baseándose no deseño da base de datos
Orixinal, Alan O'Rourke, Audience Stack

Pero en Hibernate/JPA é moi sinxelo escribir código Java.

De verdade. Para Hibernate e os seus usuarios, isto é tanto unha bendición como unha maldición. En Hibernate podes simplemente escribir un par de entidades, como esta:

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

E case todo está listo. Agora tócalle a Hibernate xerar os complexos "detalles" de como se definirá exactamente esta entidade no DDL do teu "dialecto" 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 comeza a executar a aplicación. Unha oportunidade moi xenial para comezar rapidamente e probar cousas diferentes.

Non obstante, permíteme. Estaba mentindo.

  • Hibernate aplicará realmente a definición desta chave primaria denominada?
  • Hibernate creará un índice en TITLE? – Sei con certeza que o necesitaremos.
  • Hibernate fará exactamente que esta chave se identifique na especificación de identidade?

Probablemente non. Se está a desenvolver o seu proxecto desde cero, sempre é conveniente simplemente descartar a base de datos antiga e xerar unha nova en canto engada as anotacións necesarias. Así, a entidade Libro adoptará finalmente a forma:

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

Genial. Rexenerar. De novo, neste caso será moi sinxelo ao comezo.

Pero terás que pagalo máis tarde

Tarde ou cedo terás que entrar en produción. É entón cando este modelo deixará de funcionar. Porque:

En produción, xa non será posible, se é necesario, descartar a antiga base de datos e comezar de cero. A túa base de datos converterase en herdanza.

A partir de agora e para sempre terás que escribir Scripts de migración DDL, por exemplo, usando Flyway. Que pasará coas túas entidades neste caso? Podes adaptalos manualmente (e, polo tanto, duplicar a túa carga de traballo), ou ben podes dicirlle a Hibernate que os rexenere por ti (que probabilidade hai de que os xerados deste xeito cumpran as túas expectativas?) De calquera xeito, perderás.

Entón, unha vez que entres en produción, necesitarás parches quentes. E hai que poñerse en produción moi rapidamente. Dado que non se preparou nin organizou un fluxo fluido das súas migracións para a produción, parcheas todo de forma salvaxe. E entón xa non tes tempo para facelo todo correctamente. E criticas a Hibernate, porque sempre é culpa doutra persoa, pero ti non...

Pola contra, as cousas poderían terse feito de forma completamente diferente dende o principio. Por exemplo, poñer rodas redondas nunha bicicleta.

Base de datos primeiro

A verdadeira "verdade" no seu esquema de base de datos e a "soberanía" sobre el reside dentro da base de datos. O esquema defínese só na propia base de datos e en ningún outro lugar, e cada cliente ten unha copia deste esquema, polo que ten todo o sentido facer cumprir o esquema e a súa integridade, para facelo ben na base de datos, onde está a información. almacenado.
Esta é unha vella sabedoría, incluso manida. As claves primarias e únicas son boas. As claves estranxeiras son boas. Comprobar as restricións é bo. Afirmacións - Ben.

Ademais, iso non é todo. Por exemplo, usando Oracle, probablemente quererá especificar:

  • En que espazo está a túa mesa?
  • Cal é o seu valor PCTFREE?
  • Cal é o tamaño da caché na súa secuencia (detrás do id)

Quizais isto non sexa importante en sistemas pequenos, pero non tes que esperar ata que te mudes ao ámbito dos grandes datos; podes comezar a beneficiarte das optimizacións de almacenamento proporcionadas polo provedor como as mencionadas anteriormente moito antes. Ningún dos ORM que vin (incluído jOOQ) proporciona acceso ao conxunto completo de opcións DDL que pode querer usar na súa base de datos. Os ORM ofrecen algunhas ferramentas que che axudan a escribir DDL.

Pero ao final do día, un circuíto ben deseñado está escrito a man en DDL. Calquera DDL xerado é só unha aproximación do mesmo.

E o modelo de cliente?

Como se mencionou anteriormente, no cliente necesitará unha copia do esquema da súa base de datos, a vista do cliente. Non fai falta mencionar que esta vista do cliente debe estar sincronizada co modelo real. Cal é a mellor forma de conseguilo? Usando un xerador de código.

Todas as bases de datos proporcionan a súa meta información a través de SQL. Aquí tes como obter todas as táboas da túa base de datos en diferentes dialectos 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

Estas consultas (ou outras similares, dependendo de se tamén hai que considerar vistas, vistas materializadas, funcións con valores de táboa) tamén se executan chamando DatabaseMetaData.getTables() desde JDBC ou usando o metamódulo jOOQ.

A partir dos resultados de tales consultas, é relativamente sinxelo xerar calquera representación do cliente do seu modelo de base de datos, independentemente da tecnoloxía que use no cliente.

  • Se está a usar JDBC ou Spring, pode crear un conxunto de constantes de cadea
  • Se usas JPA, podes xerar as propias entidades
  • Se usas jOOQ, podes xerar o metamodelo jOOQ

Dependendo da cantidade de funcionalidades que ofreza a API do teu cliente (por exemplo, jOOQ ou JPA), o metamodelo xerado pode ser realmente rico e completo. Tomemos, por exemplo, a posibilidade de unións implícitas, introducido en JOOQ 3.11, que depende da metainformación xerada sobre as relacións de chave estranxeira que existen entre as túas táboas.

Agora calquera incremento da base de datos actualizará automaticamente o código do cliente. Imaxina por exemplo:

ALTER TABLE book RENAME COLUMN title TO book_title;

De verdade quererías facer este traballo dúas veces? En ningún caso. Só ten que confirmar o DDL, executalo a través da súa canalización de compilación e obter a entidade actualizada:

@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 clase jOOQ actualizada. A maioría dos cambios de DDL tamén afectan á semántica, non só á sintaxe. Polo tanto, pode ser útil buscar no código compilado para ver que código se verá (ou podería) afectar polo incremento da súa base de datos.

A única verdade

Independentemente da tecnoloxía que uses, sempre hai un modelo que é a única fonte de verdade para algún subsistema ou, como mínimo, deberíamos esforzarnos por iso e evitar esa confusión empresarial, onde a "verdade" está en todas partes e en ningures á vez. . Todo podería ser moito máis sinxelo. Se só estás intercambiando ficheiros XML con outro sistema, usa XSD. Mire o metamodelo INFORMATION_SCHEMA de jOOQ en formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD enténdese ben
  • XSD tokeniza moi ben o contido XML e permite a validación en todos os idiomas do cliente
  • XSD está ben versionado e ten compatibilidade avanzada con versións anteriores
  • XSD pódese traducir a código Java usando XJC

O último punto é importante. Cando nos comunicamos cun sistema externo mediante mensaxes XML, queremos estar seguros de que as nosas mensaxes son válidas. Isto é moi sinxelo de conseguir usando JAXB, XJC e XSD. Sería unha tolemia pensar que, cun enfoque de deseño "Java first" onde facemos as nosas mensaxes como obxectos Java, poderían ser mapeadas coherentemente a XML e enviadas a outro sistema para o seu consumo. O XML xerado deste xeito sería de moi mala calidade, non documentado e difícil de desenvolver. Se houbese un acordo de nivel de servizo (SLA) para tal interface, arruinaríamos inmediatamente.

Sinceramente, isto é o que pasa todo o tempo coas API JSON, pero esa é outra historia, peleareime a próxima vez...

Bases de datos: son o mesmo

Cando se traballa con bases de datos, dáse conta de que todas son basicamente similares. A base é propietaria dos seus datos e debe xestionar o esquema. Calquera modificación realizada no esquema debe ser implementada directamente no DDL para que se actualice a fonte única de verdade.

Cando se produce unha actualización de orixe, todos os clientes tamén deben actualizar as súas copias do modelo. Algúns clientes poden escribirse en Java usando jOOQ e Hibernate ou JDBC (ou ambos). Outros clientes poden escribirse en Perl (só lles desexamos moita sorte), mentres que outros poden escribirse en C#. Non importa. O modelo principal está na base de datos. Os modelos xerados mediante ORM adoitan ser de mala calidade, mal documentados e difíciles de desenvolver.

Así que non cometas erros. Non cometas erros desde o principio. Traballar dende a base de datos. Constrúe unha canalización de implantación que se poida automatizar. Activa os xeradores de código para facilitar a copia do teu modelo de base de datos e descargalo nos clientes. E deixa de preocuparte polos xeradores de código. Son bos. Con eles serás máis produtivo. Só tes que dedicar un pouco de tempo a configuralos desde o principio, e despois agardan por ti anos de aumento da produtividade, que formarán a historia do teu proxecto.

Non mo agradeces aínda, máis tarde.

Esclarecemento

Para ser claro: este artigo non defende de ningún xeito que teñas que dobrar todo o sistema (é dicir, o dominio, a lóxica empresarial, etc., etc.) para que se axuste ao teu modelo de base de datos. O que estou dicindo neste artigo é que o código de cliente que interactúa coa base de datos debe actuar en base ao modelo de base de datos, para que el mesmo non reproduza o modelo de base de datos nun estado de "primeira clase". Esta lóxica adoita estar situada na capa de acceso a datos do teu cliente.

En arquitecturas de dous niveis, que aínda se conservan nalgúns lugares, tal modelo de sistema pode ser o único posible. Non obstante, na maioría dos sistemas a capa de acceso a datos paréceme un "subsistema" que encapsula o modelo de base de datos.

Excepcións

Hai excepcións a cada regra, e xa dixen que o enfoque de xeración de código fonte e base de datos en primeiro lugar ás veces pode ser inadecuado. Aquí tes un par de tales excepcións (probablemente haxa outras):

  • Cando o esquema é descoñecido e hai que descubrir. Por exemplo, é un provedor dunha ferramenta que axuda aos usuarios a navegar por calquera diagrama. Uf. Non hai ningunha xeración de código aquí. Pero aínda así, a base de datos é o primeiro.
  • Cando se debe xerar un circuíto sobre a marcha para resolver algún problema. Este exemplo parece unha versión lixeiramente fantasiosa do patrón valor do atributo da entidade, é dicir, realmente non tes un esquema claramente definido. Neste caso, moitas veces nin sequera podes estar seguro de que un RDBMS che conveña.

As excepcións son por natureza excepcionais. Na maioría dos casos que implica o uso dun RDBMS, o esquema coñécese de antemán, reside dentro do RDBMS e é a única fonte de "verdade", e todos os clientes teñen que adquirir copias derivadas del. O ideal é utilizar un xerador de código.

Fonte: www.habr.com

Engadir un comentario