¡Obtener datos con ORM es fácil! O no?


Introduccion


Casi cualquier sistema de información de una forma u otra interactúa con almacenes de datos externos. En la mayoría de los casos, esta es una base de datos relacional y, a menudo, se utiliza algún tipo de marco ORM para trabajar con datos. ORM elimina la mayoría de las operaciones de rutina, en su lugar ofrece un pequeño conjunto de abstracciones adicionales para trabajar con datos.


Martin Fowler publicó un artículo interesante, uno de los pensamientos clave allí: "Los ORM nos ayudan a resolver una gran cantidad de problemas en las aplicaciones empresariales ... Esta herramienta no puede llamarse bonita, pero los problemas con los que trata tampoco son agradables". Creo que ORM merece más respeto y más comprensión ".


Utilizamos ORM de forma muy intensiva en el marco de trabajo de CUBA , por lo que conocemos de primera mano los problemas y limitaciones de esta tecnología, ya que CUBA se usa en varios proyectos en todo el mundo. Hay muchos temas que pueden discutirse en relación con ORM, pero nos centraremos en uno de ellos: la elección entre los métodos de muestreo de datos "vagos" (vagos) y "codiciosos" (ansiosos). Hablaremos sobre diferentes enfoques para resolver este problema con ilustraciones de JPA API y Spring, y también describiremos cómo (y por qué exactamente) se usa ORM en CUBA y qué trabajo estamos haciendo para mejorar el trabajo con datos en nuestro marco.


Muestreo de datos: ¿perezoso o no?


Si su modelo de datos tiene solo una entidad, lo más probable es que no note ningún problema al trabajar con ORM. Veamos un pequeño ejemplo. Supongamos que tenemos una entidad de User () que tiene dos atributos: ID y Name () :


 public class User { @Id @GeneratedValue private int id; private String name; //Getters and Setters here } 

Para obtener una instancia de esta entidad de la base de datos, solo necesitamos llamar a un método del objeto EntityManager :


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, id); 

Las cosas se ponen un poco más interesantes cuando aparece una relación uno a muchos:


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses; //Getters and Setters here } 

Si necesitamos extraer una instancia de usuario de la base de datos, surge la pregunta: "¿También seleccionamos direcciones?". Y la respuesta "correcta" aquí es: "Depende de ..." En algunos casos necesitaremos direcciones, en otros, no. Normalmente, ORM proporciona dos formas de obtener registros dependientes: perezoso y codicioso. Por defecto, la mayoría de los ORM usan la forma perezosa. Pero, si escribimos este código:


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, 1); em.close(); System.out.println(user.getAddresses().get(0)); 

... entonces tenemos la excepción “LazyInitException” , que confunde terriblemente a los recién llegados que acaban de comenzar a trabajar con ORM. Y aquí llega el momento en que necesita comenzar una historia sobre qué son las instancias "Adjuntas" y "Separadas" de una entidad, qué son las sesiones y las transacciones.
Sí, eso significa que la entidad debe estar "adjunta" a la sesión para que pueda seleccionar los datos dependientes. Bueno, no cerremos las transacciones de inmediato, y la vida se volverá más fácil de inmediato. Y aquí surge otro problema: las transacciones se vuelven más largas, lo que aumenta el riesgo de punto muerto. ¿Hacer transacciones más cortas? Es posible, pero si crea muchas, muchas pequeñas transacciones, obtenemos el "Cuento de Komar Komarovich - una nariz larga y sobre una peluda Misha - una cola corta" sobre cómo ganó la horda de pequeños mosquitos oso - sucederá con la base de datos. Si el número de transacciones pequeñas aumenta significativamente, surgirán problemas de rendimiento.
Como se dijo, cuando se obtienen datos sobre un usuario, las direcciones pueden ser necesarias o no, por lo tanto, dependiendo de la lógica empresarial, debe seleccionar la colección o no. Es necesario agregar nuevas condiciones al código ... Hmmm ... Algo se está complicando de alguna manera.


Entonces, ¿qué pasa si prueba un tipo diferente de muestra?


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.EAGER) private List<Address> addresses; //Getters and Setters here } 

Bueno ... no puedes decir que ayudará mucho. Sí, eliminaremos el odiado LazyInit y no es necesario verificar si la entidad está conectada a la sesión o no. Pero ahora podemos tener problemas de rendimiento, porque no siempre necesitamos direcciones, pero aún seleccionamos estos objetos en la memoria del servidor.
¿Alguna idea más?


Spring jdbc


Algunos desarrolladores se cansan tanto de ORM que cambian a marcos alternativos. Por ejemplo, en Spring JDBC, que proporciona la capacidad de convertir datos relacionales en datos de objeto en modo "semiautomático". El desarrollador escribe consultas para cada caso donde se necesita un conjunto particular de atributos (o el mismo código se reutiliza para casos donde se necesitan las mismas estructuras de datos).


Esto nos da una gran flexibilidad. Por ejemplo, puede seleccionar solo un atributo sin crear el objeto de entidad correspondiente:


 String name = this.jdbcTemplate.queryForObject( "select name from t_user where id = ?", new Object[]{1L}, String.class); 

O seleccione un objeto en la forma habitual:


 User user = this.jdbcTemplate.queryForObject( "select id, name from t_user where id = ?", new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString("name")); user.setId(rs.getInt("id")); return user; } }); 

También puede seleccionar una lista de direcciones para el usuario, solo necesita escribir un poco más de código y componer correctamente la consulta SQL para evitar el problema de las consultas n + 1 .


Taaaan, complicado otra vez. Sí, controlamos todas las consultas y cómo se asignan los datos a los objetos, pero necesitamos escribir más código, aprender SQL y saber cómo se ejecutan las consultas en la base de datos. Personalmente, creo que el conocimiento de SQL es una habilidad necesaria para un programador de aplicaciones, pero no todos piensan de esa manera, y no voy a involucrarme en polémicas. Después de todo, el conocimiento de las instrucciones de montaje x86 en estos días también es opcional. Pensemos mejor en cómo hacer la vida más fácil para los programadores.


JPA EntityGraph


Y demos un paso atrás y pensemos, ¿qué necesitamos? Parece que solo necesitamos indicar exactamente qué atributos necesitamos en cada caso. Bueno, hagámoslo! JPA 2.1 introdujo una nueva API: EntityGraph (gráfico de entidad). La idea es muy simple: utilizamos anotaciones para describir lo que elegiremos de la base de datos. Aquí hay un ejemplo:


 @Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "user-only-entity-graph"), @NamedEntityGraph(name = "user-addresses-entity-graph", attributeNodes = {@NamedAttributeNode("addresses")}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.LAZY) private Set<Address> addresses; //Getters and Setters here } 

Se describen dos gráficos para esta entidad: el user-only-entity-graph no selecciona el atributo Addresses (marcado como vago), mientras que el segundo gráfico le dice a ORM que seleccione este atributo. Si marcamos Addresses como ansiosas, el gráfico se ignorará y las direcciones se seleccionarán de todos modos.


Entonces, en JPA 2.1, puede muestrear datos como este:


 EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph"); Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph); User user = em.find(User.class, 1, properties); em.close(); 

Este enfoque simplifica enormemente el trabajo, no es necesario pensar por separado acerca de los atributos perezosos y la duración de la transacción. Una ventaja adicional es que el gráfico se aplica al nivel de la consulta SQL, por lo que no se seleccionan datos "adicionales" en la aplicación Java. Pero hay un pequeño problema: no puede decir qué atributos se seleccionaron y cuáles no. Hay una API para verificar, esto se hace usando la clase PersistenceUtil :


 PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses")); 

Pero esto es bastante aburrido y no todos están listos para hacer tales controles. ¿Hay algo más que pueda simplificar y simplemente no mostrar atributos que no fueron seleccionados?


Proyecciones de primavera


Spring Framework tiene una gran cosa llamada Proyecciones (y esto no es lo mismo que las proyecciones en Hibernate ). Si necesita seleccionar solo algunos atributos de una entidad, se crea una interfaz con los atributos necesarios y Spring selecciona "instancias" de esta interfaz de la base de datos. Como ejemplo, considere la siguiente interfaz:


 interface NamesOnly { String getName(); } 

Ahora puede definir un repositorio Spring JPA para obtener entidades de usuario de la siguiente manera:


 interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); } 

¡En este caso, después de llamar al método findByName, en la lista resultante obtenemos entidades que solo tienen acceso a los atributos definidos en la interfaz! De acuerdo con el mismo principio, uno puede elegir entidades dependientes, es decir seleccione inmediatamente la relación "maestro-detalle". Además, Spring genera SQL "correcto" en la mayoría de los casos, es decir solo aquellos atributos que se describen en la proyección se seleccionan de la base de datos, esto es muy similar a cómo funcionan los gráficos de entidad.
Esta es una API muy poderosa. Al definir interfaces, puede usar expresiones SpEL, usar clases con algún tipo de lógica incorporada en lugar de interfaces, y mucho más, todo se describe en detalle en la documentación .
El único problema con las proyecciones es que en su interior se implementan como pares clave-valor, es decir. son de solo lectura. Esto significa que incluso si definimos un método de establecimiento para la proyección, no podremos guardar los cambios ni a través de los repositorios CRUD ni a través del EntityManager. Por lo tanto, las proyecciones son DTO que se pueden convertir de nuevo a Entity y guardar solo si escribe su propio código para esto.


Cómo seleccionar datos en CUBA


Desde el comienzo del desarrollo del marco CUBA, intentamos optimizar la parte del código que funciona con la base de datos. En CUBA, utilizamos EclipseLink como base para la API de acceso a datos. Lo bueno de EclipseLink es que admitió la carga parcial de entidades desde el principio, y este fue un factor decisivo para elegir entre él e Hibernate. En EclipseLink, podría especificar atributos para cargar mucho antes de que apareciera el estándar JPA 2.1. CUBA tiene su propia forma de describir un gráfico de entidad, denominado Vistas de CUBA . Representaciones CUBA es una API bastante desarrollada, puede heredar algunas representaciones de otras, combinarlas, aplicando a entidades maestras y de detalle. Otra motivación para crear vistas de CUBA es que queríamos usar transacciones cortas para poder trabajar con entidades separadas en la interfaz de usuario web.
En CUBA, las vistas se describen en un archivo XML, como en el siguiente ejemplo:


 <view class="com.sample.User" extends="_minimal" name="user-minimal-view"> <property name="name"/> <property name="addresses" view="address-street-only-view"/> </property> </view> 

Esta vista selecciona la entidad User y su name atributo local, y también selecciona direcciones al aplicarles la vista de address-street-only-view . Todo esto sucede (¡atención!) A nivel de la consulta SQL. Cuando se crea la vista, puede usarla en la selección de datos utilizando la clase DataManager:


 List<User> users = dataManager.load(User.class).view("user-edit-view").list(); 

Este enfoque funciona bien, mientras consume el tráfico de red de manera económica, ya que los atributos no utilizados simplemente no se transfieren de la base de datos a la aplicación, pero, como en el caso de JPA, hay un problema: no se puede decir qué atributos de la entidad se cargaron. Y en CUBA hay una excepción “IllegalStateException: Cannot get unfetched attribute [...] from detached object” , que, como LazyInit , debe haber sido encontrado por todos los que escriben utilizando nuestro marco. Al igual que en el JPA, hay formas de verificar qué atributos se cargaron y cuáles no, pero, nuevamente, escribir dichos controles es una tarea tediosa y laboriosa que molesta mucho a los desarrolladores. Hay que inventar algo más para no agobiar a las personas con el trabajo que, en teoría, las máquinas pueden hacer.


Concepto - CUBA View Interfaces


Pero, ¿qué pasa si intentas combinar gráficos de entidad y proyecciones? Decidimos probar esto y desarrollamos interfaces para las interfaces de vista de entidad que siguen el enfoque de proyección Spring. Estas interfaces se traducen en vistas CUBA al inicio de la aplicación y se pueden usar en el DataManager. La idea es simple: describimos una interfaz (o un conjunto de interfaces), que es un gráfico de entidad.


 interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } } 

Vale la pena señalar que para algunos casos específicos, puede hacer interfaces locales, como en el caso de AddressStreetOnly del ejemplo anterior, para no "contaminar" la API pública de su aplicación.


En el proceso de iniciar una aplicación CUBA (la mayoría de las cuales es la inicialización del contexto Spring), creamos programáticamente vistas CUBA y las colocamos en el repositorio interno de beans en contexto.
Ahora necesita modificar ligeramente la implementación de la clase DataManager para que acepte vistas de interfaz, y puede seleccionar entidades de esta manera:


 List<UserMinimalView> users = dataManager.load(UserMinimalView.class).list(); 

Bajo el capó, se genera un objeto proxy que implementa la interfaz y envuelve la instancia de la entidad seleccionada de la base de datos (de la misma manera que en Hibernate). Y, cuando el desarrollador solicita el valor del atributo, el proxy delega la llamada al método en la instancia "real" de la entidad.


Al desarrollar este concepto, estamos tratando de matar dos pájaros de un tiro:


  • Los datos que no se describen en la interfaz no se cargan en la aplicación, lo que ahorra recursos del servidor.
  • El desarrollador puede usar solo aquellos atributos que son accesibles a través de la interfaz (y, por lo tanto, se seleccionan de la base de datos), eliminando así las excepciones UnfetchedAttribute que escribimos anteriormente.

A diferencia de las proyecciones de Spring, envolvemos entidades en objetos proxy, además, cada interfaz hereda la interfaz CUBA estándar: Entity . Esto significa que los atributos de Vista de entidad se pueden cambiar y luego guardar estos cambios en la base de datos utilizando la API estándar de CUBA para trabajar con datos.
Y, por cierto, la "tercera liebre": puede hacer que los atributos sean de solo lectura si define una interfaz solo con métodos getter. Por lo tanto, ya establecemos las reglas de modificación en el nivel de API de la entidad.
Además, puede realizar algunas operaciones locales para entidades separadas utilizando atributos disponibles, por ejemplo, conversión de cadena de nombre, como en el ejemplo a continuación:


 @MetaProperty default String getNameLowercase() { return getName().toLowerCase(); } 

Tenga en cuenta que los atributos calculados pueden extraerse del modelo de clase de entidad y transferirse a las interfaces aplicables a una lógica comercial particular.


Otra característica interesante es la herencia de la interfaz. Puede crear varias vistas con diferentes conjuntos de atributos y luego combinarlos. Por ejemplo, puede crear una interfaz para una entidad de Usuario con los atributos de nombre y correo electrónico, y otra con los atributos de nombre y dirección. Ahora, si necesita seleccionar el nombre, el correo electrónico y las direcciones, no necesita copiar estos atributos en la tercera interfaz, solo necesita heredar de las dos primeras vistas. Y sí, las instancias de la tercera interfaz se pueden pasar a métodos que aceptan parámetros con el tipo de interfaces principales, las reglas de OOP son las mismas para todos.


También se implementó una conversión entre vistas: cada interfaz tiene un método reload (), en el que puede pasar la clase de vista como parámetro:


 UserFullView userFull = userMinimal.reload(UserFullView.class); 

UserFullView puede contener atributos adicionales, por lo que la entidad se volverá a cargar desde la base de datos, si es necesario. Y este proceso se retrasa. El acceso a la base de datos se realizará solo cuando ocurra el primer acceso a los atributos de la entidad. Esto ralentizará un poco la primera llamada, pero este enfoque se eligió intencionalmente: si la instancia de entidad se usa en el módulo "web", que contiene IU y sus propios controladores REST, este módulo se puede implementar en un servidor separado. Y esto significa que la sobrecarga forzada de la entidad creará tráfico de red adicional: acceso al módulo central y luego a la base de datos. Por lo tanto, posponiendo la sobrecarga hasta el momento en que es necesario, ahorramos tráfico y reducimos el número de consultas a la base de datos.


El concepto está diseñado como un módulo para CUBA, un ejemplo de uso se puede descargar desde GitHub .


Conclusión


Parece que en el futuro cercano todavía estaremos usando masivamente ORM en aplicaciones empresariales simplemente porque necesitamos algo que convierta los datos relacionales en objetos. Por supuesto, se desarrollarán soluciones específicas para aplicaciones complejas, únicas y de carga ultra alta, pero parece que los marcos de ORM vivirán tanto como las bases de datos relacionales.
En CUBA, intentamos simplificar al máximo el trabajo con ORM, y en futuras versiones presentaremos nuevas funciones para trabajar con datos. Será difícil decir si serán interfaces de presentación u otra cosa, pero estoy seguro de una cosa: seguiremos simplificando el trabajo con datos en futuras versiones del marco.

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


All Articles