La verdad primero, o por qué el sistema debe diseñarse en función de la estructura de la base de datos

¡Hola, Habr!

Seguimos investigando el tema. Java и Primavera, incluso a nivel de base de datos. Hoy lo invitamos a leer sobre por qué, al diseñar aplicaciones grandes, la estructura de la base de datos y no el código Java debe tener una importancia decisiva, cómo se hace esto y qué excepciones existen a esta regla.

En este artículo bastante tardío, explicaré por qué creo que en casi todos los casos, el modelo de datos de una aplicación debe diseñarse "a partir de la base de datos" en lugar de "a partir de las capacidades de Java" (o cualquier lenguaje de cliente que esté utilizando). trabajando con). Al adoptar el segundo enfoque, se está preparando para un largo camino de dolor y sufrimiento una vez que su proyecto comience a crecer.

El artículo fue escrito basándose en una pregunta, proporcionado en Stack Overflow.

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

Codigo de GENERACION

Qué me sorprendió que haya un segmento tan pequeño de usuarios que, después de familiarizarse con jOOQ, estén indignados por el hecho de que jOOQ dependa seriamente de la generación de código fuente para funcionar. Nadie le impide utilizar jOOQ como mejor le parezca ni le obliga a utilizar la generación de código. Pero la forma predeterminada (como se describe en el manual) de trabajar con jOOQ es comenzar con un esquema de base de datos (heredado), realizar ingeniería inversa utilizando el generador de código jOOQ para obtener así un conjunto de clases que representen sus tablas y luego escribir el tipo. -Consultas seguras a 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 en cada ensamblaje. Por ejemplo, dicha regeneración puede seguir inmediatamente después Migración de la base de datos Flyway, que también se puede realizar de forma manual o automática.

Generación de código fuente

Existen varias filosofías, ventajas y desventajas asociadas con estos enfoques para la generación de código (manual y automática) que no voy a discutir en detalle en este artículo. Pero, en general, el objetivo del código generado es que nos permite reproducir en Java esa “verdad” que damos por sentada, ya sea dentro de nuestro sistema o fuera de él. En cierto sentido, esto es lo que hacen los compiladores cuando generan código de bytes, código de máquina o alguna otra forma de código fuente: obtenemos una representación de nuestra "verdad" en otro idioma, independientemente de las razones específicas.

Existen muchos generadores de códigos 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, una especificación, un modelo de datos, etc.
  • Necesitamos una representación local de esta verdad en nuestro lenguaje de programación.

Además, casi siempre es aconsejable generar dicha representación para evitar redundancias.

Proveedores de tipos y procesamiento de anotaciones

Nota: otro enfoque más moderno y específico para generar código para jOOQ es utilizar proveedores de tipos, tal como están implementados en F#. En este caso, el código lo genera el compilador, en realidad en la etapa de compilación. En principio, dicho código no existe en forma fuente. Java tiene herramientas similares, aunque no tan elegantes: procesadores de anotaciones, por ejemplo, Lombok.

En cierto sentido aquí sucede lo mismo que en el primer caso, a excepción de:

  • No ves el código generado (¿quizás esta situación le parezca menos repulsiva a alguien?)
  • Debe asegurarse de que se puedan proporcionar tipos, es decir, "verdadero" siempre debe estar disponible. Esto es fácil en el caso de Lombok, que comenta “verdad”. Es un poco más complicado con los modelos de bases de datos que dependen de una conexión en vivo disponible constantemente.

¿Cuál es el problema con la generación de código?

Además de la difícil cuestión de cuál es la mejor manera de ejecutar la generación de código, ya sea manual o automáticamente, también debemos mencionar que hay personas que creen que la generación de código no es necesaria en absoluto. La justificación de este punto de vista, con el que me encontré con mayor frecuencia, es que entonces es difícil configurar un proceso de construcción. Sí, es realmente difícil. Surgen costos de infraestructura adicionales. Si recién está comenzando con un producto en particular (ya sea jOOQ, JAXB, Hibernate, etc.), configurar un entorno de producción requiere tiempo que preferiría dedicar a aprender la API en sí para poder extraer valor de ella. .

Si los costos asociados con la comprensión de la estructura del generador son demasiado altos, entonces, de hecho, la API hizo un mal trabajo en la usabilidad del generador de código (y luego resulta que la personalización del usuario también es difícil). La usabilidad debe ser la máxima prioridad para cualquier API de este tipo. Pero este es sólo un argumento en contra de la generación de código. De lo contrario, es absolutamente manual escribir una representación local de la verdad interna o externa.

Muchos dirán que no tienen tiempo para hacer todo esto. Se están acabando los plazos para su Súper Producto. Algún día ordenaremos las cintas transportadoras de montaje, tendremos tiempo. Yo les responderé:

La verdad primero, o por qué el sistema debe diseñarse en función de la estructura de la base de datos
Original, Alan O'Rourke, Pila de audiencia

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

En realidad. Para Hibernate y sus usuarios, esto es a la vez una bendición y una maldición. En Hibernate puedes simplemente escribir un par de entidades, como esta:

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

Y casi todo está listo. Ahora le toca a Hibernate generar los "detalles" complejos de cómo exactamente se definirá 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 comienza a ejecutar la aplicación. Una oportunidad realmente genial para empezar rápidamente y probar cosas diferentes.

Sin embargo, permítame. Estaba mintiendo.

  • ¿Hibernate realmente aplicará la definición de esta clave primaria nombrada?
  • ¿Hibernate creará un índice en TITLE? – Estoy seguro de que lo necesitaremos.
  • ¿Hibernate hará exactamente que esta clave se identifique en la Especificación de identidad?

Probablemente no. Si está desarrollando su proyecto desde cero, siempre es conveniente simplemente descartar la base de datos anterior y generar una nueva tan pronto como agregue las anotaciones necesarias. Por lo tanto, la entidad Libro finalmente 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;
}

Fresco. Regenerado. De nuevo, en este caso será muy fácil al principio.

Pero tendrás que pagarlo más tarde.

Tarde o temprano tendrás que pasar a producción. Entonces será cuando este modelo dejará de funcionar. Porque:

En producción, ya no será posible, si es necesario, descartar la base de datos antigua y empezar desde cero. Su base de datos se convertirá en un legado.

De ahora en adelante y para siempre tendrás que escribir Scripts de migración DDL, por ejemplo, usando Flyway. ¿Qué pasará con sus entidades en este caso? Puede adaptarlos manualmente (y así duplicar su carga de trabajo), o puede decirle a Hibernate que los regenere por usted (¿qué probabilidades hay de que los generados de esta manera cumplan con sus expectativas?) De cualquier manera, pierde.

Entonces, una vez que entres en producción, necesitarás parches calientes. Y es necesario ponerlos en producción muy rápidamente. Como no se preparó ni organizó un proceso fluido de sus migraciones para producción, parchea todo alocadamente. Y entonces ya no tendrás tiempo para hacer todo correctamente. Y criticas a Hibernate, porque siempre es culpa de otra persona, pero no de ti...

En cambio, las cosas podrían haberse hecho de manera completamente diferente desde el principio. Por ejemplo, ponle ruedas redondas a una bicicleta.

Base de datos primero

La verdadera "verdad" en el esquema de su base de datos y la "soberanía" sobre él se encuentra dentro de la base de datos. El esquema se define sólo en la propia base de datos y en ningún otro lugar, y cada cliente tiene una copia de este esquema, por lo que tiene mucho sentido hacer cumplir el esquema y su integridad, hacerlo directamente en la base de datos, donde se encuentra la información. almacenado.
Esta es una sabiduría antigua e incluso trillada. Las claves primarias y únicas son buenas. Las claves externas son buenas. Verificar las restricciones es bueno. Declaraciones - Bien.

Además, eso no es todo. Por ejemplo, al utilizar Oracle, probablemente desee especificar:

  • ¿En qué tablespace está tu mesa?
  • ¿Cuál es su valor PCTFREE?
  • ¿Cuál es el tamaño de la caché en su secuencia (detrás de la identificación)?

Puede que esto no sea importante en sistemas pequeños, pero no tiene que esperar hasta pasar al ámbito de los big data: puede comenzar a beneficiarse de optimizaciones de almacenamiento proporcionadas por proveedores como las mencionadas anteriormente mucho antes. Ninguno de los ORM que he visto (incluido jOOQ) proporciona acceso al conjunto completo de opciones de DDL que quizás desee utilizar en su base de datos. Los ORM ofrecen algunas herramientas que le ayudan a escribir DDL.

Pero al final del día, un circuito bien diseñado está escrito a mano en DDL. Cualquier DDL generado es sólo una aproximación del mismo.

¿Qué pasa con el modelo de 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 mencionar 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. A continuación se explica cómo obtener todas las tablas de su 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 (o similares, dependiendo de si también hay que considerar vistas, vistas materializadas, funciones con valores de tabla) también se ejecutan llamando DatabaseMetaData.getTables() desde JDBC, o usando el metamódulo jOOQ.

A partir de los resultados de dichas consultas, es relativamente fácil generar cualquier representación del lado del cliente de su modelo de base de datos, independientemente de la tecnología que utilice en el cliente.

  • Si está utilizando JDBC o Spring, puede crear un conjunto de constantes de cadena
  • Si usa JPA, puede generar las propias entidades.
  • Si usa jOOQ, puede generar el metamodelo jOOQ

Dependiendo de cuánta funcionalidad 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, introducido en jOOQ 3.11, que se basa en metainformación generada sobre las relaciones de clave externa que existen entre sus tablas.

Ahora cualquier incremento de la base de datos actualizará automáticamente el código del cliente. Imagínese por ejemplo:

ALTER TABLE book RENAME COLUMN title TO book_title;

¿Realmente querrías hacer este trabajo dos veces? En ningún caso. Simplemente confirme el DDL, ejecútelo a través de su canal de compilación 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 la clase jOOQ actualizada. La mayoría de los cambios de DDL también afectan la semántica, no sólo la sintaxis. Por lo tanto, puede resultar útil buscar en el código compilado para ver qué código se verá (o podría) verse afectado por el incremento de su base de datos.

la unica verdad

Independientemente de la tecnología que se utilice, siempre hay un modelo que es la única fuente de verdad para algún subsistema o, como mínimo, debemos esforzarnos por conseguirlo y evitar esa confusión empresarial en la que la “verdad” está en todas partes y en ninguna a la vez. . Todo podría ser mucho más sencillo. Si sólo estás intercambiando archivos XML con algún otro sistema, simplemente usa XSD. Mire el metamodelo INFORMACIÓN_SCHEMA de jOOQ en formato XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD se entiende bien
  • XSD tokeniza muy bien el contenido XML y permite la validación en todos los idiomas del cliente.
  • XSD está bien versionado y tiene compatibilidad con versiones anteriores avanzadas.
  • XSD se puede traducir a código Java usando XJC

El último punto es importante. Cuando nos comunicamos con un sistema externo mediante mensajes XML, queremos estar seguros de que nuestros mensajes sean válidos. Esto es muy fácil de lograr usando JAXB, XJC y XSD. Sería una locura pensar que, con un enfoque de diseño "Java primero", en el que hacemos nuestros mensajes como objetos Java, de alguna manera podrían mapearse coherentemente a XML y enviarse a otro sistema para su consumo. El XML generado de esta manera sería de muy mala calidad, no estaría documentado y sería difícil de desarrollar. Si hubiera un acuerdo de nivel de servicio (SLA) para dicha interfaz, lo arruinaríamos inmediatamente.

Honestamente, esto es lo que sucede todo el tiempo con las API JSON, pero esa es otra historia, pelearé la próxima vez...

Bases de datos: son lo mismo

Cuando trabajas con bases de datos, te das cuenta de que todas son básicamente similares. La base es propietaria de sus datos y debe gestionar el esquema. Cualquier modificación realizada al esquema debe implementarse directamente en el DDL para que se actualice la fuente única de verdad.

Cuando se produce una actualización de origen, todos los clientes también deben actualizar sus copias del modelo. Algunos clientes se pueden escribir en Java usando jOOQ e Hibernate o JDBC (o ambos). Otros clientes pueden escribirse en Perl (solo les deseamos buena suerte), mientras que otros pueden escribirse en C#. No importa. El modelo principal está en la base de datos. Los modelos generados mediante ORM suelen ser de mala calidad, mal documentados y difíciles de desarrollar.

Así que no cometas errores. No cometas errores desde el principio. Trabajar desde la base de datos. Cree un canal de implementación que pueda automatizarse. Habilite los generadores de código para facilitar la copia de su modelo de base de datos y volcarlo en los clientes. Y deja de preocuparte por los generadores de códigos. Ellos son buenos. Con ellos serás más productivo. Sólo necesita dedicar un poco de tiempo a configurarlos desde el principio, y luego le esperan años de mayor productividad, que conformarán la historia de su proyecto.

No me agradezcas todavía, más tarde.

Aclaración

Para ser claros: este artículo de ninguna manera recomienda que sea necesario adaptar todo el sistema (es decir, dominio, lógica de negocios, etc., etc.) para que se ajuste a su modelo de base de datos. Lo que digo en este artículo es 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, de modo que él mismo no reproduzca el modelo de la base de datos en un estado de "primera clase". Esta lógica generalmente se encuentra en la capa de acceso a datos de su cliente.

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

Excepciones

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

  • Cuando el esquema es desconocido y necesita ser descubierto. Por ejemplo, usted es proveedor de una herramienta que ayuda a los usuarios a navegar por cualquier diagrama. Puaj. Aquí no hay generación de código. Pero aun así, la base de datos es lo primero.
  • Cuando se debe generar un circuito sobre la marcha para solucionar algún problema. Este ejemplo parece una versión un poco extravagante del patrón. valor del atributo de entidad, es decir, realmente no tienes un esquema claramente definido. En este caso, a menudo ni siquiera puede estar seguro de que un RDBMS sea adecuado para sus necesidades.

Las excepciones son por naturaleza excepcionales. En la mayoría de los casos que implican el uso de un RDBMS, el esquema se conoce de antemano, reside dentro del RDBMS y es la única fuente de "verdad", y todos los clientes tienen que adquirir copias derivadas de él. Lo ideal es utilizar un generador de códigos.

Fuente: habr.com

Añadir un comentario