La verdad en primer lugar, o por qué el sistema debe diseñarse en función del dispositivo de la base de datos

Hola Habr!

Continuamos explorando el tema de Java y Spring , incluso a nivel de base de datos. Hoy ofrecemos leer sobre por qué, cuando se diseñan aplicaciones grandes, es la estructura de la base de datos, y no el código Java, lo que debería tener un significado decisivo sobre cómo se hace esto y cuáles son las excepciones a esta regla.

En este artículo bastante tardío, explicaré por qué creo que en casi todos los casos el modelo de datos en la aplicación debe diseñarse "en base a la base de datos" y no "en función de las capacidades de Java" (u otro lenguaje de cliente con el que trabaje). Al elegir el segundo enfoque, te embarcas en un largo viaje de dolor y sufrimiento tan pronto como tu proyecto comienza a crecer.

Este artículo se basa en una pregunta que se hace en Stack Overflow.

Interesantes discusiones sobre reddit en las secciones / r / java y / r / programación .

Codigo de GENERACION


Me sorprende que haya una capa tan pequeña de usuarios que, habiéndose familiarizado con jOOQ, están indignados por el hecho de que cuando trabaja jOOQ se basa seriamente en la generación de código fuente. Nadie te molesta para usar jOOQ como mejor te parezca, y no te obliga a usar la generación de código. Pero de manera predeterminada (como se describe en el manual), trabajar con jOOQ sucede de esta manera: comienza con un esquema de base de datos (heredado), realiza ingeniería inversa utilizando el generador de código jOOQ, para obtener un conjunto de clases que representan sus tablas, y luego escriba consultas de tipo seguro en estas tablas:

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

El código se genera manualmente fuera del ensamblaje o manualmente con cada ensamblaje. Por ejemplo, dicha regeneración puede seguir inmediatamente después de la migración de la base de datos Flyway, que también se puede hacer de forma manual o automática .

Generación de código fuente


Varias filosofías, ventajas y desventajas están asociadas con tales enfoques para la generación de código, manual y automático, que no voy a discutir en detalle en este artículo. Pero, en general, todo el punto del código generado es que nos permite reproducir en Java esa "verdad" que damos por sentado, ya sea dentro de nuestro sistema o fuera de él. En cierto sentido, los compiladores que generan bytecode, código de máquina u otro tipo de código fuente hacen lo mismo: obtenemos una representación de nuestra "verdad" en otro idioma, independientemente de razones específicas.

Hay muchos generadores de código de este tipo. Por ejemplo, XJC puede generar código Java basado en archivos XSD o WSDL . El principio es siempre el mismo:

  • Hay algo de verdad (interna o externa), por ejemplo, especificación, modelo de datos, etc.
  • Necesitamos una representación local de esta verdad en nuestro lenguaje de programación.

Además, generar tal representación es casi siempre recomendable, para evitar la redundancia.

Proveedores de tipos y procesamiento de anotaciones


Nota: otro enfoque más moderno y específico para la generación de código para jOOQ está asociado con el uso de proveedores de tipos, en la forma en que se implementan en F # . En este caso, el compilador genera el código, en realidad en la etapa de compilación. En forma de fuentes, dicho código, en principio, no existe. Existen herramientas similares, aunque menos elegantes, en Java: estos son procesadores de anotaciones como Lombok .

En cierto sentido, aquí suceden las mismas cosas que en el primer caso, con la excepción de:

  • No ve el código generado (¿tal vez esta situación le parece a alguien no tan repulsivo?)
  • , , , «» . Lombok, “”. , .

?


Además de la pregunta difícil de cómo es mejor comenzar la generación de código, de forma manual o automática, es necesario mencionar que hay personas que creen que la generación de código no es necesaria en absoluto. La razón de este punto de vista, que encontré con mayor frecuencia, es que es difícil configurar la tubería de ensamblaje. Sí, muy duro Hay costos adicionales de infraestructura. Si recién está comenzando a trabajar con un determinado producto (ya sea jOOQ, o JAXB, o Hibernate, etc.), lleva tiempo configurar el entorno de trabajo que le gustaría gastar en aprender la API en sí, y luego extraer valor de él.

Si la sobrecarga asociada con la comprensión del dispositivo del generador es demasiado grande, entonces la API realmente trabajó un poco en la usabilidad del generador de código (y en el futuro, resulta que la configuración del usuario es complicada). La facilidad de uso debe ser la máxima prioridad para cualquier API de este tipo. Pero este es solo un argumento en contra de la generación de código. Por lo demás, es completamente completamente manual escribir una representación local de la verdad interna o externa.

Muchos dirán que no tienen tiempo para hacer todo esto. Tienen plazos para su Súper Producto. Algún tiempo después peinamos los transportadores de ensamblaje, llegará a tiempo. Les responderé:


Original , Alan O'Rourke, Audience Stack.

Pero en Hibernate / JPA es muy fácil escribir código "para Java".

De Verdad. Para Hibernate y sus usuarios, esto es tanto una bendición como una maldición. En Hibernate, simplemente puede escribir un par de entidades, como esta:

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

Y casi todo está listo. Ahora el destino de Hibernate es generar "detalles" complejos de cómo se definirá exactamente esta entidad en el DDL de su "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);

... y comenzamos a manejar la aplicación. Una oportunidad realmente genial para comenzar rápidamente y probar cosas diferentes.

Sin embargo, permitir. Estaba engañando

  • ¿Hibernate realmente aplica la definición de esta clave primaria con nombre?
  • ¿Hibernate creará un índice en TITLE? "Sé con certeza que lo necesitaremos".
  • ¿Hibernate hace exactamente esta clave identificable en la Especificación de identidad?

Probablemente no. Si está desarrollando su proyecto desde cero, siempre es conveniente simplemente eliminar la base de datos anterior y generar una nueva tan pronto como agregue las anotaciones necesarias. Entonces, la entidad Libro eventualmente tomará la forma:

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

Frio. Regenerado. Nuevamente, en este caso, al principio será muy fácil.

Pero luego tienes que pagar por ello


Tarde o temprano tienes que entrar en producción. En ese momento, ese modelo dejará de funcionar. Porque:

en producción ya no será posible, si es necesario, descartar la base de datos anterior y comenzar desde cero. Su base de datos se convertirá en una heredada.

A partir de ahora, tendrá que escribir secuencias de comandos de migración DDL, por ejemplo, utilizando Flyway . ¿Y qué pasa entonces con tus entidades? Puede adaptarlos manualmente (y así duplicar su carga de trabajo) u ordenar a Hibernate que los regenere por usted (¿qué posibilidades hay de que se generen de esta manera para satisfacer sus expectativas?) De todos modos, pierde.

Por lo tanto, tan pronto como entre en producción, necesitará parches calientes. Y deben ponerse en producción muy rápidamente. Dado que no ha preparado y organizado una transportación fluida de sus migraciones para la producción, está remendando todo salvajemente. Y luego no tienes tiempo para hacer todo bien. Y regañe a Hibernate, porque siempre se puede culpar a cualquiera, pero no a usted ...

En cambio, desde el principio todo se podría hacer de una manera completamente diferente. Por ejemplo, ponga ruedas redondas en una bicicleta.

Base de datos primero


La verdadera "verdad" en el esquema de su base de datos y la "soberanía" sobre ella se encuentran dentro de la base de datos. Un esquema se define solo en la base de datos y en ningún otro lugar, y cada cliente tiene una copia de este esquema, por lo que es completamente recomendable imponer el cumplimiento del esquema y su integridad, para hacerlo directamente en la base de datos, donde se almacena la información.
Esta es incluso la vieja sabiduría gastada. Las claves primarias y únicas son buenas. Las claves foráneas son buenas. La comprobación de límites es buena. Las declaraciones son buenas.

Además, esto no es todo. Por ejemplo, usando Oracle, probablemente desee especificar:

  • ¿En qué espacio de tabla está tu tabla?
  • ¿Cuál es su valor PCTFREE?
  • ¿Cuál es el tamaño de la caché en su secuencia (detrás del identificador)

Quizás todo esto no sea importante en sistemas pequeños, pero no es necesario esperar la transición al área de "big data"; es posible y mucho antes comenzar a beneficiarse de las optimizaciones de almacenamiento proporcionadas por el proveedor, como las mencionadas anteriormente. Ninguno de los ORM que he visto (incluido jOOQ) proporciona acceso al conjunto completo de opciones de DDL que es posible que desee utilizar en su base de datos. Los ORM ofrecen algunas herramientas que ayudan a escribir DDL.

Pero al final, un circuito bien diseñado se escribe manualmente en DDL. Cualquier DDL generado es solo una aproximación de él.

¿Qué pasa con el modelo del cliente?


Como se mencionó anteriormente, en el cliente necesitará una copia del esquema de su base de datos, la vista del cliente. No hace falta decir que esta vista del cliente debe estar sincronizada con el modelo real. ¿Cuál es la mejor manera de lograr esto? Usando un generador de código.

Todas las bases de datos proporcionan su metainformación a través de SQL. Aquí le mostramos cómo obtener todas las tablas en diferentes dialectos de SQL de su base de datos:

	-- 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 (o similares, dependiendo de si también tiene que considerar representaciones, representaciones materializadas, funciones con un valor de tabla) también se realizan utilizando una llamada DatabaseMetaData.getTables()de JDBC o utilizando el metamódulo jOOQ.

A partir de los resultados de tales consultas, es relativamente fácil generar cualquier vista de cliente de su modelo de base de datos, independientemente de qué tecnología se utilice en su cliente.

  • Si usa JDBC o Spring, puede crear un conjunto de constantes de cadena
  • Si usa JPA, puede generar entidades ellos mismos
  • Si usa jOOQ, puede generar el metamodelo jOOQ

Dependiendo de cuántas funciones ofrezca la API de su cliente (por ejemplo, jOOQ o JPA), el metamodelo generado puede ser realmente rico y completo. Tomemos, por ejemplo , la posibilidad de uniones implícitas que aparecieron en jOOQ 3.11 , que se basa en la metainformación generada sobre las relaciones de claves foráneas entre sus tablas.

Ahora, cualquier incremento de la base de datos conducirá automáticamente a la actualización del código del cliente. Imagina por ejemplo:

ALTER TABLE book RENAME COLUMN title TO book_title;

¿Realmente te gustaría hacer este trabajo dos veces? En ningún caso. Simplemente arregle el DDL, ejecútelo a través de su canal de ensamblaje y obtenga la entidad 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;
}

O una clase jOOQ actualizada. La mayoría de los cambios DDL también afectan la semántica, no solo la sintaxis. Por lo tanto, puede ser conveniente ver en el código compilado qué código se verá (o puede ser) afectado por el incremento de su base de datos.

La unica verdad


No importa qué tecnología use, siempre hay un modelo que es la única fuente de verdad para algún subsistema, o, al menos, debemos esforzarnos por esto y evitar tal confusión empresarial, donde la "verdad" está en todas partes y en ninguna parte. Todo puede ser mucho más simple. Si solo está intercambiando archivos XML con algún otro sistema, simplemente use XSD. Mire el metamodelo INFORMATION_SCHEMA de jOOQ en formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD se entiende bien
  • XSD XML
  • XSD
  • XSD Java XJC

El último punto es importante. Cuando nos comunicamos con un sistema externo utilizando mensajes XML, queremos estar seguros de la validez de nuestros mensajes. Esto es muy fácil de lograr con JAXB, XJC y XSD. Sería una locura esperar que, cuando nos acercamos al diseño de “Java primero”, donde hacemos nuestros mensajes en forma de objetos Java, de alguna manera pudieran mostrarse claramente en XML y enviarse para su consumo a otro sistema. El XML generado de esta manera sería de muy baja calidad, no documentado y sería difícil de desarrollar. Si hubiera un acuerdo sobre el nivel de calidad de servicio (SLA) en dicha interfaz, lo arruinaríamos de inmediato.

Honestamente, eso es exactamente lo que sucede todo el tiempo desde la API hasta JSON, pero esa es otra historia, lo juro la próxima vez ...

Bases de datos: es lo mismo


Cuando trabajas con bases de datos, entiendes que son, en principio, similares. La base posee sus datos y debe administrar el esquema. Cualquier modificación realizada en el esquema debe implementarse directamente en DDL para actualizar una única fuente de verdad.

Cuando se produce una actualización de origen, todos los clientes también deben actualizar sus copias del modelo. Algunos clientes pueden escribirse en Java usando jOOQ e Hibernate o JDBC (o todos a la vez). Se pueden escribir otros clientes en Perl (queda desearles buena suerte) y otros en C #. No importa. El modelo principal está en la base de datos. Los modelos generados con ORM, generalmente de baja calidad, están poco documentados y son difíciles de desarrollar.

Por lo tanto, no cometas errores. No cometer errores desde el principio. Trabajar desde una base de datos. Cree una tubería de implementación que pueda automatizarse. Active los generadores de código para que sea conveniente copiar su modelo de base de datos y volcarlo en los clientes. Y deja de preocuparte por los generadores de código. Ellos son buenos. Con ellos serás más productivo. Solo necesita pasar un poco de tiempo desde el principio para configurarlos, y luego tendrá años de mayor productividad que formarán la historia de su proyecto.

Hasta entonces, gracias.

Explicación


Para mayor claridad: este artículo de ninguna manera aboga por que, bajo el modelo de su base de datos, necesite doblar todo el sistema (es decir, área temática, lógica comercial, etc., etc.). En este artículo, digo que el código del cliente que interactúa con la base de datos debe actuar sobre la base del modelo de la base de datos para que no reproduzca el modelo de la base de datos en el estado de "primera clase". Esta lógica generalmente se encuentra en el nivel de acceso a datos de su cliente.

En arquitecturas de dos niveles, que aún se conservan en algunos lugares, tal modelo de sistema puede ser el único posible. Sin embargo, en la mayoría de los sistemas, el nivel de acceso a datos me parece un "subsistema" que encapsula un modelo de base de datos.

Excepciones


Hay excepciones a cualquier regla, y ya he dicho que un enfoque con primacía de la base de datos y generación de código fuente a veces puede ser inapropiado. Aquí hay un par de excepciones (probablemente hay otras):

  • Cuando el circuito es desconocido, y debe abrirse. Por ejemplo, usted es un proveedor de una herramienta para ayudar a los usuarios a navegar cualquier esquema. Uf No hay generación de código. Pero aún así, la base de datos está por encima de todo.
  • Cuando se debe generar un circuito sobre la marcha para resolver un problema determinado. Este ejemplo parece una versión ligeramente fantasiosa del patrón de valor de atributo de entidad , es decir, realmente no tiene un esquema bien definido. En este caso, a menudo es imposible estar seguro de que un RDBMS es adecuado para usted.

Las excepciones son inherentemente excepcionales. En la mayoría de los casos que involucran el uso de un RDBMS, el esquema se conoce de antemano, se encuentra dentro del RDBMS y es la única fuente de "verdad", y todos los clientes tienen que adquirir copias que se derivan de él. Idealmente, debe usar un generador de código.

All Articles