Celesta 7.x: ORM, migración y pruebas "en un paquete"

Quizás ya sepa algo sobre la biblioteca de código abierto Celesta . Si no, no importa, ahora te contaremos todo. Pasó otro año, se lanzó la versión 7.x, muchas cosas habían cambiado, y era hora de resumir los cambios y, al mismo tiempo, recordar qué es Celesta en general.



Si aún no ha escuchado nada sobre Celesta, y al leer este artículo desea saber para qué tareas comerciales es más efectiva su aplicación, puedo recomendar la primera parte de la publicación anterior o este video de media hora (excepto las palabras sobre el uso del lenguaje Python). Pero mejor aún, lea este artículo primero. Comenzaré con los cambios que ocurrieron en la versión 7, y luego repasaré un ejemplo técnico completo del uso de la versión moderna de Celesta para escribir un pequeño servicio de back-end para una aplicación Java usando Spring Boot.


¿Qué ha cambiado en la versión 7.x?


  1. Nos negamos a usar Jython como un lenguaje integrado en Celesta. Si antes comenzamos a hablar de Celesta con el hecho de que la lógica de negocios está escrita en Python, ahora ... cualquier lenguaje Java puede servir como lenguaje de lógica de negocios: Java, Groovy, JRuby o el mismo Jython. Ahora Celesta no llama al código de lógica de negocios, pero el código de lógica de negocios usa a Celesta y sus clases de acceso a datos como la biblioteca Java más común. Sí, debido a esto se violó la compatibilidad con versiones anteriores, pero este es el precio que estábamos dispuestos a pagar. Desafortunadamente, nuestra apuesta por Jython perdió. Cuando comenzamos a usar Jython hace unos años, era un proyecto animado y prometedor, pero a lo largo de los años su desarrollo se ralentizó, la acumulación de la especificación del lenguaje se acumuló, los problemas de compatibilidad para la mayoría de las bibliotecas de pip no se resolvieron. La gota que colmó el vaso fue la aparición de nuevos errores en los últimos lanzamientos de idiomas, que se manifestaron al trabajar en una carga de producción. Nosotros mismos no tenemos los recursos para apoyar el proyecto Jython, y decidimos separarnos de él. Celesta ya no depende de Jython.
  2. Las clases de acceso a datos ahora se generan en código en Java (y no en Python, como antes) utilizando el complemento Maven. Y debido a que pasamos de la escritura dinámica a la escritura estática debido a esto, hubo más oportunidades para refactorizar y se hizo más fácil escribir código subjetivamente correcto.
  3. Apareció la extensión para JUnit5, por lo que se volvió muy conveniente escribir pruebas de lógica que funcionen con la base de datos en JUnit5 (que se discutirá más adelante).
  4. Ha aparecido un proyecto separado: spring-boot-starter-celesta , que, como su nombre lo indica, es el iniciador Celesta en Spring Boot. La capacidad de empaquetar aplicaciones Celesta en servicios Spring Boot fácilmente implementables compensó la pérdida de la capacidad de actualizar la aplicación en el servidor simplemente cambiando la carpeta con scripts Python.
  5. Transferimos toda la documentación de la Wiki al formato AsciiDoctor , la colocamos en el control de versiones junto con el código, y ahora tenemos documentación actualizada para cada versión de Celesta. Para la última versión, la documentación en línea está disponible aquí: https://courseorchestra.imtqy.com/celesta/
  6. A menudo se nos preguntó si es posible utilizar la migración de la base de datos a través de DDL idempotente por separado de Celesta. Ahora existe esa oportunidad usando la herramienta 2bass .

¿Qué es Celesta y qué puede hacer ella?


En pocas palabras, Celesta es:


  • una capa intermedia entre la base de datos relacional y el código de lógica de negocios, basada en el enfoque de diseño de la primera base de datos ,
  • mecanismo de migración de estructura de base de datos,
  • marco para probar código que funciona con datos.

Admitimos cuatro tipos de bases de datos relacionales: PostgreSQL, MS SQL Server, Oracle y H2.


Características clave de Celesta:


  1. Un principio muy similar al principio básico de Java: "Escribir una vez, ejecutar en todos los RDBMS compatibles". El código de lógica de negocios no sabe en qué tipo de base de datos se ejecutará. Puede escribir el código de lógica de negocios y ejecutarlo en MS SQL Server, luego cambiar a PostgreSQL, y esto sucederá sin complicaciones (bueno, casi :)
  2. Reestructuración automática en una base de datos en vivo. La mayor parte del ciclo de vida de los proyectos de Celesta ocurre cuando la base de datos de trabajo ya está allí y está llena de datos que deben guardarse, pero también es necesario cambiar constantemente su estructura. Una de las características clave de Celesta es la capacidad de "ajustar" automáticamente la estructura de la base de datos a su modelo de datos.
  3. Pruebas Se presta mucha atención para garantizar que el código de Celesta sea comprobable, de modo que podamos probar automáticamente los métodos que modifican los datos en la base de datos, haciendo esto de manera fácil, rápida y elegante, sin usar herramientas externas como DbUnit y contenedores.

¿Por qué necesita independencia del tipo de DBMS?


La independencia del código de lógica de negocios del tipo de DBMS no fue el primer punto que pusimos: el código escrito para Celesta no sabe en absoluto en qué DBMS se está ejecutando. Por qué


En primer lugar, debido a que la elección de un tipo de DBMS no es un problema tecnológico, sino político. Al llegar a un nuevo cliente comercial, a menudo descubrimos que él ya tiene un tipo favorito de DBMS en el que se invierten los fondos, y el cliente quiere ver otras soluciones en la infraestructura existente. El panorama tecnológico está cambiando: PostgreSQL se encuentra cada vez más en agencias gubernamentales y empresas privadas, aunque MS SQL Server prevaleció en nuestra práctica hace unos años. Celesta admite los DBMS más comunes, y no estamos preocupados por estos cambios.


En segundo lugar, me gustaría transferir el código ya creado para resolver problemas estándar de un proyecto a otro, para crear una biblioteca reutilizable. Cosas como directorios jerárquicos o módulos de distribución de notificaciones por correo electrónico son inherentemente estándar, y ¿por qué necesitamos admitir múltiples versiones para clientes con diferentes relaciones?


En tercer lugar, por último, pero no menos importante, la capacidad de ejecutar pruebas unitarias sin usar DbUnit y contenedores utilizando una base de datos H2 en memoria. En este modo, la base H2 comienza instantáneamente. Celesta crea muy rápidamente un esquema de datos en él, después de lo cual puede realizar las pruebas necesarias y "olvidar" la base de datos. Dado que el código de lógica empresarial realmente no sabe sobre qué base se ejecuta, entonces, si funciona sin errores en H2, sin errores funcionará en PostgreSQL. Por supuesto, la tarea de los desarrolladores del sistema Celesta es hacer todas las pruebas utilizando DBMS reales para asegurarse de que nuestra plataforma realice su API por igual en diferentes relaciones. Y lo hacemos Pero el desarrollador de lógica de negocios ya no es necesario.


CelestaSQL


¿Cómo se logra el cross-basamentism? Por supuesto, a costa de trabajar con datos solo a través de una API especial que aísla la lógica de cualquier información específica de la base de datos. Celesta genera clases Java para acceder a datos, por un lado, y código SQL y algunos objetos auxiliares dentro de la base de datos, por otro lado.


Celesta no proporciona mapeo relacional de objetos en su forma más pura, porque al diseñar un modelo de datos, no venimos de clases, sino de la estructura de la base de datos. Es decir, primero construimos un modelo ER de tablas, y luego, en base a este modelo, Celesta genera clases de cursor para acceder a los datos.


Es posible lograr el mismo trabajo en todos los DBMS compatibles solo para la funcionalidad que se implementa aproximadamente de manera equitativa en cada uno de ellos. Si representamos condicionalmente el conjunto de capacidades funcionales de cada una de las bases que apoyamos en forma de "círculos de Euler", obtenemos la siguiente imagen:



Si brindamos total independencia del tipo de base de datos, entonces la funcionalidad que abrimos para los programadores de lógica de negocios debería estar dentro de la intersección de todas las bases. A primera vista, parece que esta es una limitación significativa. Sí: algunas características específicas, por ejemplo, no podemos usar SQL Server. Pero sin excepción, las bases de datos relacionales admiten tablas, claves externas, vistas, secuencias, consultas SQL con JOIN y GROUP BY. En consecuencia, podemos dar estas oportunidades a los desarrolladores. Proporcionamos a los desarrolladores "SQL despersonalizado", que llamamos "CelestaSQL", y en el proceso generamos consultas SQL para dialectos de las bases de datos correspondientes.


El lenguaje CelestaSQL incluye DDL para definir objetos de base de datos y consultas SELECT para vistas y filtros, pero no contiene comandos DML: los cursores se utilizan para modificar los datos, que aún deben discutirse.


Cada base de datos tiene su propio conjunto de tipos de datos. CelestaSQL también tiene su propio conjunto de tipos. Al momento de escribir, hay nueve de ellos, y esta tabla los compara con tipos reales en varias bases de datos y tipos de datos Java.


Puede parecer que nueve tipos no son suficientes (en comparación con lo que admite PostgreSQL, por ejemplo), pero en realidad estos son los tipos que son suficientes para almacenar información financiera, comercial y logística: cadenas, enteros, fraccionales , fechas, valores booleanos y blobs son siempre suficientes para representar dichos datos.


El lenguaje CelestaSQL en sí mismo se describe en la documentación con una gran cantidad de diagramas de sintaxis.


Modificación de la estructura de la base de datos. DDL idempotente


Otra característica clave de Celesta es su enfoque para migrar la estructura de la base de datos de trabajo a medida que se desarrolla el proyecto. Para hacer esto, se utiliza el enfoque integrado en Celesta usando DDL idempotente.


En pocas palabras, cuando escribimos en CelestaSQL el siguiente texto:


CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); 

- este texto no es interpretado por Celesta como "crear una tabla, pero si ya hay una tabla, entonces dar un error", sino "llevar la tabla a la estructura deseada". Es decir: "si no hay una tabla, créela, si hay una tabla, vea qué campos hay en ella, con qué tipos, qué índices, qué claves externas, qué valores predeterminados, etc., y si es necesario cambiar algo en esta tabla para llevarlo al tipo correcto ".


Con este enfoque, implementamos la capacidad de refactorizar y guiones de control de versiones para determinar la estructura de la base de datos:


  • vemos en el script la "imagen deseada" actual de la estructura,
  • qué, por quién y por qué en la estructura ha cambiado con el tiempo, podemos mirar a través del sistema de control de versiones,
  • En cuanto a los comandos ALTER, Celesta los genera y ejecuta automáticamente "bajo el capó" según sea necesario.

Por supuesto, este enfoque tiene sus limitaciones. Celesta hace todo lo posible para garantizar que la migración automática sea sencilla y sin problemas, pero esto no es posible en todos los casos. La motivación, las posibilidades y las limitaciones de este enfoque se describieron en esta publicación (su versión en inglés también está disponible).


Para acelerar el proceso de verificación / actualización de la estructura de la base de datos, Celesta aplica el almacenamiento de las sumas de verificación del script DDL en la base de datos (hasta que se cambie la suma de verificación, el proceso de verificación y actualización de la estructura de la base de datos no se inicia). Para que el proceso de actualización continúe sin problemas relacionados con el orden de cambio de los objetos que dependen uno del otro, se aplica la clasificación topológica de dependencias entre esquemas por claves externas. El proceso de migración automática se describe con más detalle en la documentación .


Crear un proyecto de Celesta y un modelo de datos


El proyecto de demostración, que consideraremos, está disponible en el github . Veamos cómo puede usar Celesta al escribir una aplicación Spring Boot. Estas son las dependencias de Maven que necesita:


  • org.springframework.boot:spring-boot-starter-web y ru.curs:spring-boot-starter-celesta (para más detalles, ru.curs:spring-boot-starter-celesta documentación).
  • Si no está utilizando Spring Boot, puede conectar la ru.curs:celesta-system-services directamente.
  • Para la generación de código de clases de acceso a datos basadas en scripts Celesta-SQL, ru.curs:celesta-maven-plugin necesita ru.curs:celesta-maven-plugin : el código fuente para un ejemplo de demostración o documentación describe cómo conectarlo.
  • Para aprovechar la capacidad de escribir pruebas unitarias JUnit5 para métodos que modifican datos, debe conectar ru.curs:celesta-unit en el alcance de la prueba.

Ahora cree un modelo de datos y compile clases de acceso a datos.


Digamos que estamos haciendo un proyecto para una empresa de comercio electrónico que recientemente se fusionó con otra empresa. Cada uno tiene su propia base de datos. Recopilan pedidos, pero hasta que fusionen sus bases de datos, necesitan un único punto de entrada para recopilar pedidos desde el exterior.


La implementación de este "punto de entrada" debería ser bastante tradicional: un servicio HTTP con operaciones CRUD que almacena datos en una base de datos relacional.


Debido al hecho de que Celesta implementa el enfoque de diseño de la base de datos primero, primero necesitamos crear una estructura de tabla que almacene los pedidos. Un pedido, como sabe, es una entidad compuesta: consiste en un encabezado donde se almacena la información sobre el cliente, la fecha del pedido y otros atributos del pedido, así como muchas líneas (artículos básicos).


Entonces, para el trabajo: crear


  • src/main/celestasql : de forma predeterminada, esta es la ruta a los scripts del proyecto CelestaSQL
  • Contiene subcarpetas que repiten la estructura de carpetas de los paquetes de Java ( ru/curs/demo en nuestro caso).
  • en la carpeta del paquete, cree un archivo .sql con el siguiente contenido:

 CREATE SCHEMA demo VERSION '1.0'; /** */ CREATE TABLE OrderHeader( id VARCHAR(30) NOT NULL, date DATETIME, customer_id VARCHAR(30), /**  */ customer_name VARCHAR(50), manager_id VARCHAR(30), CONSTRAINT Pk_OrderHeader PRIMARY KEY (id) ); /** */ CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); ALTER TABLE OrderLine ADD CONSTRAINT fk_OrderLine FOREIGN KEY (order_id) REFERENCES OrderHeader(id); CREATE VIEW OrderedQty AS SELECT item_id, sum(qty) AS qty FROM OrderLine GROUP BY item_id; 

Aquí describimos dos tablas conectadas por una clave externa, y una vista que devolverá una cantidad de resumen para los productos presentes en todos los pedidos. Como puede ver, esto no es diferente del SQL normal, con la excepción del comando CREATE SCHEMA , en el que CREATE SCHEMA la demo esquema de demo (para ver cómo el número de versión afecta la migración automática, consulte la documentación ). Pero también hay características. Por ejemplo, todos los nombres de las tablas y campos que usamos solo pueden ser de tal manera que se puedan convertir en nombres válidos de clase y variable en el lenguaje Java. Por lo tanto, espacios, caracteres especiales están excluidos. También puede observar que en los comentarios que colocamos sobre los nombres de las tablas y algunos de los campos, no comenzamos con / *, como de costumbre, sino con / **, cómo comienzan los comentarios JavaDoc, ¡y esto no es accidental! Un comentario definido sobre una entidad que comience con / ** estará disponible en tiempo de ejecución en la propiedad .getCelestaDoc() de esa entidad. Esto es útil cuando queremos proporcionar elementos de la base de datos con metainformación adicional: por ejemplo, nombres de campos legibles por humanos, información sobre cómo representar campos en la interfaz de usuario, etc.


El script CelestaSQL sirve dos tareas igualmente importantes: en primer lugar, para el despliegue / modificación de la estructura de una base de datos relacional, y en segundo lugar, para la generación de código de clases de acceso a datos.


Ahora podemos generar clases de acceso a datos, simplemente ejecute el mvn generate-sources o, si está trabajando en IDEA, haga clic en el botón 'Generar fuentes y actualizar carpetas' en el panel de control de Maven. En el segundo caso, IDEA " target/generated-sources/celesta carpeta creada en target/generated-sources/celesta y hace que su contenido esté disponible para su importación en los códigos fuente del proyecto. El resultado de la generación de código será el siguiente: una clase para cada objeto en la base de datos:



La conexión a la base de datos se especifica en la configuración de la aplicación, en nuestro caso, en el archivo src/main/resources/application.yml . Al usar spring-boot-starter-celesta, IDEA le indicará las opciones de código disponibles en la finalización del código.


Si no queremos molestarnos con el RDBMS "real" con fines de demostración, podemos hacer que Celesta trabaje con la base de datos H2 incorporada en modo memoria utilizando la siguiente configuración:


 celesta: h2: inMemory: true 

Para conectar una base de datos "real", cambie la configuración a algo como


 celesta: jdbc: url: jdbc:postgresql://127.0.0.1:5432/celesta username: <your_username> password: <your_password> 

(en este caso, también deberá agregar un controlador JDBC PostgreSQL a su aplicación a través de la dependencia de Maven).


Cuando inicia una aplicación Celesta con una conexión a un servidor de base de datos, puede observar que las tablas, vistas, índices, etc. necesarios se crean para una base de datos vacía, y para una no vacía, se actualizan a las estructuras especificadas en el DDL.


Crear métodos de manipulación de datos.


Una vez que haya descubierto cómo crear una estructura de base de datos, puede comenzar a escribir la lógica empresarial.


Para poder implementar los requisitos para distribuir los derechos de acceso y las acciones de registro, cualquier operación de datos en Celesta se realiza en nombre de un usuario, no hay operaciones "anónimas". Por lo tanto, cualquier código Celesta se ejecuta en el contexto de la llamada descrita en la clase CallContext .


  • Antes de comenzar una operación que puede modificar datos en la base de datos, CallContext activa CallContext .
  • En el momento de la activación, se toma una conexión a la base de datos del grupo de conexiones y comienza la transacción.
  • Una vez CallContext la operación CallContext ejecuta commit() si la operación fue exitosa o rollback() si se produjo una excepción no controlada durante la ejecución, CallContext cierra y la conexión de la base de datos se devuelve al grupo.

Si usamos spring-boot-starter-celesta, estas acciones se realizan automáticamente para todos los métodos anotados por @CelestaTransaction .


Supongamos que queremos escribir un controlador que guarde el documento en la base de datos. Su código de nivel de controlador podría verse así:


 @RestController @RequestMapping("/api") public class DocumentController { private final DocumentService srv; public DocumentController(DocumentService srv) { this.srv = srv; } @PutMapping("/save") public void saveOrder(@RequestBody OrderDto order) { CallContext ctx = new CallContext("user1"); //new SystemCallContext(); srv.postOrder(ctx, order); } 

Como regla general, en el nivel del método del controlador (es decir, cuando la autenticación ya ha pasado), conocemos la ID de usuario y podemos usarla al crear el CallContext . La vinculación de un usuario a un contexto determina los permisos para acceder a las tablas y también proporciona la capacidad de registrar los cambios realizados en su nombre. Es cierto, en este caso, para la operatividad del código que interactúa con la base de datos, los derechos para el usuario "usuario1" deben indicarse en las tablas del sistema . Si no desea utilizar el sistema de distribución de acceso Celesta y otorgar al contexto de la sesión todos los derechos sobre cualquier tabla, puede crear un objeto SystemCallContext .


El método para guardar la factura en el nivel de servicio puede verse así:


 @Service public class DocumentService { @CelestaTransaction public void postOrder(CallContext context, OrderDto doc) { try (OrderHeaderCursor header = new OrderHeaderCursor(context); OrderLineCursor line = new OrderLineCursor(context)) { header.setId(doc.getId()); header.setDate(Date.from(doc.getDate().atStartOfDay(ZoneId.systemDefault()).toInstant())); header.setCustomer_id(doc.getCustomerId()); header.setCustomer_name(doc.getCustomerName()); header.insert(); int lineNo = 0; for (OrderLineDto docLine : doc.getLines()) { lineNo++; line.setLine_no(lineNo); line.setOrder_id(doc.getId()); line.setItem_id(docLine.getItemId()); line.setQty(docLine.getQty()); line.insert(); } } } 

Tenga en cuenta la anotación @CelestaTransaction . Gracias a él, el objeto proxy DocumentService realizará todas esas acciones de servicio con el parámetro CallContext ctx descrito anteriormente. Es decir, al comienzo de la ejecución del método, ya estará vinculado a la conexión de la base de datos y la transacción estará lista para comenzar. Podemos centrarnos en escribir lógica de negocios. En nuestro caso, leer el objeto OrderDto y guardarlo en la base de datos.


Para hacer esto, usamos los llamados cursores, clases generadas usando el celesta-maven-plugin . Ya hemos visto lo que son. Se crea una clase para cada uno de los objetos de esquema: dos tablas y una vista. Y ahora podemos usar estas clases para acceder a los objetos de la base de datos en nuestra lógica de negocios.


Para crear un cursor en la tabla de pedidos y seleccionar el primer registro, debe escribir el siguiente código:


 OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst(); 

Después de crear el objeto de encabezado, podemos acceder a los campos de la entrada de la tabla a través de getters y setters:



Al crear un cursor, debemos usar el contexto de llamada activa; esta es la única forma de crear un cursor. El contexto de la llamada lleva información sobre el usuario actual y sus derechos de acceso.


Con el objeto cursor, podemos hacer diferentes cosas: filtrar, revisar los registros y, naturalmente, insertar, eliminar y actualizar registros. Toda la API del cursor se describe en detalle en la documentación .


Por ejemplo, el código de nuestro ejemplo podría desarrollarse de la siguiente manera:


 OrderHeaderCursor header = new OrderHeaderCursor(context); header.setRange("manager_id", "manager1"); header.tryFirst(); header.setCounter(header.getCounter() + 1); header.update(); 

En este ejemplo, establecemos el filtro por el campo manager_id, luego encontramos el primer registro usando el método tryFirst.


(por qué "intentar")

Los métodos get , first , insert , update tienen dos opciones: sin el prefijo try (solo get(...) , etc.) y con el prefijo try ( tryGet(...) , tryFirst() , etc.) . Los métodos sin el prefijo try arrojan una excepción si la base de datos no tiene los datos apropiados para realizar la acción. Por ejemplo, first () arrojará una excepción si ningún registro ingresa al conjunto de filtros en el cursor. Al mismo tiempo, los métodos con el prefijo try no arrojan una excepción, sino que devuelven un valor booleano que indica el éxito o el fracaso de la operación correspondiente. La práctica recomendada es utilizar métodos sin el prefijo try siempre que sea posible. De esta manera, se crea un código de "autocomprobación", que señala errores a tiempo en la lógica y / o los datos de la base de datos.


Cuando tryFirst activa tryFirst , las variables del tryFirst se llenan con los datos de un registro, podemos leer y asignarles valores. Y cuando los datos en el cursor están completamente preparados, ejecutamos update() , y almacena el contenido del cursor en la base de datos.


¿Qué problema podría verse afectado este código? Por supuesto, la aparición de condición de carrera / actualización perdida. Porque entre el momento en que recibimos los datos en la línea con "tryFirst" y el momento en que intentamos actualizar estos datos en el punto de "actualización", alguien más ya puede recibir, modificar y actualizar estos datos en la base de datos. Después de leer los datos, el cursor no bloquea su uso por otros usuarios. Para protegerse contra las actualizaciones perdidas, Celesta utiliza el principio de bloqueo optimista. En cada tabla, por defecto, Celesta crea un campo de recversion , y en el nivel ON del activador UPDATE incrementa el número de versión y verifica que los datos actualizados tengan la misma versión que la tabla. Si ocurre un problema, lanza una excepción. Puede leer más sobre esto en el artículo del artículo " Protección contra actualizaciones perdidas ".


Recuerde nuevamente que una transacción está asociada con el objeto CallContext. Si el procedimiento Celesta tiene éxito, se produce una confirmación. Si el método Celesta termina con una excepción no controlada, se produce la reversión. Por lo tanto, si se produce un error en algún procedimiento complicado, la transacción completa relacionada con el contexto de la llamada se revierte, como si no hubiéramos comenzado a hacer nada con los datos, los datos no están dañados. Si por alguna razón necesita una confirmación en medio de, digamos, algún tipo de procedimiento grande, entonces se puede ejecutar una confirmación explícita llamando a context.commit() .


Métodos de prueba de datos


OrderDto una prueba unitaria que verifique la corrección del método de servicio que almacena OrderDto en la base de datos.


Cuando se usa JUnit5 y la extensión para JUnit5 disponible en el módulo de celesta-unit , esto es muy fácil. La estructura de la prueba es la siguiente:


 @CelestaTest public class DocumentServiceTest { DocumentService srv = new DocumentService(); @Test void documentIsPutToDb(CallContext context) { OrderDto doc =... srv.postOrder(context, doc); //Check the fact that records are in the database OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst(); assertEquals(doc.getId(), header.getId()); OrderLineCursor line = new OrderLineCursor(context); line.setRange("order_id", doc.getId()); assertEquals(2, line.count()); } } 

Gracias a la anotación @CelestaTest , que es una extensión para JUnit5, podemos declarar el parámetro de CallContext context en los métodos de prueba. Este contexto ya está activado y vinculado a la base de datos (en memoria H2) y, por lo tanto, no necesitamos envolver la clase de servicio en un proxy; lo creamos usando new y no usando Spring. Sin embargo, si es necesario, inyecte el servicio en la prueba usando las herramientas Spring, no hay obstáculos para esto.


Creamos pruebas unitarias bajo el supuesto de que en el momento de su ejecución la base de datos estará completamente vacía, pero con la estructura que necesitamos, y después de su ejecución no podemos preocuparnos por el hecho de que dejamos "basura" en la base de datos. Estas pruebas se realizan a una velocidad muy alta.


Creemos un segundo procedimiento que devuelva JSON con valores agregados que muestren cuántos productos pedimos.


La prueba escribe dos órdenes en la base de datos, después de lo cual verifica el valor total devuelto por el nuevo método getAggregateReport :


 @Test void reportReturnsAggregatedQuantities(CallContext context) { srv.postOrder(context, . . .); srv.postOrder(context, . . .); Map<String, Integer> result = srv.getAggregateReport(context); assertEquals(5, result.get("A").intValue()); assertEquals(7, result.get("B").intValue()); } 

Para implementar el método getAggregateReport usaremos la vista OrderedQty, que, recuerdo, en el archivo CelestaSQL se ve así:


 create view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id; 

La solicitud es estándar: resumimos las líneas de pedido por cantidad y las agrupamos por código de producto. Ya se ha creado un cursor OrderedQtyCursor para la vista, que podemos usar. Declaramos este cursor, iteramos sobre él y recogemos el Map<String, Integer> deseado:


 @CelestaTransaction public Map<String, Integer> getAggregateReport(CallContext context) { Map<String, Integer> result = new HashMap<>(); try (OrderedQtyCursor ordered_qty = new OrderedQtyCursor(context)) { for (OrderedQtyCursor line : ordered_qty) { result.put(ordered_qty.getItem_id(), ordered_qty.getQty()); } } return result; } 

Vistas materializadas de Celesta


¿Por qué es malo usar una vista para obtener datos agregados? Este enfoque es bastante viable, pero en realidad pone una bomba de tiempo en todo nuestro sistema: después de todo, una vista, que es una consulta SQL, se ejecuta más y más lentamente a medida que los datos se acumulan en el sistema. Tendrá que resumir y agrupar más y más líneas. ¿Cómo evitar esto?


Celesta intenta implementar todas las tareas estándar que los programadores de lógica de negocios enfrentan constantemente a nivel de plataforma.


MS SQL Server tiene el concepto de vistas materializadas (indexadas), que se almacenan como tablas y se actualizan rápidamente a medida que cambian los datos en las tablas de origen. Si estuviéramos trabajando en un servidor MS SQL "limpio", entonces para nuestro caso, el reemplazo de la vista con una vista indizada sería justo lo que necesitábamos: recuperar el informe agregado no se ralentizaría a medida que se acumularan los datos, y el trabajo para actualizar el informe agregado se realizaría en ese momento insertando datos en la tabla de líneas de orden y tampoco aumentaría mucho con un aumento en el número de filas.


Pero en caso de que trabajemos con PostgreSQL a través de Celesta, ¿qué podemos hacer? Redefina la vista agregando la palabra materializada:


 create materialized view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id; 

Comencemos el sistema y veamos qué pasó con la base de datos.


OrderedQty que la OrderedQty ha desaparecido y la tabla OrderedQty ha aparecido en su OrderedQty . Además, como la tabla OrderLine está llena de datos, la información en la tabla OrderedQty se actualizará "mágicamente", como si OrderedQty fuera una vista.


No hay magia aquí si miramos los desencadenantes creados en la tabla OrderLine . Celesta, habiendo recibido la tarea de crear una "vista materializada", analizó la consulta y creó desencadenantes en la tabla OrderLine que actualizan OrderedQty . Al insertar una sola palabra clave, materialized , en el archivo CelestaSQL, resolvimos el problema de la degradación del rendimiento, ¡y el código de lógica de negocios ni siquiera necesitó ser cambiado!


, , , . «» Celesta , , JOIN-, GROUP BY. , , , , . . .


Conclusión


Celesta. — .

Source: https://habr.com/ru/post/455746/


All Articles