Persistencia de Java con JPA e Hibernate, Parte 1: Entidades y relaciones

La API de persistencia de Java (JPA) es una especificación de Java que cierra la brecha entre las bases de datos relacionales y la programación orientada a objetos. Este tutorial de dos partes presenta JPA y explica cómo se modelan los objetos Java como entidades JPA, cómo se definen las relaciones entre entidades y cómo utilizar JPA EntityManagercon el patrón Repository en sus aplicaciones Java.

Tenga en cuenta que este tutorial utiliza Hibernate como proveedor de JPA. La mayoría de los conceptos se pueden extender a otros marcos de persistencia de Java.

¿Qué es JPA?

Consulte "¿Qué es JPA? Introducción a la API de persistencia de Java" para conocer la evolución de JPA y los marcos relacionados, incluido EJB 3.0. y JDBC.

Relaciones de objeto en JPA

Las bases de datos relacionales han existido como un medio para almacenar datos de programas desde la década de 1970. Si bien los desarrolladores de hoy tienen muchas alternativas a la base de datos relacional, este tipo de base de datos es escalable y se comprende bien, y todavía se usa ampliamente en el desarrollo de software a pequeña y gran escala.

Los objetos Java en un contexto de base de datos relacional se definen como entidades . Las entidades se colocan en tablas donde ocupan columnas y filas. Los programadores usan claves externas y tablas de unión para definir las relaciones entre entidades, es decir, relaciones uno a uno, uno a muchos y muchos a muchos. También podemos usar SQL (lenguaje de consulta estructurado) para recuperar e interactuar con datos en tablas individuales y en varias tablas, utilizando restricciones de clave externa. El modelo relacional es plano, pero los desarrolladores pueden escribir consultas para recuperar datos y construir objetos a partir de esos datos.

Desajuste de impedancia de relaciones de objeto

Es posible que esté familiarizado con el término desajuste de impedancia de relaciones de objeto , que se refiere al desafío de mapear objetos de datos a una base de datos relacional. Este desajuste se produce porque el diseño orientado a objetos no se limita a relaciones uno a uno, uno a muchos y muchos a muchos. En cambio, en el diseño orientado a objetos, pensamos en los objetos, sus atributos y comportamiento, y cómo se relacionan los objetos. Dos ejemplos son la encapsulación y la herencia:

  • Si un objeto contiene otro objeto, definimos esto a través de la encapsulación --una tiene -una relación.
  • Si un objeto es una especialización de otro objeto, definimos esto a través de la herencia --una es: una relación.

Asociación, agregación, composición, abstracción, generalización, realización y dependencias son conceptos de programación orientada a objetos que pueden ser difíciles de asignar a un modelo relacional.

ORM: mapeo relacional de objetos

El desajuste entre el diseño orientado a objetos y el modelado de bases de datos relacionales ha llevado a una clase de herramientas desarrolladas específicamente para el mapeo relacional de objetos (ORM). Las herramientas de ORM como Hibernate, EclipseLink e iBatis traducen modelos de bases de datos relacionales, incluidas las entidades y sus relaciones, en modelos orientados a objetos. Muchas de estas herramientas existían antes de la especificación JPA, pero sin un estándar, sus características dependían del proveedor.

Lanzada por primera vez como parte de EJB 3.0 en 2006, la API de persistencia de Java (JPA) ofrece una forma estándar de anotar objetos para que puedan mapearse y almacenarse en una base de datos relacional. La especificación también define una construcción común para interactuar con bases de datos. Tener un estándar ORM para Java aporta consistencia a las implementaciones de los proveedores, al tiempo que permite flexibilidad y complementos. A modo de ejemplo, si bien la especificación JPA original es aplicable a las bases de datos relacionales, algunas implementaciones de proveedores han extendido JPA para su uso con bases de datos NoSQL.

Evolución de JPA

La primera versión de JPA, la versión 1.0, se publicó en 2006 a través de Java Community Process (JCP) como Java Specification Request (JSR) 220. La versión 2.0 (JSR 317) se publicó en 2009, la versión 2.1 (JSR 338) en 2013, y la versión 2.2 (una versión de mantenimiento de JSR 338) se publicó en 2017. JPA 2.2 ha sido seleccionada para su inclusión y desarrollo continuo en Yakarta EE.

Empezando con JPA

La API de persistencia de Java es una especificación, no una implementación: define una abstracción común que puede utilizar en su código para interactuar con los productos ORM. Esta sección revisa algunas de las partes importantes de la especificación JPA.

Aprenderá a:

  • Defina entidades, campos y claves primarias en la base de datos.
  • Crea relaciones entre entidades en la base de datos.
  • Trabaja con EntityManagery sus métodos.

Definición de entidades

Para definir una entidad, debe crear una clase que esté anotada con la @Entityanotación. La @Entityanotación es una anotación de marcador , que se utiliza para descubrir entidades persistentes. Por ejemplo, si quisiera crear una entidad de libro, la anotaría de la siguiente manera:

 @Entity public class Book { ... } 

De forma predeterminada, esta entidad se asignará a la Booktabla, según lo determinado por el nombre de clase dado. Si desea asignar esta entidad a otra tabla (y, opcionalmente, a un esquema específico), puede usar la @Tableanotación para hacerlo. Así es como mapearía la Bookclase a una tabla BOOKS:

 @Entity @Table(name="BOOKS") public class Book { ... } 

Si la tabla LIBROS estaba en el esquema PUBLICACIÓN, puede agregar el esquema a la @Tableanotación:

 @Table(name="BOOKS", schema="PUBLISHING") 

Asignación de campos a columnas

Con la entidad asignada a una tabla, su próxima tarea es definir sus campos. Los campos se definen como variables miembro en la clase, y el nombre de cada campo se asigna a un nombre de columna en la tabla. Puede anular esta asignación predeterminada utilizando la @Columnanotación, como se muestra aquí:

 @Entity @Table(name="BOOKS") public class Book { private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

En este ejemplo, aceptamos la asignación predeterminada para el nameatributo, pero especificamos una asignación personalizada para el isbnatributo. El nameatributo se asignará a la columna de nombre , pero el isbnatributo se asignará a la columna ISBN_NUMBER.

La @Columnanotación nos permite definir propiedades adicionales del campo / columna, incluida la longitud, si es anulable, si debe ser único, su precisión y escala (si es un valor decimal), si es insertable y actualizable, etc. .

Especificando la clave primaria

Uno de los requisitos para una tabla de base de datos relacional es que debe contener una clave principal o una clave que identifique de forma única una fila específica en la base de datos. En JPA, usamos la @Idanotación para designar un campo para que sea la clave principal de la tabla. La clave principal debe ser un tipo primitivo de Java, un contenedor primitivo, como Integero Long, a String, a Date, a BigIntegero a BigDecimal.

En este ejemplo, asignamos el idatributo, que es an Integer, a la columna ID en la tabla BOOKS:

 @Entity @Table(name="BOOKS") public class Book { @Id private Integer id; private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

También es posible combinar la @Idanotación con la @Columnanotación para sobrescribir la asignación de nombre de columna de la clave principal.

Relaciones entre entidades

Ahora que sabe cómo definir una entidad, veamos cómo crear relaciones entre entidades. JPA define cuatro anotaciones para definir entidades:

  • @OneToOne
  • @OneToMany
  • @ManyToOne
  • @ManyToMany

Relaciones uno a uno

La @OneToOneanotación se utiliza para definir una relación uno a uno entre dos entidades. Por ejemplo, puede tener una Userentidad que contenga un nombre de usuario, correo electrónico y contraseña, pero es posible que desee mantener información adicional sobre un usuario (como edad, sexo y color favorito) en una UserProfileentidad separada . La @OneToOneanotación facilita el desglose de sus datos y entidades de esta manera.

La Usersiguiente clase tiene una sola UserProfileinstancia. Los UserProfilemapas a una sola Userinstancia.

 @Entity public class User { @Id private Integer id; private String email; private String name; private String password; @OneToOne(mappedBy="user") private UserProfile profile; ... } 
 @Entity public class UserProfile { @Id private Integer id; private int age; private String gender; private String favoriteColor; @OneToOne private User user; ... } 

Los usos proveedor JPA UserProfile's userde campo para asignar UserProfilea User. La asignación se especifica en el mappedByatributo de la @OneToOneanotación.

Relaciones de uno a muchos y de muchos a uno

Las anotaciones @OneToManyy @ManyToOnefacilitan ambos lados de la misma relación. Considere un ejemplo en el que a Bookpuede tener solo uno Author, pero Authorpuede tener muchos libros. La Bookentidad definiría una @ManyToOnerelación con Authory la Authorentidad definiría una @OneToManyrelación con Book.

 @Entity public class Book { @Id private Integer id; private String name; @ManyToOne @JoinColumn(name="AUTHOR_ID") private Author author; ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @OneToMany(mappedBy = "author") private List books = new ArrayList(); ... } 

En este caso, la Authorclase mantiene una lista de todos los libros escritos por ese autor y la Bookclase mantiene una referencia a su único autor. Además, @JoinColumnespecifica el nombre de la columna en la Booktabla para almacenar el ID del Author.

Relaciones de muchos a muchos

Finally, the @ManyToMany annotation facilitates a many-to-many relationship between entities. Here's a case where a Book entity has multiple Authors:

 @Entity public class Book { @Id private Integer id; private String name; @ManyToMany @JoinTable(name="BOOK_AUTHORS", [email protected](name="BOOK_ID"), [email protected](name="AUTHOR_ID")) private Set authors = new HashSet(); ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(mappedBy = "author") private Set books = new HashSet(); ... } 

In this example, we create a new table, BOOK_AUTHORS, with two columns: BOOK_ID and AUTHOR_ID. Using the joinColumns and inverseJoinColumns attributes tells your JPA framework how to map these classes in a many-to-many relationship. The @ManyToMany annotation in the Author class references the field in the Book class that manages the relationship; namely the authors property.

That's a quick demo for a fairly complex topic. We'll dive further into the @JoinTable and @JoinColumn annotations in the next article.

Working with the EntityManager

EntityManager is the class that performs database interactions in JPA. It is initialized through a configuration file named persistence.xml. This file is found in the META-INF folder in your CLASSPATH, which is typically packaged in your JAR or WAR file. The persistence.xml file contains:

  • The named "persistence unit," which specifies the persistence framework you're using, such as Hibernate or EclipseLink.
  • A collection of properties specifying how to connect to your database, as well as any customizations in the persistence framework.
  • A list of entity classes in your project.

Let's look at an example.

Configuring the EntityManager

First, we create an EntityManager using the EntityManagerFactory retrieved from the Persistence class:

 EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books"); EntityManager entityManager = entityManagerFactory.createEntityManager(); 

In this case we've created an EntityManager that is connected to the "Books" persistence unit, which we've configured in the persistence.xml file.

The EntityManager class defines how our software will interact with the database through JPA entities. Here are some of the methods used by EntityManager:

  • find retrieves an entity by its primary key.
  • createQuery creates a Query instance that can be used to retrieve entities from the database.
  • createNamedQuery loads a Query that has been defined in a @NamedQuery annotation inside one of the persistence entities. Named queries provide a clean mechanism for centralizing JPA queries in the definition of the persistence class on which the query will execute.
  • getTransaction defines an EntityTransaction to use in your database interactions. Just like database transactions, you will typically begin the transaction, perform your operations, and then either commit or rollback your transaction. The getTransaction() method lets you access this behavior at the level of the EntityManager, rather than the database.
  • merge() adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using merge(), objects are not managed.
  • persist adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using persist(), objects are managed.
  • refresh refreshes the state of the current entity from the database.
  • flush synchronizes the state of the persistence context with the database.

No se preocupe por integrar todos estos métodos a la vez. Los conocerá trabajando directamente con el EntityManager, que haremos más en la siguiente sección.