¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de realizar la implementación de la persistencia de una entidad utilizando JPA y el framework Spring Boot.

¿Qué necesita?

Para realizar este tutorial Ud. debe:

  1. Tener el ambiente de desarrollo instalado.
  2. Contar con el modelo de entidades.
  3. Tener clonado el proyecto back de ejemplo.

El siguiente diagrama muestra el modelo conceptual del caso de estudio sobre el que se basa este tutorial.

El ejemplo ha sido creado para que tenga distintos tipos de relaciones entre los recursos. La siguiente tabla describe las relaciones. En paréntesis está el nombre del extremo de la relación que se convertirá en el atributo que se modela en Java. Por ejemplo, la clase Book tendrá un atributo llamado reviews.

Asociación

Descripción

Book - Review

Un libro tiene un conjunto de revisiones (reviews)

Una revisión le pertenece a un único libro (book)

Book - Author

Un libro puede tener varios autores (authors)

Un autor puede haber escrito varios libros (books)

Book - Editorial

Un libro es editado por una única editorial (editorial).

Una editorial puede edita muchos libros (books)

Author - Prize

Un autor puede haber recibido muchos premios o ninguno (prizes)

Un premio le pertenece a un único autor (author).

Prize - Organization

Un premio es otorgado por una única organización (organization)

Una organización otorga un único premio (prize)

El siguiente diagrama muestra la transformación del modelo conceptual al modelo de entidades.

Las decisiones de diseño que se toman son las siguientes:

  1. Por cada clase del modelo conceptual se crea una clase que representa la entidad que va a persistir en la base de datos.
  2. A cada clase se le agrega el sufijo Entity
  3. Todas las entidades heredan de BaseEntity (no se incluye esto en el modelo por claridad).
  4. Para cada clase entidad, se toman todos los extremos opuestos de las relaciones que tiene con otras clases (clases destino de la relación). Cada extremo se anota de acuerdo con la cardinalidad de la asociación.

Dependiendo del caso, en cada anotación también se debe incluir, entre otros:

  1. El atributo correspondiente en la clase destino.
  2. El modo de cargue de la asociación, es decir, si se hace en forma inmediata (EAGER) o en forma perezosa (LAZY).
  3. Si la operación de persistencia se va a propagar a las clases relacionadas con esta (Cascade).

A nivel de código fuente todas las entidades del proyecto en la carpeta entities. Para el caso del ejemplo del curso, la ruta de la carpeta es la siguiente:

 src/main/java/co/edu/uniandes/dse/bookstore/entities

La clase BaseEntity es una clase abstracta que es una superclase de todas las clases de entidades del proyecto. La clase tiene la anotación @Data de la librería lombok sirve para autogenerar setters y getters de los atributos de la clase. La anotación @MappedSuperclass indica que esta es la clase de la cual las entidades van a heredar, además de omitir la creación de una tabla en la base de datos con esta clase.

Tiene el atributo id de tipo Long que corresponde a la llave primaria. Este atributo contiene varias anotaciones:

El siguiente es el contenido completo de esa clase:

package co.edu.uniandes.dse.bookstore.entities;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import lombok.Data;
import uk.co.jemos.podam.common.PodamExclude;

/**
 * Entidad genérica de la que heredan todas las entidades. Contiene la
 * referencia al atributo id
 *
 * @author ISIS2603
 */

@Data
@MappedSuperclass
public abstract class BaseEntity {

        @PodamExclude
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
}

Ahora vamos a analizar la implementación de la clase BookEntity. Tenga en cuenta que si quiere replicar este ejercicio en VS Code debe crear un archivo denominado BookEntity.java el cual se ubica en la carpeta entities tal como se observa en la siguiente imagen:

Este es el contenido del archivo BookEntity.java:

package co.edu.uniandes.dse.bookstore.entities;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import lombok.Data;
import uk.co.jemos.podam.common.PodamExclude;

/**
 * Clase que representa un libro en la persistencia
 *
 * @author ISIS2603
 */

@Data
@Entity
public class BookEntity extends BaseEntity {

        private String name;
        private String isbn;
        private String image;

        @Temporal(TemporalType.DATE)
        private Date publishingDate;

        private String description;

        @PodamExclude
        @ManyToOne
        private EditorialEntity editorial;

        @PodamExclude
        @OneToMany(mappedBy = "book", cascade = CascadeType.PERSIST, orphanRemoval = true)
        private List<ReviewEntity> reviews = new ArrayList<>();

        @PodamExclude
        @ManyToMany
        private List<AuthorEntity> authors = new ArrayList<>();
}

Notamos que la clase está anotada con @Entity. Esto indica que la clase de Java se convertirá en una tabla en la base de datos.

También tiene la anotación de Lombok @Data para facilitar la definición implícita de getters y setters para cada uno de los atributos de la clase.

Cuenta con los siguientes atributos de tipo String: el nombre, el isbn, la imagen y la descripción.

Incluye un atributo publishingDate que es de tipo Date. Este atributo tiene la anotación @Temporal(TemporalType.DATE). Esta sirve para indicarle al ORM que la columna de la tabla en la base de datos almacenará solo la parte de la fecha.

La clase tiene tres asociaciones con las clases EditorialEntity, ReviewEntity y AuthorEntity.

Tomando como referencia el diagrama anterior podemos observar que los extremos opuestos de la asociación (del lado de la clase destino) tienen el nombre que se usará para denominar el atributo. En este caso los atributos serán denominados editorial, reviews y authors.

Además de tener un tipo, cada uno de estos atributos deben tener una anotación que le permita a JPA crear adecuadamente las tablas. A continuación explicaremos esas anotaciones.

Asociación Book - Review

Esta es una relación de uno a muchos (OneToMany); es decir, un libro tiene muchos reviews.

La clase BookEntity tendrá la siguiente definición del atributo reviews:

...
@OneToMany(mappedBy = "book", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<ReviewEntity> reviews = new ArrayList<>();
...

Note que la anotación @OneToMany sobre reviews tiene varios atributos:

Lo anterior implica que cuando se implemente la clase ReviewEntity deberá tener la siguiente definición del atributo book:

...
@ManyToOne
 private BookEntity book;
...

Asociación Book - Author

Esta es una relación de muchos a muchos (ManyToMany); es decir, un libro tiene muchos autores.

La clase BookEntity tendrá la siguiente definición del atributo authors:

...
@ManyToMany
private List<AuthorEntity> authors = new ArrayList<>();
...

En este caso, como la cardinalidad de la asociación es "muchos" significa que el atributo será una colección, en este caso una lista.

Lo anterior implica que cuando se implemente la clase AuthorEntity deberá tener la siguiente definición del atributo books:

...
@ManyToMany(mappedBy = "authors")
private List<BookEntity> books = new ArrayList<>();
...

Tenga en cuenta que la clase que no tenga la anotación mappedBy será la dueña de la asociación. En este caso en la asociación Book - Author la dueña de la asociación es BookEntity. Esto es importante recordarlo cuando se haga la implementación de la lógica.

Asociación Book - Editorial

Esta es una relación de muchos a uno (ManyToOne); es decir, un libro tiene una única editorial.

La clase BookEntity tendrá la siguiente definición del atributo editorial:

..
@ManyToOne
private EditorialEntity editorial;
..

Lo anterior implica que cuando se implemente la clase EditorialEntity deberá tener la siguiente definición del atributo books:

..
@OneToMany(mappedBy = "editorial")
private List<BookEntity> books = new ArrayList<>();
..

Luego de haber implementado la entidad con las anotaciones necesarias, continuamos con la implementación de la interfaz Java encargada de la persistencia de la entidad.

En el paquete repositories del proyecto encontramos toda la capa de persistencia. Cada entidad que se haya definido en el paquete entities debe tener una interfaz de persistencia asignada. Esa interfaz extiende de JpaRepository, la cual incluye los métodos para las operaciones CRUD una entidad: Create, Retrieve, Update, Delete.

Si quiere replicar el ejemplo, en la carpeta repositories agregue un nuevo archivo denominado BookRepository.java.

Revisemos el contenido del archivo BookRepository.java:

package co.edu.uniandes.dse.bookstore.repositories;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import co.edu.uniandes.dse.bookstore.entities.BookEntity;

@Repository
public interface BookRepository extends JpaRepository<BookEntity, Long> {
        List<BookEntity> findByIsbn(String isbn);
}

La interfaz está anotada con @Repository, lo que indica que será un repositorio, es decir, que encapsulará el comportamiento de almacenamiento, recuperación y búsqueda que emula una colección de objetos.

La interfaz extiende de JpaRepository. Esto permite que la clase tenga acceso a la API completa de CrudRepository y PagingAndSortingRepository. De este modo, se contará con las operaciones CRUD básicas (crear, obtener, actualizar y borrar) y también operaciones para paginación y ordenamiento.

En los parámetros de la interface genérica JpaRepository se agrega el tipo de la entidad (en este caso BookEntity) y el tipo de dato de la clave primaria (en este caso Long).

Con lo anterior sería suficiente. No obstante, es posible que queramos hacer métodos específicos de búsqueda en nuestra base de datos, por ejemplo con la estructura SELECT * FROM Tabla WHERE columna=parámetro. En este caso debemos crear las definiciones de esos métodos en la interfaz.

Como podemos observar en el ejemplo, se ha agregado la búsqueda de un libro a partir de su isbn. Este es un método adicional al método que trae la interface JpaRepository para buscar un libro por su llave primaria (findById). Para esto declaramos el método findByIsbn el cual es implementado automáticamente por la clase que implementa nuestra interface BookRepository y que es creada por el framework. Para que la implementación sea automática se usa el prefijo findBy y luego se agrega el nombre del atributo por el que se quiere hacer la búsqueda en notación Pascal Case.

De este modo ya hemos implementado la entidad y la persistencia de esa entidad. Esto mismo debe hacerlo con todas las clases resultantes para su proyecto.

Luego de implementar la persistencia y asegurarse de que el proyecto se haya ejecutado correctamente se debe verificar la creación de las tablas en la base de datos.

Para esto ingrese en un navegador a la dirección http://localhost:8080/api/h2-console

En la ventana que aparece a continuación se pedirán tres valores: JDBC URL, User Name y Password. La información para estos valores la puede consultar en el código fuente de su proyecto, en el archivo src/main/resources/application.properties. Para el caso del proyecto de ejemplo del curso, los valores son los siguientes:

spring.datasource.url=jdbc:h2:mem:bookstore (este es el valor para JDBC URL)

spring.datasource.username=sa (este es el valor para User Name)

spring.datasource.password=password (este es el valor para password)

Al ingresar, en la parte izquierda de la pantalla podrá observar las tablas de las entidades y las asociaciones que fueron creadas: