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.
Para realizar este tutorial Ud. debe:
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 ( Una revisión le pertenece a un único libro ( |
Book - Author | Un libro puede tener varios autores ( Un autor puede haber escrito varios libros ( |
Book - Editorial | Un libro es editado por una única editorial ( Una editorial puede edita muchos libros ( |
Author - Prize | Un autor puede haber recibido muchos premios o ninguno ( Un premio le pertenece a un único autor (author). |
Prize - Organization | Un premio es otorgado por una única organización ( Una organización otorga un único premio ( |
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:
BaseEntity
(no se incluye esto en el modelo por claridad).Dependiendo del caso, en cada anotación también se debe incluir, entre otros:
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:
@PodamExclude
: indica que no se generarán valores sobre este atributo cuando se utilice Podam.@Id
: indica que el atributo es la llave primaria.@GeneratedValue(strategy = GenerationType.IDENTITY)
: indica que el valor de este atributo será generado en la base de datos cada vez que se ingresa un registro. La estrategia es que el primer objeto ingresado tenga el id 1 y este se incremente en cada registro de la tabla.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.
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:
mappedBy = "book"
para indicar que esta relación que estamos definiendo se refiere a la misma que en la clase ReviewEntity
está definida con el atributo "book". Con respecto a las tablas significa que es la tabla de reviews quien tiene la llave foránea del libro al que corresponde cada review.cascade = CascadeType.PERSIST
significa que cuando se persista un libro con reviews asociadas se persistirán también las reviews. De igual forma, si un libro se elimina se deben eliminar todas sus reviews.orphanRemoval = true
significa que no puede haber una review si no está asociada con un libro; es decir, una review no puede estar "huérfana".Lo anterior implica que cuando se implemente la clase ReviewEntity
deberá tener la siguiente definición del atributo book
:
...
@ManyToOne
private BookEntity book;
...
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.
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: