Columnas Definidas en Tiempo de Ejecución Con asentinel-orm

asentinel-orm es una herramienta ORM ligera construida sobre Spring JDBC, particularmente JdbcTemplate. Por lo tanto, posee la mayoría de las características que se esperarían de un ORM básico, como generación de SQL, carga perezosa, etc.

Al aprovechar el JdbcTemplate, significa que permite participar en transacciones gestionadas por Spring, y se puede integrar fácilmente en cualquier proyecto que ya utilice JdbcTemplate como medio para interactuar con la base de datos.

Desde 2015, asentinel-orm se ha utilizado con éxito en varias aplicaciones y se ha mejorado continuamente según las necesidades del negocio. En el verano de 2024, se convirtió oficialmente en un proyecto de código abierto, lo que consideramos acelerará su evolución y aumentará el número de colaboradores.

En este artículo, se construye una aplicación de ejemplo para destacar varias características clave de ORM:

  • Configuración simple
  • Modelado de entidades de dominio directo a través de anotaciones personalizadas
  • Escritura fácil y ejecución segura de sentencias SQL simples
  • Generación automática de sentencias SQL
  • Esquema dinámico (las entidades se enriquecen con atributos adicionales en tiempo de ejecución, se persisten y se leen sin cambios en el código)

Aplicación

Configuración

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • Base de datos H2

Configuración

Para interactuar con el asentinel-orm y aprovechar sus funcionalidades, se requiere una instancia de OrmOperations .

Como se indica en el JavaDoc, esta es la interfaz central para realizar operaciones ORM, y no está destinada ni es necesaria ser implementada específicamente en el código del cliente.

La aplicación de muestra incluye el código de configuración para crear un bean de este tipo.

Java

 

OrmOperations tiene dos super interfaces:

  • SqlBuilderFactory – crea instancias de SqlBuilder que pueden ser utilizadas para crear consultas SQL. SqlBuilder es capaz de generar automáticamente partes de la consulta, por ejemplo, la que selecciona las columnas. La cláusula where, la cláusula order by, otras condiciones y las columnas reales pueden ser agregadas también usando métodos de la clase SqlBuilder. En la siguiente parte de esta sección, se muestra un ejemplo de consulta generada por un SqlBuilder.
  • Updater – utilizado para guardar entidades en sus respectivas tablas de la base de datos. Puede realizar inserciones o actualizaciones dependiendo de si la entidad es nueva o ya existe. Existe una interfaz de estrategia llamada NewEntityDetector , que se utiliza para determinar si una entidad es nueva. Por defecto, se utiliza el SimpleNewEntityDetector.

Todas las consultas generadas por el ORM se ejecutan utilizando una instancia de SqlQueryTemplate , que además necesita un JdbcOperations/JdbcTemplate de Spring para funcionar. Finalmente, todas las consultas llegan al viejo y querido JdbcTemplate a través del cual se ejecutan mientras participan en transacciones de Spring, al igual que cualquier JdbcTemplate ejecución directa.

Las construcciones y lógica SQL específicas de la base de datos se proporcionan a través de implementaciones de la interfaz JdbcFlavor, que se inyectan en la mayoría de los beans mencionados anteriormente. En este artículo, dado que se utiliza una base de datos H2, se configura una implementación de H2JdbcFlavor.

La configuración completa del ORM como parte de la aplicación de muestra es OrmConfig.

Implementación

El modelo de dominio experimental expuesto por la aplicación de muestra es sencillo y consta de dos entidades: fabricantes de automóviles y modelos de automóviles. Representando exactamente lo que sus nombres denotan, la relación entre ellos es obvia: un fabricante de automóviles puede poseer múltiples modelos de automóviles.

Además de su nombre, el fabricante de automóviles se enriquece con atributos (columnas) que son ingresados por el usuario de la aplicación dinámicamente en tiempo de ejecución. El caso de uso ejemplificado es directo:

  • Se solicita al usuario que proporcione los nombres y tipos deseados para los atributos dinámicos
  • Se crean un par de fabricantes de automóviles con valores concretos para los atributos dinámicos previamente añadidos, y luego
  • Las entidades se cargan nuevamente descritas por los atributos iniciales y los atributos definidos en tiempo de ejecución

Las entidades iniciales se mapean utilizando las tablas de la base de datos a continuación:

SQL

 

Las clases de dominio correspondientes están decoradas con anotaciones específicas de ORM para configurar los mapeos a las tablas de base de datos anteriores.

Java

 

Java

 

Algunas consideraciones:

  • @Table – mapea (asocia) la clase a una tabla de base de datos
  • @PkColumn – mapea el id (identificador único) a la clave primaria de la tabla
  • @Column – mapea un miembro de la clase a una columna de la tabla 
  • @Child – define la relación con otra entidad
  • @Child miembros anotados – configurados para ser cargados de forma perezosa
  • type columna de tabla – mapeada a un campo enumCarType

Para que la clase CarManufacturer soporte atributos definidos en tiempo de ejecución (mapeados a columnas de tabla definidas en tiempo de ejecución), se define una subclase como la de abajo:

Java

 

Esta clase almacena los atributos definidos en tiempo de ejecución (campos) en un Map. La interacción entre los valores de campo en tiempo de ejecución y el ORM se realiza a través de la implementación de la interfaz DynamicColumnEntity .

Java

 

  • setValue() – se utiliza para establecer el valor de la columna definida en tiempo de ejecución cuando se lee de la tabla
  • getValue() – se utiliza para recuperar el valor de una columna definida en tiempo de ejecución cuando se guarda en la tabla

El DynamicColumn mapea atributos definidos en tiempo de ejecución a sus columnas correspondientes de manera similar a como la anotación @Column mapea miembros conocidos en tiempo de compilación.

Al ejecutar la aplicación, se ejecuta el CfRunner. Se le pide al usuario que ingrese nombres y tipos para los atributos dinámicos personalizados deseados que enriquecen la entidad CarManufacturer  (por simplicidad, solo se admiten los tipos int y varchar ). 

Para cada par nombre-tipo, se ejecuta un comando DML para que las nuevas columnas puedan ser añadidas a la tabla de la base de datos CarManufacturer . El siguiente método (declarado en CarService) realiza la operación.

Java

 

Cada atributo de entrada se registra como un DefaultDynamicColumn, una implementación de referencia de DynamicColumn .

Una vez que todos los atributos están definidos, se añaden dos fabricantes de automóviles a la base de datos, ya que el usuario proporciona valores para cada uno de esos atributos.

Java

 

El siguiente método (declarado en CarService) crea realmente la entidad a través del ORM.

Java

 

La versión de 2 parámetros del método OrmOperations update() se llama, lo que permite pasar una instancia de UpdateSettings y comunicar al ORM durante la ejecución que hay valores definidos en tiempo de ejecución que deben ser persistidos.

Por último, se crean dos modelos de coche, correspondientes a uno de los fabricantes de coches añadidos previamente.

Java

 

El método a continuación (declarado en CarService) crea realmente las entidades a través del ORM, esta vez utilizando el método OrmOperations update() para persistir entidades sin atributos dinámicos. Para mayor comodidad, se crean múltiples entidades en una sola llamada.

Java

 

Como último paso, uno de los fabricantes creados se carga de nuevo por su nombre utilizando una consulta generada por el ORM.

Java

 

Vale la pena hacer algunas aclaraciones sobre el método definido anteriormente.

El método OrmOperations newSqlBuilder() crea una instancia de SqlBuilder, y como su nombre indica, esto se puede usar para generar consultas SQL. El método SqlBuilder select() genera la parte select from table de la consulta, mientras que el resto (where, order by) debe ser añadido. La parte select de la consulta se puede personalizar pasando instancias de EntityDescriptorNodeCallback (los detalles sobre EntityDescriptorNodeCallback pueden ser objeto de un artículo futuro).

Para que el ORM sepa que el plan es seleccionar y mapear columnas definidas en tiempo de ejecución, es necesario pasar un DynamicColumnsEntityNodeCallback . Junto con esto, se proporciona un AutoEagerLoader  para que el ORM entienda que debe cargar ansiosamente la lista de CarModels  asociadas con el fabricante. Sin embargo, esto no tiene nada que ver con los atributos definidos en tiempo de ejecución, pero demuestra cómo se puede cargar ansiosamente un miembro hijo.

Conclusión

Si bien probablemente haya otras formas de trabajar con columnas definidas en tiempo de ejecución cuando los datos se almacenan en bases de datos relacionales, el enfoque presentado en este artículo tiene la ventaja de utilizar columnas de base de datos estándar que se leen/escriben utilizando consultas SQL estándar generadas directamente por el ORM.

No fue raro cuando tuvimos la oportunidad de discutir en “la comunidad” el asentinel-orm, las razones por las que tuvimos que desarrollar tal herramienta. Por lo general, a primera vista, los desarrolladores demostraron ser reacios y reservados cuando se trataba de un ORM hecho a medida, preguntando por qué no usar Hibernate u otras implementaciones de JPA.

En nuestro caso, el principal impulsor fue la necesidad de una forma rápida, flexible y fácil de trabajar con a veces un número bastante grande de atributos (columnas) definidos en tiempo de ejecución para entidades que forman parte del dominio empresarial. Para nosotros, demostró ser el camino correcto. Las aplicaciones están funcionando sin problemas en producción, los clientes están contentos con la velocidad y el rendimiento alcanzado, y los desarrolladores se sienten cómodos y creativos con la API intuitiva.

Como el proyecto ahora es de código abierto, es muy fácil para cualquier persona interesada echar un vistazo, formarse una opinión objetiva al respecto y, ¿por qué no?, bifurcarlo, abrir un PR y contribuir.

Recursos

  • El proyecto ORM de código abierto está aquí.
  • El código fuente de la aplicación de muestra está aquí.

Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm