JavaRush /Blog Java /Random-ES /JPA: Presentación de la tecnología
Viacheslav
Nivel 3

JPA: Presentación de la tecnología

Publicado en el grupo Random-ES
El mundo del desarrollo moderno está lleno de diversas especificaciones diseñadas para hacer la vida más fácil. Conociendo las herramientas, puedes elegir la correcta. Sin saberlo, puedes hacerte la vida más difícil. Esta revisión levantará el velo del secreto sobre el concepto de JPA - Java Persistence API. Espero que después de leer quieras sumergirte aún más en este misterioso mundo.
JPA: Introducción a la tecnología - 1

Introducción

Como sabemos, una de las principales tareas de los programas es almacenar y procesar datos. En los viejos tiempos, la gente simplemente almacenaba datos en archivos. Pero tan pronto como se necesita acceso simultáneo de lectura y edición, cuando hay una carga (es decir, llegan varias solicitudes al mismo tiempo), almacenar datos simplemente en archivos se convierte en un problema. Para obtener más información sobre qué problemas resuelven las bases de datos y cómo, te aconsejo leer el artículo “ Cómo se estructuran las bases de datos ”. Esto significa que decidimos almacenar nuestros datos en una base de datos. Durante mucho tiempo, Java ha podido trabajar con bases de datos utilizando la API JDBC (Java Database Connectivity). Puede leer más sobre JDBC aquí: " JDBC o donde todo comienza ". Pero el tiempo pasó y los desarrolladores cada vez se enfrentaron a la necesidad de escribir el mismo tipo y código de "mantenimiento" innecesario (el llamado código Boilerplate) para operaciones triviales para guardar objetos Java en la base de datos y viceversa, creando objetos Java utilizando datos de la base de datos. base de datos. Y luego, para resolver estos problemas, nació un concepto como ORM. ORM - Mapeo relacional de objetos o traducido al mapeo relacional de objetos ruso. Es una tecnología de programación que vincula las bases de datos con los conceptos de los lenguajes de programación orientados a objetos. Para simplificar, ORM es la conexión entre objetos Java y registros en una base de datos: JPA: Introducción a la Tecnología - 2ORM es esencialmente el concepto de que un objeto Java se puede representar como datos en una base de datos (y viceversa). Se materializó en la forma de la especificación JPA: Java Persistence API. La especificación ya es una descripción de la API de Java que expresa este concepto. La especificación nos dice qué herramientas se nos deben proporcionar (es decir, con qué interfaces podemos trabajar) para poder trabajar de acuerdo con el concepto ORM. Y cómo utilizar estos fondos. La especificación no describe la implementación de las herramientas. Esto hace posible utilizar diferentes implementaciones para una especificación. Puedes simplificarlo y decir que una especificación es una descripción de la API. El texto de la especificación JPA se puede encontrar en el sitio web de Oracle: " JSR 338: JavaTM Persistence API ". Por lo tanto, para poder utilizar JPA, necesitamos alguna implementación con la que usaremos la tecnología. Las implementaciones JPA también se denominan proveedores JPA. Una de las implementaciones JPA más notables es Hibernate . Por tanto, propongo considerarlo.
JPA: Introducción a la Tecnología - 3

Creando un proyecto

Dado que JPA trata sobre Java, necesitaremos un proyecto Java. Podríamos crear manualmente la estructura del directorio nosotros mismos y agregar las bibliotecas necesarias nosotros mismos. Pero es mucho más conveniente y correcto utilizar sistemas para automatizar el ensamblaje de proyectos (es decir, en esencia, este es solo un programa que gestionará el ensamblaje de proyectos por nosotros. Cree directorios, agregue las bibliotecas necesarias al classpath, etc. .). Uno de esos sistemas es Gradle. Puede leer más sobre Gradle aquí: " Una breve introducción a Gradle ". Como sabemos, la funcionalidad de Gradle (es decir, las cosas que puede hacer) se implementa mediante varios complementos de Gradle. Usemos Gradle y el complemento " Gradle Build Init Plugin ". Ejecutemos el comando:

gradle init --type java-application
Gradle hará la estructura de directorios necesaria por nosotros y creará una descripción declarativa básica del proyecto en el script de compilación build.gradle. Entonces, tenemos una aplicación. Necesitamos pensar en lo que queremos describir o modelar con nuestra aplicación. Usemos alguna herramienta de modelado, por ejemplo: app.quickdatabasediagrams.com JPA: Introducción a la Tecnología - 4 Aquí vale decir que lo que hemos descrito es nuestro “modelo de dominio”. Un dominio es un “área temática”. En general, dominio es “posesión” en latín. En la Edad Media se llamaba así a las zonas propiedad de reyes o señores feudales. Y en francés se convirtió en la palabra "domaine", que se traduce simplemente como "área". Así describimos nuestro “modelo de dominio” = “modelo sujeto”. Cada elemento de este modelo es una especie de “esencia”, algo de la vida real. En nuestro caso, se trata de entidades: Categoría ( Category), Asunto ( Topic). Creemos un paquete separado para las entidades, por ejemplo con el nombre modelo. Y agreguemos clases Java que describan entidades. En el código Java, dichas entidades son un POJO normal , que puede verse así:
public class Category {
    private Long id;
    private String title;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
Copiemos el contenido de la clase y creemos una clase por analogía Topic. Sólo se diferenciará en lo que sabe acerca de la categoría a la que pertenece. Por lo tanto, agreguemos Topica la clase un campo de categoría y métodos para trabajar con él:
private Category category;

public Category getCategory() {
	return category;
}

public void setCategory(Category category) {
	this.category = category;
}
Ahora tenemos una aplicación Java que tiene su propio modelo de dominio. Ahora es el momento de empezar a conectarnos al proyecto JPA.
JPA: Introducción a la Tecnología - 5

Agregar APP

Entonces, como recordamos, JPA significa que guardaremos algo en la base de datos. Por lo tanto, necesitamos una base de datos. Para usar una conexión de base de datos en nuestro proyecto, necesitamos agregar una biblioteca de dependencia para conectarnos a la base de datos. Como recordamos, usamos Gradle, que creó un script de compilación para nosotros build.gradle. En él describiremos las dependencias que necesita nuestro proyecto. Las dependencias son aquellas bibliotecas sin las cuales nuestro código no puede funcionar. Comencemos con una descripción de la dependencia para conectarse a la base de datos. Hacemos esto de la misma manera que lo haríamos si solo estuviéramos trabajando con JDBC:

dependencies {
	implementation 'com.h2database:h2:1.4.199'
Ahora tenemos una base de datos. Ahora podemos agregar una capa a nuestra aplicación que sea responsable de mapear nuestros objetos Java en conceptos de bases de datos (de Java a SQL). Como recordamos, vamos a utilizar una implementación de la especificación JPA llamada Hibernate para esto:

dependencies {
	implementation 'com.h2database:h2:1.4.199'
	implementation 'org.hibernate:hibernate-core:5.4.2.Final'
Ahora necesitamos configurar JPA. Si leemos la especificación y el apartado "8.1 Unidad de Persistencia", sabremos que una Unidad de Persistencia es algún tipo de combinación de configuraciones, metadatos y entidades. Y para que JPA funcione, es necesario describir al menos una unidad de persistencia en el archivo de configuración, que se llama persistence.xml. Su ubicación se describe en el capítulo de especificaciones "8.2 Embalaje de la unidad de persistencia". Según este apartado, si tenemos un entorno Java SE, entonces debemos ponerlo en la raíz del directorio META-INF.
JPA: Introducción a la Tecnología - 6
Copiemos el contenido del ejemplo dado en la especificación JPA en el 8.2.1 persistence.xml filecapítulo " ":
<persistence>
	<persistence-unit name="JavaRush">
        <description>Persistence Unit For test</description>
        <class>hibernate.model.Category</class>
        <class>hibernate.model.Topic</class>
    </persistence-unit>
</persistence>
Pero esto no es suficiente. Necesitamos decir quién es nuestro proveedor JPA, es decir aquel que implementa la especificación JPA:
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
Ahora agreguemos configuraciones ( properties). Algunas de ellas (comenzando con javax.persistence) son configuraciones JPA estándar y se describen en la especificación JPA en la sección "8.2.1.9 propiedades". Algunas configuraciones son específicas del proveedor (en nuestro caso, afectan a Hibernate como proveedor Jpa. Nuestro bloque de configuración se verá así:
<properties>
    <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
    <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />
    <property name="javax.persistence.jdbc.user" value="sa" />
    <property name="javax.persistence.jdbc.password" value="" />
    <property name="hibernate.show_sql" value="true" />
    <property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
Ahora tenemos una configuración compatible con JPA persistence.xml, hay un proveedor JPA Hibernate y hay una base de datos H2, y también hay 2 clases que son nuestro modelo de dominio. Finalmente hagamos que todo esto funcione. En el catálogo /test/java, nuestro Gradle generó amablemente una plantilla para pruebas unitarias y la llamó AppTest. Usémoslo. Como se indica en el capítulo "7.1 Contextos de persistencia" de la especificación JPA, las entidades en el mundo JPA viven en un espacio llamado Contexto de persistencia. Pero no trabajamos directamente con Persistence Context. Para ello utilizamos Entity Managero "administrador de entidades". Es él quien conoce el contexto y qué entidades viven allí. Interactuamos con Entity Manager'om. Entonces todo lo que queda es entender de dónde podemos sacarlo Entity Manager. Según el capítulo "7.2.2 Obtención de un Entity Manager gestionado por aplicaciones" de la especificación JPA, debemos utilizar EntityManagerFactory. Por lo tanto, armémonos con la especificación JPA y tomemos un ejemplo del capítulo “7.3.2 Obtención de una fábrica de Entity Manager en un entorno Java SE” y formateémoslo en forma de una prueba unitaria simple:
@Test
public void shouldStartHibernate() {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
	EntityManager entityManager = emf.createEntityManager();
}
Esta prueba ya mostrará el error "Versión JPA persistence.xml XSD no reconocida". La razón es que persistence.xmlnecesita especificar correctamente el esquema a utilizar, como se indica en la especificación JPA en la sección "8.3 Esquema persistence.xml":
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
             http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">
Además, el orden de los elementos es importante. Por lo tanto, providerse debe especificar antes de enumerar las clases. Después de esto, la prueba se ejecutará exitosamente. Hemos completado la conexión JPA directa. Antes de continuar, pensemos en las pruebas restantes. Cada una de nuestras pruebas requerirá EntityManager. Asegurémonos de que cada prueba tenga la suya EntityManageral inicio de la ejecución. Además, queremos que la base de datos sea nueva cada vez. Debido a que usamos inmemoryla opción, basta con cerrar EntityManagerFactory. La creación Factoryes una operación costosa. Pero para las pruebas está justificado. JUnit le permite especificar métodos que se ejecutarán antes (Antes) y después (Después) de la ejecución de cada prueba:
public class AppTest {
    private EntityManager em;

    @Before
    public void init() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
        em = emf.createEntityManager();
    }

    @After
    public void close() {
        em.getEntityManagerFactory().close();
        em.close();
    }
Ahora bien, antes de ejecutar cualquier prueba, se creará una nueva EntityManagerFactory, lo que supondrá la creación de una nueva base de datos, porque hibernate.hbm2ddl.autotiene el significado create. Y de la nueva fábrica obtendremos uno nuevo EntityManager.
JPA: Introducción a la Tecnología - 7

Entidades

Como recordamos, previamente creamos clases que describen nuestro modelo de dominio. Ya hemos dicho que estas son nuestras “esencias”. Esta es la Entidad que gestionaremos utilizando EntityManager. Escribamos una prueba simple para guardar la esencia de una categoría:
@Test
public void shouldPersistCategory() {
	Category cat = new Category();
	cat.setTitle("new category");
	// JUnit обеспечит тест свежим EntityManager'ом
	em.persist(cat);
}
Pero esta prueba no funcionará de inmediato, porque... recibiremos varios errores que nos ayudarán a entender qué son las entidades:
  • Unknown entity: hibernate.model.Category
    ¿Por qué Hibernate no entiende qué Categoryes esto entity? La cuestión es que las entidades deben describirse según el estándar JPA.
    Las clases de entidad deben estar anotadas con la anotación @Entity, como se indica en el capítulo "2.1 La clase de entidad" de la especificación JPA.

  • No identifier specified for entity: hibernate.model.Category
    Las entidades deben tener un identificador único que pueda usarse para distinguir un registro de otro.
    Según el capítulo "2.4 Claves primarias e identidad de la entidad" de la especificación JPA, "Cada entidad debe tener una clave primaria", es decir. Cada entidad debe tener una "clave primaria". Dicha clave primaria debe especificarse mediante la anotación.@Id

  • ids for this class must be manually assigned before calling save()
    La identificación tiene que venir de algún lado. Se puede especificar manualmente o se puede obtener automáticamente.
    Por lo tanto, como se indica en los capítulos "11.2.3.3 GeneratedValue" y "11.1.20 GeneratedValue Annotation", podemos especificar la anotación @GeneratedValue.

Entonces para que la clase de categoría se convierta en una entidad debemos realizar los siguientes cambios:
@Entity
public class Category {
    @Id
    @GeneratedValue
    private Long id;
Además, la anotación @Idindica cuál usar Access Type. Puedes leer más sobre el tipo de acceso en la especificación JPA, en la sección "2.3 Tipo de acceso". Para decirlo muy brevemente, porque... especificamos @Idencima del campo ( field), entonces el tipo de acceso será el predeterminado field-based, no property-based. Por lo tanto, el proveedor JPA leerá y almacenará valores directamente desde los campos. Si lo colocamos @Idencima del captador, entonces property-basedse usaría el acceso, es decir. a través de getter y setter. Al ejecutar la prueba, también vemos qué solicitudes se envían a la base de datos (gracias a la opción hibernate.show_sql). Pero al guardar, no vemos ninguno insert. ¿Resulta que en realidad no guardamos nada? JPA le permite sincronizar el contexto de persistencia y la base de datos utilizando el método flush:
entityManager.flush();
Pero si lo ejecutamos ahora, obtendremos un error: no hay ninguna transacción en curso . Y ahora es el momento de aprender cómo JPA utiliza las transacciones.
JPA: Introducción a la Tecnología - 8

Transacciones APP

Como recordamos, JPA se basa en el concepto de contexto de persistencia. Este es el lugar donde viven las entidades. Y gestionamos entidades a través de EntityManager. Cuando ejecutamos el comando persist, colocamos la entidad en el contexto. Más precisamente, les decimos EntityManagerque es necesario hacerlo. Pero este contexto es sólo un área de almacenamiento. A veces incluso se le llama "caché de primer nivel". Pero necesita estar conectado a la base de datos. El comando flush, que anteriormente falló con un error, sincroniza los datos del contexto de persistencia con la base de datos. Pero esto requiere transporte y este transporte es una transacción. Las transacciones en JPA se describen en la sección "7.5 Control de transacciones" de la especificación. Existe una API especial para usar transacciones en JPA:
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
Necesitamos agregar gestión de transacciones a nuestro código, que se ejecuta antes y después de las pruebas:
@Before
public void init() {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
	em = emf.createEntityManager();
	em.getTransaction().begin();
}
@After
public void close() {
	if (em.getTransaction().isActive()) {
		em.getTransaction().commit();
        }
	em.getEntityManagerFactory().close();
	em.close();
}
Después de agregar, veremos en el registro de inserción una expresión en SQL que no estaba allí antes:
JPA: Introducción a la Tecnología - 9
Los cambios acumulados en EntityManagerla transacción fueron confirmados (confirmados y guardados) en la base de datos. Intentemos ahora encontrar nuestra esencia. Creemos una prueba para buscar una entidad por su ID:
@Test
public void shouldFindCategory() {
	Category cat = new Category();
	cat.setTitle("test");
	em.persist(cat);
	Category result = em.find(Category.class, 1L);
	assertNotNull(result);
}
En este caso, recibiremos la entidad que guardamos anteriormente, pero no veremos las consultas SELECT en el registro. Y todo se basa en lo que decimos: "Administrador de entidad, búsqueme la entidad de categoría con ID = 1". Y el administrador de la entidad primero busca en su contexto (usa una especie de caché), y solo si no lo encuentra, busca en la base de datos. Vale la pena cambiar el ID a 2 (no existe tal cosa, solo guardamos 1 instancia), y veremos que SELECTaparece la solicitud. Debido a que no se encontraron entidades en el contexto y EntityManagerla base de datos está intentando encontrar una entidad, existen diferentes comandos que podemos usar para controlar el estado de una entidad en el contexto. La transición de una entidad de un estado a otro se llama ciclo de vida de la entidad lifecycle.
JPA: Introducción a la Tecnología - 10

Ciclo de vida de la entidad

El ciclo de vida de las entidades se describe en la especificación JPA en el capítulo "3.2 Ciclo de vida de la instancia de entidad". Porque las entidades viven en un contexto y están controladas por EntityManager, entonces dicen que las entidades están controladas, es decir, administrado. Veamos las etapas de la vida de una entidad:
// 1. New o Transient (временный)
Category cat = new Category();
cat.setTitle("new category");
// 2. Managed o Persistent
entityManager.persist(cat);
// 3. Транзакция завершена, все сущности в контексте detached
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
// 4. Сущность изымаем из контекста, она становится detached
entityManager.detach(cat);
// 5. Сущность из detached можно снова сделать managed
Category managed = entityManager.merge(cat);
// 6. И можно сделать Removed. Интересно, что cat всё равно detached
entityManager.remove(managed);
Y aquí hay un diagrama para consolidarlo:
JPA: Introducción a la Tecnología - 11
JPA: Introducción a la Tecnología - 12

Cartografía

En JPA podemos describir las relaciones de entidades entre sí. Recordemos que ya analizamos las relaciones de las entidades entre sí cuando tratamos nuestro modelo de dominio. Luego utilizamos el recurso quickdatabasediagrams.com :
JPA: Introducción a la Tecnología - 13
Establecer conexiones entre entidades se llama mapeo o asociación (Association Mappings). Los tipos de asociaciones que se pueden establecer utilizando JPA se presentan a continuación:
JPA: Introducción a la tecnología - 14
Veamos una entidad Topicque describe un tema. ¿Qué podemos decir sobre la actitud Topichacia Category? Muchos Topicpertenecerán a una categoría. Por eso necesitamos una asociación ManyToOne. Expresemos esta relación en JPA:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
Para recordar qué anotaciones poner, puedes recordar que la última parte es responsable del campo sobre el cual se indica la anotación. ToOne- instancia específica. ToMany- colecciones. Ahora nuestra conexión es unidireccional. Hagamos que sea una comunicación bidireccional. Aumentemos Categoryel conocimiento sobre todos Topiclos que están incluidos en esta categoría. Debe terminar con ToMany, porque tenemos una lista Topic. Es decir, la actitud hacia “muchos” temas. La pregunta sigue siendo - OneToManyo ManyToMany:
JPA: Introducción a la Tecnología - 15
Se puede leer una buena respuesta sobre el mismo tema aquí: " Explique la relación ORM oneToMany, manyToMany como si tuviera cinco ". Si una categoría tiene una conexión con ToManytemas, entonces cada uno de estos temas puede tener solo una categoría, entonces será One, de lo contrario Many. Entonces la Categorylista de todos los temas se verá así:
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
Y no olvidemos Categoryescribir un captador para obtener una lista de todos los temas:
public Set<Topic> getTopics() {
	return this.topics;
}
Las relaciones bidireccionales son algo muy difícil de rastrear automáticamente. Por lo tanto, JPA transfiere esta responsabilidad al desarrollador. Lo que esto significa para nosotros es que cuando establecemos una Topicrelación de entidad con Categorynosotros mismos debemos garantizar la coherencia de los datos. Esto se hace simplemente:
public void setCategory(Category category) {
	category.getTopics().add(this);
	this.category = category;
}
Escribamos una prueba simple para verificar:
@Test
public void shouldPersistCategoryAndTopics() {
	Category cat = new Category();
	cat.setTitle("test");
	Topic topic = new Topic();
	topic.setTitle("topic");
	topic.setCategory(cat);
 	em.persist(cat);
}
El mapeo es un tema completamente aparte. El propósito de esta revisión es comprender los medios por los cuales esto se logra. Puede leer más sobre el mapeo aquí:
JPA: Introducción a la tecnología - 16

JPQL

JPA presenta una herramienta interesante: consultas en Java Persistence Query Language. Este lenguaje es similar a SQL, pero utiliza el modelo de objetos Java en lugar de tablas SQL. Veamos un ejemplo:
@Test
public void shouldPerformQuery() {
	Category cat = new Category();
	cat.setTitle("query");
	em.persist(cat);
	Query query = em.createQuery("SELECT c from Category c WHERE c.title = 'query'");
 	assertNotNull(query.getSingleResult());
}
Como podemos ver, en la consulta utilizamos una referencia a una entidad Categoryy no a una tabla. Y también en el campo de esta entidad title. JPQL ofrece muchas funciones útiles y merece su propio artículo. Se pueden encontrar más detalles en la revisión:
JPA: Introducción a la tecnología - 17

API de criterios

Y finalmente, me gustaría hablar de la API de Criteria. JPA presenta una herramienta de creación de consultas dinámicas. Ejemplo de uso de la API de Criteria:
@Test
public void shouldFindWithCriteriaAPI() {
	Category cat = new Category();
	em.persist(cat);
	CriteriaBuilder cb = em.getCriteriaBuilder();
	CriteriaQuery<Category> query = cb.createQuery(Category.class);
	Root<Category> c = query.from(Category.class);
	query.select(c);
	List<Category> resultList = em.createQuery(query).getResultList();
	assertEquals(1, resultList.size());
}
Este ejemplo equivale a ejecutar la solicitud " SELECT c FROM Category c". La API de Criteria es una herramienta poderosa. Puedes leer más sobre esto aquí:

Conclusión

Como podemos ver, JPA proporciona una gran cantidad de funciones y herramientas. Cada uno de ellos requiere experiencia y conocimiento. Incluso en el marco de la revisión de la APP no fue posible mencionarlo todo, por no hablar de una inmersión detallada. Pero espero que después de leerlo quede más claro qué son ORM y JPA, cómo funcionan y qué se puede hacer con ellos. Pues para la merienda te ofrezco varios materiales: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION