¡Hola, Habr!
Continuamos explorando el tema y , incluyendo a nivel de bases de datos. Hoy les proponemos leer sobre por qué, al diseñar aplicaciones grandes, la estructura de la base de datos y no el código Java debería tener un papel decisivo, cómo se hace y qué excepciones hay a esta regla.
En este artículo bastante atrasado explicaré por qué creo que, prácticamente en todos los casos, el modelo de datos en una aplicación debe ser diseñado "basado en la base de datos", y no "basado en las capacidades de Java" (o cualquier otro lenguaje del cliente que estés utilizando). Si eliges el segundo enfoque, te adentras en un largo camino de dolor y sufrimiento, tan pronto como tu proyecto comience a crecer.
El artículo está basado en , formulada en Stack Overflow.
Interesantes discusiones en reddit en las secciones y .
Generación de código
Me sorprendió mucho que haya tan poca capa de usuarios que, al conocer jOOQ, se indignan con el hecho de que al trabajar con jOOQ se confía considerablemente en la generación de código fuente. Nadie te impide usar jOOQ como consideres necesario, y no te obliga a usar la generación de código. Pero por defecto (como se describe en la guía), trabajar con jOOQ funciona así: comienzas con un esquema de base de datos (heredado), realizas su ingeniería inversa con la ayuda del generador de código jOOQ para obtener un conjunto de clases que representan tus tablas, y luego escribes consultas seguras en tipo a esas tablas:
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ La información de tipos es inferida a partir
// del código generado al que se refiere la
// condición SELECT siguiente
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ nombres generados
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}El código se genera ya sea manualmente fuera de la compilación, o manualmente en cada compilación. Por ejemplo, tal regeneración puede seguir inmediatamente después de .
Generación de código fuente
Con enfoques como la generación de código, tanto manuales como automáticos, hay distintas filosofías, ventajas y desventajas que no voy a discutir en detalle en este artículo. Pero, en general, toda la esencia del código generado es que permite reproducir en Java esa "verdad" que aceptamos como un hecho, ya sea dentro de nuestro sistema o fuera de él. En cierto sentido, lo mismo hacen los compiladores, generando bytecode, código máquina o algún otro tipo de código a partir del código fuente; obtenemos una representación de nuestra "verdad" en otro lenguaje, independientemente de las razones concretas.
Existen muchos generadores de este tipo. Por ejemplo, El principio siempre es el mismo:
- Hay una verdad (interna o externa), como una especificación, un modelo de datos, etc.
- Necesitamos una representación local de esa verdad en nuestro lenguaje de programación.
Además, generar tal representación casi siempre es conveniente para evitar la redundancia.
Proveedores de tipos y procesamiento de anotaciones
A tener en cuenta: otro enfoque más moderno y específico para la generación de código para jOOQ está relacionado con el uso de proveedores de tipos, En este caso, el código es generado por el compilador, precisamente en la fase de compilación. En forma de código fuente, tal código en principio no existe. En Java hay herramientas similares, aunque no tan elegantes: son los procesadores de anotaciones, por ejemplo, .
En cierto sentido, aquí ocurren las mismas cosas que en el primer caso, excepto que:
- No ves el código generado (¿quizás esta situación no es tan desalentadora para algunos?).
- Debes garantizar que los tipos puedan ser proporcionados, es decir, la "verdad" siempre debe ser accesible. Esto es fácil en el caso de Lombok, que anota la "verdad". Es un poco más complicado con los modelos de bases de datos, cuya operación depende de una conexión en vivo disponible de forma constante.
¿Cuál es el problema con la generación de código?
Además de la astuta pregunta sobre la mejor manera de iniciar la generación de código, ya sea manualmente o de forma automática, hay que mencionar que hay personas que consideran que la generación de código no es necesaria en absoluto. La justificación de este punto de vista, que he encontrado con más frecuencia, es que luego es complicado configurar la tubería de construcción. Sí, realmente es complicado. Surgen costos adicionales de infraestructura. Si apenas estás comenzando a trabajar con un producto determinado (ya sea jOOQ, JAXB, Hibernate, etc.), el tiempo que se dedica a configurar el entorno de trabajo es un tiempo que preferirías gastar en estudiar la API misma, para luego extraer valor de ella.
Si los costos relacionados con entender el funcionamiento del generador son demasiado altos, entonces, efectivamente, la usabilidad del generador de código en la API no ha sido bien trabajada (y posteriormente resulta que la configuración personalizada en él también es complicada). La facilidad de uso debe ser la máxima prioridad para cualquier API de este tipo. Pero este es tan solo un argumento en contra de la generación de código. En el resto de los casos, es completamente necesario escribir manualmente la representación local de la verdad interna o externa.
Muchos dirán que no tienen tiempo para ocuparse de todo esto. Tienen fechas límites para entregar su Súper Producto. Alguna vez después ajustaremos las tuberías de construcción, habrá tiempo. Yo les responderé:

,
Pero en Hibernate / JPA es tan sencillo escribir código "para Java".
De hecho. Para Hibernate y sus usuarios, esto es al mismo tiempo una bendición y una maldición. En Hibernate, puedes simplemente escribir un par de entidades, así:
@Entity
class Book {
@Id
int id;
String title;
}Y casi todo está listo. Ahora, la tarea de Hibernate es generar los complejos "detalles" sobre cómo exactamente esta entidad será definida en el DDL de tu "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 empezamos a ejecutar la aplicación. Realmente es una gran oportunidad para comenzar a trabajar rápidamente y probar diferentes cosas.
Sin embargo, permíteme. He sido un poco deshonesto.
- ¿Y Hibernate realmente aplicará la definición de esta clave primaria nombrada?
- ¿Hibernate creará un índice en TITLE? – sé con certeza que lo necesitaremos.
- ¿Y Hibernate realmente hará que esta clave sea identificativa en la Especificación de Identidad?
Probablemente no. Si estás desarrollando tu proyecto desde cero, siempre es conveniente simplemente deshacerse de la antigua base de datos y generar una nueva, tan pronto como agregues las anotaciones necesarias. Así, la entidad Book al final 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;
}
Genial. Regenerar de nuevo. De nuevo, en ese caso será muy fácil al principio.
Pero posteriormente tendrás que pagar por ello.
Tarde o temprano, tendrás que salir a producción. Es precisamente entonces cuando este tipo de modelo dejará de funcionar. Porque:
En producción ya no podrás deshacerte de la antigua base de datos cuando sea necesario y comenzar de nuevo desde cero. Tu base de datos se convertirá en heredada.
A partir de ahora y para siempre, tendrás que escribir . ¿Y qué pasará entonces con tus entidades? Podrás adaptarlas manualmente (y así duplicar tu carga de trabajo), o le pedirás a Hibernate que las regenere por ti (¿qué probabilidades hay de que lo generado esté a la altura de tus expectativas?) En cualquier caso, pierdes.
Así que, una vez que pases a producción, necesitarás parches urgentes. Y necesitas implementarlos en producción rápidamente. Dado que no te preparaste y no organizaste una canalización suave para tus migraciones en producción, todo será parcheado de manera desordenada. Y luego ya no tendrás tiempo para hacerlo bien. Y culpas a Hibernate, ya que siempre hay alguien más a quien culpar, menos a ti...
En lugar de eso, desde el principio todo podría haberse hecho de manera completamente diferente. Por ejemplo, poner ruedas redondas en la bicicleta.
Primero la base de datos.
La verdadera "verdad" en el esquema de tu base de datos y la "soberanía" sobre ella reside dentro de la base de datos. El esquema se define únicamente en la propia base de datos y en ninguna otra parte, y cada uno de los clientes tiene una copia de ese esquema, por lo que tiene total sentido imponer el cumplimiento del esquema y su integridad, hacer esto directamente en la base de datos - allí donde se almacena la información.
Es una sabiduría antigua, incluso manida. Las claves primarias y únicas son buenas. Las claves foráneas son buenas. La verificación de restricciones es buena. son buenas.
Sin embargo, eso no es todo. Por ejemplo, si utiliza Oracle, probablemente querrá especificar:
- ¿En qué espacio de tablas se encuentra su tabla?
- ¿Cuál es su valor PCTFREE?
- ¿Cuál es el tamaño de la caché en su secuencia (por el identificador)?
Puede que todo esto no sea relevante en sistemas pequeños, pero no es necesario esperar a entrar en la zona de "big data"; se puede comenzar a beneficiarse de las optimizaciones de almacenamiento proporcionadas por los proveedores mucho antes, como las mencionadas anteriormente. Ninguna de las ORM que he visto (incluyendo jOOQ) proporciona acceso al conjunto completo de opciones DDL que puede que desee usar en su base de datos. Las ORM ofrecen algunas herramientas que ayudan a escribir DDL.
Pero, al final, un esquema bien diseñado se escribe manualmente en DDL. Cualquier DDL generado es solo una aproximación de ello.
¿Qué pasa con el modelo del cliente?
Como se mencionó anteriormente, en el cliente necesitará una copia del esquema de su base de datos, una vista del cliente. Es innecesario mencionar que esta vista del cliente debe estar sincronizada con el modelo real. ¿Cuál es la mejor manera de lograr esto? A través de un generador de código.
Todas las bases de datos proporcionan su metainformación a través de SQL. Así es como 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 se deben tener en cuenta las vistas, vistas materializadas, funciones con valor de tabla) también se realizan a través de la llamada desde JDBC, o a través del módulo meta de jOOQ.
A partir de los resultados de tales consultas, es relativamente fácil generar cualquier vista del modelo de su base de datos en el cliente, independientemente de la tecnología que utilice.
- Si utiliza JDBC o Spring, puede crear un conjunto de constantes de cadena.
- Si utiliza JPA, puede generar las entidades usted mismo.
- Si utiliza jOOQ, puede generar el modelo meta de jOOQ.
Dependiendo del volumen de posibilidades que ofrece su API cliente (por ejemplo, jOOQ o JPA), el modelo de metadatos generado puede ser realmente rico y completo. Tomemos, por ejemplo, la posibilidad de uniones implícitas, , que se basa en la metainformación generada sobre las relaciones de claves externas que existen entre sus tablas.
Ahora, cualquier modificación en la base de datos actualizará automáticamente el código cliente. Imagine, por ejemplo:
ALTER TABLE book RENAME COLUMN title TO book_title;¿Realmente querría hacer este trabajo dos veces? De ninguna manera. Simplemente registramos el DDL, lo pasamos a través de su pipeline de compilación y obtenemos la entidad actualizada:
@Entity
@Table(name = "book", indexes = {
// ¿Lo ha pensado?
@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 en el DDL también se reflejan en la semántica, no solo en la sintaxis. Por lo tanto, puede ser conveniente ver en el código compilado qué código será (o puede ser) afectado por la modificación de su base de datos.
La única verdad
Independientemente de la tecnología que utilice, siempre hay un modelo que es la única fuente de verdad para algún subsistema, o al menos, debemos aspirar a ello y evitar esa 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 modelo de metadatos INFORMATION_SCHEMA de jOOQ en formato XML:
- XSD es muy comprensible
- XSD etiqueta muy bien el contenido XML y permite realizar validaciones en todos los lenguajes cliente
- XSD tiene un buen versionado y presenta una sólida compatibilidad hacia atrás
- XSD se puede traducir a código Java usando XJC
El último punto es importante. Al comunicarnos con un sistema externo mediante mensajes XML, queremos asegurarnos de que nuestros mensajes sean válidos. Esto se puede lograr fácilmente con JAXB, XJC y XSD. Sería una locura esperar que, con un enfoque de diseño 'Java primero', donde creamos nuestros mensajes como objetos Java, se pudieran mapear de manera clara a XML y enviarse para ser consumidos por otro sistema. El XML generado de esta manera tendría una calidad muy baja, no estaría documentado y sería difícil de desarrollar. Si hubiera un acuerdo sobre el nivel de calidad del servicio (SLA) para tal interfaz, lo habríamos estropeado de inmediato.
Para ser honesto, esto es lo que constantemente sucede con las API en JSON, pero esa es otra historia, la próxima vez me quejaré...
Bases de datos: es lo mismo.
Al trabajar con bases de datos, usted comprende que todas son, en principio, similares. Una base de datos posee sus propios datos y debe gestionar su esquema. Cualquier modificación realizada en el esquema debe implementarse directamente en DDL, para que se actualice la única fuente de verdad.
Cuando se actualiza la fuente, todos los clientes también deben actualizar sus copias del modelo. Algunos clientes pueden estar escritos en Java usando jOOQ y Hibernate o JDBC (o todos a la vez). Otros clientes pueden estar escritos en Perl (les queda desearles suerte), los terceros, en C#. No importa. El modelo principal se encuentra en la base de datos. Los modelos generados mediante ORM suelen ser de mala calidad, poco documentados y difíciles de desarrollar.
Así que no cometas errores. Desde el principio, no cometas errores. Trabaja desde la base de datos. Construye un canal de despliegue que pueda ser automatizado. Incluye generadores de código para facilitar la copia del modelo de tu base de datos y su transferencia a los clientes. Y deja de preocuparte por los generadores de código. Son buenos. Con ellos serás más productivo. Solo necesitas dedicar un poco de tiempo al principio para configurarlos, y luego te esperan años de mayor productividad, de los que se compone la historia de tu proyecto.
No me agradezcas todavía, después.
Explicación
Para mayor claridad: Este artículo no promueve de ninguna manera que toda la sistema (es decir, el dominio del problema, la lógica empresarial, etc.) deba ajustarse al modelo de su base de datos. En este artículo, hablo sobre cómo el código del cliente que interactúa con la base de datos debe actuar, basándose en el modelo de la base de datos, de modo que el propio código no reproduzca el modelo de la base de datos en un estado de «primera clase». Esta lógica generalmente se ubica en el nivel de acceso a los datos en su cliente.
En arquitecturas de dos niveles, que todavía se conservan en algunos lugares, este modelo de sistema puede ser la única opción posible. Sin embargo, en la mayoría de los sistemas, el nivel de acceso a los datos me parece una «sub-sistema» que encapsula el modelo de la base de datos.
Excepciones
De cualquier regla hay excepciones, y ya he mencionado que el enfoque con la primacía de la base de datos y la generación de código fuente a veces puede resultar inapropiado. Aquí hay un par de tales excepciones (probablemente haya otras):
- Cuando la estructura es desconocida y debe ser descubierta. Por ejemplo, usted es un proveedor de herramientas que ayuda a los usuarios a orientarse en cualquier esquema. Uf. Aquí no hay forma de generar código. Pero aun así, la base de datos es primordial.
- Cuando la estructura debe generarse sobre la marcha para resolver alguna tarea. Este ejemplo parece una versión un poco extravagante del patrón , es decir, realmente no tiene una estructura claramente definida. En este caso, a menudo ni siquiera se puede estar seguro de que le convenga un RDBMS.
Las excepciones, por su naturaleza, son excepcionales. En la mayoría de los casos relacionados con el uso de RDBMS, la estructura se conoce de antemano, está dentro del RDBMS y es la única fuente de «verdad», y todos los clientes deben obtener copias derivadas de ella. Idealmente, se debe utilizar un generador de código en este caso.
Fuente: habr.com
