====== 05 - JPA ====== Hasta ahora, hemos utilizado nuestras propias clases (DBUtil, DAOs...) para conectarnos a la bbdd y tratar los datos, pero ésto no es la forma más habitual de hacerlo. La persistencia de datos en aplicaciones es un aspecto fundamental para el desarrollo de sistemas robustos y escalables. En el entorno de Java, la **Java Persistence API** (**JPA**) ha surgido como una solución estándar para el mapeo objeto-relacional (ORM), ofreciendo a los desarrolladores una forma eficiente y coherente de interactuar con bases de datos relacionales utilizando objetos Java. JPA facilita la representación de entidades de persistencia como objetos en el código Java, permitiendo su almacenamiento y recuperación en una base de datos de manera transparente. Al abstraer la lógica de persistencia, JPA simplifica considerablemente el manejo de la capa de acceso a datos, promoviendo un código más limpio, mantenible y portable entre diferentes proveedores de bases de datos. JPA define la gestión de datos en aplicaciones mediante la representación de objetos en una base de datos relacional. JPA proporciona un conjunto de interfaces y clases abstractas que permiten a los desarrolladores interactuar con la base de datos de una manera orientada a objetos, sin tener que preocuparse por detalles específicos de la implementación subyacente de la base de datos. Existen diferentes implementaciones de JPA, entre las que destacan: * **Hibernate**: Es una de las implementaciones JPA más populares. Ofrece funcionalidades avanzadas y flexibilidad en el mapeo objeto-relacional, además de herramientas adicionales que simplifican el desarrollo y la gestión de la base de datos.\\ \\ * **EclipseLink**: Otra implementación JPA robusta, potente y de alto rendimiento. Ofrece características de mapeo avanzadas, soporte para estándares JPA y herramientas de persistencia útiles.\\ \\ * **Apache OpenJPA**: Una implementación JPA que proviene del proyecto OpenJPA de Apache. Proporciona funcionalidades completas de JPA, cumpliendo con los estándares de la API.\\ \\ Estas implementaciones ofrecen funcionalidades similares en términos de mapeo objeto-relacional y operaciones CRUD, pero pueden diferir en sus características específicas, herramientas adicionales y rendimiento en ciertos contextos. Al utilizar JPA, los desarrolladores pueden escribir código independiente de la base de datos subyacente, lo que les permite cambiar de proveedor de bases de datos sin tener que modificar considerablemente el código de la aplicación, lo que resulta en sistemas más flexibles y mantenibles a largo plazo. En Spring Boot tenemos opciones para trabajar con JPA. Si bien **spring-boot-starter-data-jpa** es una forma común y conveniente de comenzar, podemos personalizar el enfoque dependiendo de las necesidades específicas del proyecto. Por ejemplo, además de //spring-boot-starter-data-jpa//, podríamos optar por configurar JPA manualmente agregando las dependencias necesarias de forma individual. Esto nos ofrece un mayor control sobre cada componente que utilizamos. Además, podemos elegir implementaciones específicas de JPA según nuestras preferencias o requisitos del proyecto. Aunque //Hibernate// es la implementación JPA predeterminada en Spring Boot, podemos optar por otras implementaciones como //EclipseLink// o //Apache OpenJPA//. ===== Añadiendo la dependencia y configurando la conexión ===== En nuestro caso, vamos a utilizar la configuración común de JPA en un proyecto Spring. Al incluir //spring-boot-starter-data-jpa// en un proyecto Spring Boot, obtenemos una configuración predeterminada que facilita la interacción con bases de datos utilizando JPA. Este starter no solo proporciona //Spring Data JPA//, sino que también configura automáticamente otros componentes esenciales como //Spring JDBC//, //Spring Transactions//, //Spring AOP// y //Spring Aspects//, si se requieren para el contexto de persistencia de datos. Como siempre, lo primero que tenemos que hacer es añadir la dependencia de JPA a nuestro archivo pom.xml: org.springframework.boot spring-boot-starter-data-jpa Ahora ya estamos listos para usar JPA. Para conectar con nuestra bbdd tenemos que añadir algunas propiedades en nuestro archivo //application.properties//: # Configuración de Hibernate y base de datos spring.datasource.url=jdbc:mariadb://localhost:3306/movies spring.datasource.username=root spring.datasource.password=root spring.jpa.hibernate.ddl-auto=none # Habilitar logs de consultas SQL spring.jpa.show-sql=true Las 3 primeras líneas son la url de conexión, el usuario y la contraseña con el que nos conectamos a la bbdd. La propiedad **spring.jpa.hibernate.ddl-auto** es una configuración en aplicaciones Spring que determina cómo Hibernate maneja la creación y actualización de la estructura de la base de datos. Tiene varios valores que permiten definir cómo se realiza esta gestión: * **none**: Este valor desactiva cualquier acción automática de creación o actualización de la base de datos por parte de Hibernate. Con este modo, Hibernate no hace ningún cambio en la estructura de la base de datos existente.\\ \\ * **create**: Al utilizar este valor, Hibernate creará la estructura de la base de datos si no existe. Si la base de datos ya está presente, Hibernate la eliminará y la volverá a crear, lo que conlleva la pérdida de datos existentes.\\ \\ * **create-drop**: Similar a create, Hibernate crea la estructura de la base de datos si no existe. Sin embargo, al cerrar la sesión de Hibernate o detener la aplicación, Hibernate eliminará la base de datos, lo que puede ser útil para entornos de desarrollo o pruebas.\\ \\ * **update**: Este valor indica a Hibernate que actualice la estructura de la base de datos según los cambios en las entidades. No elimina los datos existentes, pero puede modificar o eliminar columnas, tablas, etc., para reflejar los cambios en el modelo de datos. La última línea (//spring.jpa.show-sql=true//) es una configuración que se utiliza en aplicaciones Spring con Hibernate como implementación JPA. Cuando se establece a //true//, esta propiedad le indica a Hibernate que imprima las consultas SQL generadas por la aplicación en la consola. Ésto nos será útil más adelante para entender como funciona la carga perezosa (//lazy loading//) en Hibernate. ===== Entidades ===== JPA utiliza sus propias entidades que mapean una tabla de la bbdd. Para ello, sólo tenemos que añadir la anotación **@Entity** a nuestras entidades: @Entity @Data @NoArgsConstructor public class MovieEntity { El problema, es que nuestras entidades llevan el sufijo //Entity// para distinguirlas de las entidades de dominio. Por suerte, la solución es muy sencilla: indicarle el nombre de la tabla mediante la anotación **@Table**: @Entity @Table(name = "movies") @Data @NoArgsConstructor public class MovieEntity { Lo siguiente será añadir anotaciones a nuestros campos para indicar algunas propiedades para que JPA sea capaz de mapear directamente desde la bbdd. En primer lugar, añadiremos **@ID** y **@GeneratedValue**. La anotación //@GeneratedValue// en JPA, junto con //@Id//, se utiliza para especificar cómo se generan los valores para una clave primaria en una entidad persistente. En particular, //@GeneratedValue// se usa para indicar la estrategia que se utilizará para generar los valores de las claves primarias de manera automática por parte de la base de datos. La estrategia **GenerationType.IDENTITY** especifica que la generación de valores de clave primaria se realizará utilizando una columna de identidad de la base de datos, lo que significa que la base de datos se encargará de generar automáticamente valores únicos para la clave primaria cuando se inserten nuevas filas en la tabla asociada a la entidad: @Entity @Table(name = "movies") @Data @NoArgsConstructor public class MovieEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private int year; private int runtime; Vamos a crear ahora el resto de entidades. Empezaremos con los directores: @Entity @Table(name = "directors") @Data @NoArgsConstructor public class DirectorEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; private int birthYear; private Integer deathYear; } Si lo dejamos así, veremos que JPA no es capaz de rellenar los campos //birthYear// y //deathYear//. Normal, ya que en la bbdd los campos se llaman //birth_year// y //death_year//. Por suerte, contamos con la anotación **@Column** para indicarle el nombre de la columna en la bbdd. Además, podemos añadir el atributo **nullable** para indicar que ese campo puede contener nulos: @Entity @Table(name = "directors") @Data @NoArgsConstructor public class DirectorEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; @Column(name = "birth_year") private int birthYear; @Column(name = "death_year", nullable = true) private Integer deathYear; } Nuestra entidad actores será casi igual: @Entity @Table(name = "actors") @Data @NoArgsConstructor public class ActorEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; @Column(name = "birth_year") private int birthYear; @Column(name = "death_year", nullable = true) private Integer deathYear; } ==== Relaciones entre entidades ==== Vamos a voler a //MovieEntity//. Por ahora, hemos creado los campos básicos de la tabla: //id//, //title//, //year// y //runtime// ¿Cómo añadimos la relación con los directores y los personajes? Para hacerlo, contamos con una serie de anotaciones según el tipo de relación que queramos representar: * **@OneToOne**: Define una relación uno a uno entre dos entidades.\\ \\ * **@OneToMany**: Define una relación de uno a muchos, donde una entidad tiene una asociación con múltiples entidades de otro tipo.\\ \\ * **@ManyToOne**: Establece una relación muchos a uno, indicando que varias entidades de una clase están relacionadas con una única entidad de otra clase.\\ \\ * **@ManyToMany**: Define una relación muchos a muchos entre dos entidades, lo que implica que una entidad puede estar asociada con múltiples entidades de otro tipo y viceversa. En general, no queremos relaciones bidireccionales, con lo que vamos a añadir, por ahora, la anotación //@ManyToOne// en //MovieEntity// para indicar la relación con //DirectorEntity//: @Entity @Table(name = "movies") @Data @NoArgsConstructor public class MovieEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private int year; private int runtime; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "director_id") private DirectorEntity directorEntity; Fíjate en el código de arriba. Hemos añadido la opción **fetch = FetchType.LAZY** para indicar que queremos que ese campo se recupere con carga perezosa (//lazy loading//). Además, hemos indicado el campo por el que están relacionadas ambas tablas mediante la anotación **@JoinColumn**. Si quisiésemos añadir la relación entre actores y películas (sin tener en cuenta los personajes), podríamos hacerlo usando la anotación **@JointTable**, donde indicaríamos la tabla intermedia con las columnas de enganche: @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "actors_movies", joinColumns = @JoinColumn(name = "movie_id"), inverseJoinColumns = @JoinColumn(name = "actor_id") ) private List actorEntities; En cualquier caso, nosotros vamos a crear una relación con la entidad CharacterMovieEntity, de forma similar a como lo hicimos con los directores, aunque cambiando el tipo de relación a //@OneToMany//. Para eso, primero modificamos la entidad de personajes para relacionarla con los actores: @Entity @Table(name = "actors_movies") @Data @NoArgsConstructor public class CharacterMovieEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "actor_id") private ActorEntity actorEntity; } Ahora, añadimos la relación en la entidad de películas: @Entity @Table(name = "movies") @Data @NoArgsConstructor public class MovieEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private int year; private int runtime; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "director_id") private DirectorEntity directorEntity; @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "movie_id") private List characterMovieEntities; Fíjate que en la nueva relación estamos indicando que el campo //movie_id// de nuestra tabla //actors_movies// se relaciona con el campo //id// de la tabla //movies//. En el caso de las relaciones //@OneToMany// el atributo //name// hace referencia al nombre de la clave ajena de la segunda tabla. Además, indicamos que los cambios (actualizaciones y borrados) queremos que sean en cascada. La opción **orphanRemoval = true** nos servirá para indicar que borre los registros de la tabla //actors_movies// cuando se borre una película. Ésto es debido a que, al no haber una relación bidireccional entre //MovieEntity// y //CharacterMovieEntity//, JPA actualiza la tabla //movie_actors// con //movie_id = null// antes de hacer cualquier cambio. En nuestro caso, estamos conectándonos a una bbdd que ya existe. Si quisiésemos que JPA creara la bbdd de forma automática, tendríamos que cambiar el valor de //spring.jpa.hibernate.ddl-auto// a //create//, por ejemplo.\\ \\ Pruedes probar a crear la bbdd (con otro nombre) de forma automática para comprobar que te crea las mismas tablas (con las mismas claves foráneas) que la nuestra original. A veces, tu configuración del SGBD no permite crear la bbdd de forma automática. Una solución sencilla es crear la bbdd desde fuera y ejecutar Spring para crear las tablas.\\ \\ Aunque Hibernate puede inferir el dialecto de la base de datos basándose en la URL de conexión, especificar explícitamente el dialecto a través de //spring.jpa.properties.hibernate.dialect// es una práctica recomendada para asegurar una configuración precisa y completa. Por ejemplo, en nuestro caso podríamos añadir: spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect spring.jpa.hibernate.ddl-auto=create ===== Repositorios ===== Ahora que ya tenemos creadas nuestras entidades es hora de hacer lo mismo con los repositorios. Aunque en JPA las clases que tratan directamente con los datos se llaman repositorios, nosotros vamos a mantener el nombre de DAOs (acuérdate que los repositorios trabajan con los modelos de datos de la capa de negocio, con lo que no tienen nada que ver con las bbdd). Por lo tanto, lo que tendremos que hacer es modificar nuestros DAO para hacer que hereden de **JPARepository** indicando la entidad que manejan y el tipo de datos de la clave primaria de esa entidad. Además, ya no serán clases, si no interfaces: @Repository public interface MovieDAO extends JpaRepository { } Como ves, //Spring Data JPA// simplifica enormemente la interacción con la base de datos al proporcionar una implementación predeterminada de métodos CRUD en la interfaz //JpaRepository//. Cuando extiendes esta interfaz para crear tu repositorio, obtienes métodos como //save//, //findById//, //findAll//, //delete//, //count//, entre otros, listos para ser usados sin necesidad de escribir la implementación de cada uno. Además de estos métodos predefinidos, puedes definir métodos en tu interfaz de repositorio siguiendo una convención de nombres específica. //Spring Data JPA// analiza el nombre del método y genera consultas SQL correspondientes automáticamente. Por ejemplo, si quisiésemos buscar películas por título, bastaría con declarar el método en nuestra interfaz MovieDAO: @Repository public interface MovieDAO extends JpaRepository { List findByTitle(String title); } Spring Data JPA interpretará este método y generará una consulta SQL para buscar películas por título. La convención de nombres juega un papel crucial aquí: el prefijo //findBy// indica que quieres buscar entidades por un campo específico (title en este caso). Spring Data JPA analiza el nombre del método, separa findBy, interpreta Title como el nombre del campo y genera la consulta correspondiente. Esta abstracción ahorra tiempo y esfuerzo al eliminar la necesidad de escribir consultas SQL repetitivas. En lugar de preocuparte por la sintaxis SQL, puedes centrarte en definir métodos descriptivos en tu interfaz de repositorio para manejar las consultas de manera más intuitiva y enfocarte en la lógica de tu aplicación. Ten en cuenta que para que ésto funcione tienes que seguir la nomenclatura de Spring Data JPA para los métodos. [[https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html|Aquí]] puedes ver una referencia de esa nomenclatura. Otra cosa importante es que los métodos //findBy// en //Spring Data JPA// devuelven un //Optional// en lugar de una instancia directa de la entidad. Vamos a probar a devolver un listado de películas. Pero antes, vamos a hacer un pequeño cambio en los mapeadores para entender como funciona la carga perezosa con Spring. Por ahora, comenta todos los mapeadores de la clase //MovieMapper// y deshabilita la búsqueda por id, inserción, actualización y borrado de las mismas en el controlador y servicio (haz que el repositorio devuelva null o 0 en esos métodos, por ejemplo). Primero, modificamos el mapeador de //MovieEntity// a //Movie// para indicarle que el director y los personajes sean nulos. En cuanto al mapeo de //Movie// a //MoveListWeb// lo dejamos como está. Aprovecharemos también para crear un método que mapee un listado de //MovieEntity// a un listado de //Movie//: @Mapping(target = "director", ignore = true) @Mapping(target = "characterMovies", ignore = true) Movie toMovie(MovieEntity movieEntity); @Mapping(target = "director", ignore = true) @Mapping(target = "characterMovies", ignore = true) List toMovieList(List movieEntities); MovieListWeb toMovieListWeb(Movie movie); Además, vamos a modificar el método //getTotalNumberOfRecords()// del repositorio para usar el método predefinido //MovieDAO.count()//. El problema es que ese método devuelve un tipo //long// en lugar de //int//, con lo que nos tocará modificarlo en el repositorio, servicio y controlador: Repositorio: @Override public long getTotalNumberOfRecords() { return movieDAO.count(); } Servicio: @Override public long getTotalNumberOfRecords() { return movieRepository.getTotalNumberOfRecords(); } Controlador: @ResponseStatus(HttpStatus.OK) @GetMapping("") public Response getAll(@RequestParam(required = false) Integer page, @RequestParam(required = false) Integer pageSize) { pageSize = (pageSize != null)? pageSize : PAGE_SIZE; List movies = (page != null)? movieService.getAll(page, pageSize) : movieService.getAll(); List moviesWeb = movies.stream() .map(MovieMapper.mapper::toMovieListWeb) .toList(); long totalRecords = movieService.getTotalNumberOfRecords(); Response response = Response.builder() .data(moviesWeb) .totalRecords(totalRecords) .build(); if(page != null) { response.paginate(page, pageSize, urlBase); } return response; } En nuestra implementación del repositorio simplemente tenemos que llamar al método //findAll// del DAO. ¿Y cómo implementamos la paginación? Por suerte, JPA ya tiene implementado esa opción pasándole un objeto de tipo [[https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html|Pageable]] al método. Podemos utilizar el método //of()// de la clase [[https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/PageRequest.html|PageRequest]] que implementa la interfaz anterior para crear el objeto //Pageable//. El problema es que //JPA// asume que la primera página es la 0, con lo que si queremos mantener el orden (primera página = 1), deberemos restar uno al número de página que nos pasen: @Override public List getAll(Integer page, Integer pageSize) { List movieEntities; if(page != null && page > 0) { Pageable pageable = PageRequest.of(page - 1, pageSize); movieEntities = movieDAO.findAll(pageable).stream().toList(); } else { movieEntities = movieDAO.findAll(); } return MovieMapper.mapper.toMovieList(movieEntities); } Si todo ha ido bien, deberías poder ser capaz de listar las películas con su //id//, título y el número de registros: {{ :clase:daw:dws:2eval:jpa01.png?400 |}} Si te fijas en la terminal del IDE, nos muestra las sentencias SQL que se ejecutan, al tener esa opción habilitada en //application.properties//: {{ :clase:daw:dws:2eval:jpa02.png?400 |}} Vamos ahora con la búsqueda de una pelicula. En principio, es bastante sencillo, ya que sólo debemos llamar al método //findById()// de JPA y mapear lo que nos devuelva (//Optional//) a //Movie//: @Override public Optional find(int id) { return Optional.ofNullable(MovieMapper.mapper.toMovie(movieDAO.findById(id).get())); } Probamos la salida: {{ :clase:daw:dws:2eval:jpa03.png?400 |}} Pero nosotros queremos mostrar también los datos del director y los personajes. Para eso, vamos a ver como funciona la carga perezosa en //JPA//. ==== Lazy Loading ==== Como sabes, la carga perezosa (//lazy loading//) es un mecanismo por el cual sólo accedemos a los recursos cuando los necesitamos, con lo que, en este caso, nos ahorramos hacer sentencias SQL de más, ya que sólo traeremos los datos del director y personajes de una película cuando los vayamos a necesitar. Si ves las sentencias SQL que se han ejecutado cuando se trae una película por //id//, verás que sólo ha ejecutado una, ya que en las entidades hemos indicado que queremos carga perezosa para los recursos director y personajes: {{ :clase:daw:dws:2eval:jpa04.png?400 |}} Vamos a probar la carga perezosa. En nuestro repositorio accedemos al método //getDirectorEntity()// para acceder a ese recurso: @Override public Optional find(int id) { MovieEntity movieEntity = movieDAO.findById(id).orElse(null); if(movieEntity != null) { movieEntity.getDirectorEntity(); } return Optional.ofNullable(MovieMapper.mapper.toMovie(movieEntity)); } Vemos las sentencias que se ejecutan: {{ :clase:daw:dws:2eval:jpa05.png?400 |}} ¿Qué está pasando? Parece ser que no está funcionando la carga perezosa, ya que no se trae los datos del director. En realidad, sí que está funcionando, pero todavía no hemos accedido a ningún dato del director. Por ejemplo, vamos a acceder al nombre del director: @Override public Optional find(int id) { MovieEntity movieEntity = movieDAO.findById(id).orElse(null); if(movieEntity != null) { movieEntity.getDirectorEntity().getName(); } return Optional.ofNullable(MovieMapper.mapper.toMovie(movieEntity)); } {{ :clase:daw:dws:2eval:jpa06.png?400 |}} Ahora sí que ejecuta la sentencia SQL para traerse los datos del director. Como ves, //JPA// intenta no acceder a los datos hasta que son realmente necesarios. Sabiendo eso, volvemos a dejar el método como estaba y cambiamos nuestro mapeador para traer los datos del director y de los personajes: @Override public Optional find(int id) { MovieEntity movieEntity = movieDAO.findById(id).orElse(null); if(movieEntity == null) { return Optional.empty(); } return Optional.of(MovieMapper.mapper.toMovie(movieEntity)); } @Mapping(target = "director", expression = "java(DirectorMapper.mapper.toDirector(movieEntity.getDirectorEntity()))") @Mapping(target = "characterMovies", expression = "java(CharacterMovieMapper.mapper.toCharacterMovies(movieEntity.getCharacterMovieEntities()))") Movie toMovie(MovieEntity movieEntity); Perfecto, ahora vemos los datos del director y personajes, además de las sentencias ejecutadas correctamente: {{ :clase:daw:dws:2eval:jpa07.png?400 |}} {{ :clase:daw:dws:2eval:jpa08.png?400 |}} Vuelve ahora a cargar el listado de películas y mira las sentencias SQL que se ejecutan: {{ :clase:daw:dws:2eval:jpa09.png?400 |}} ¡Está ejecutando todas las sentencias SQL de directores y personajes por cada película del listado! En realidad, ésto tiene que ver con como funciona //MapStruct//. Si ves la implementación que crea, llama a nuestro método //toMovie// cuando mapea el listado de películas: public List toMovieList(List movieEntities) { if (movieEntities == null) { return null; } else { List list = new ArrayList(movieEntities.size()); Iterator var3 = movieEntities.iterator(); while(var3.hasNext()) { MovieEntity movieEntity = (MovieEntity)var3.next(); list.add(this.toMovie(movieEntity)); } return list; } } Por suerte, hay una forma muy sencilla de solucionarlo. Cambiamos el nombre al método //toMovie// por //toMovieWithDirectorAndCharacterMovies//. Creamos otro método //toMovie// ignorando el director y los personajes. Asignamos nombres específicos a los métodos de nuestro mapeador con //@Named// y le indicamos al método //toMovieList// que utilice el que nos interesa con //@IterableMapping//: @Mapping(target = "director", ignore = true) @Mapping(target = "characterMovies", ignore = true) @Named("toMovie") Movie toMovie(MovieEntity movieEntity); @Mapping(target = "director", ignore = true) @Mapping(target = "characterMovies", ignore = true) @IterableMapping(qualifiedByName = "toMovie") @Named("toMovieList") List toMovieList(List movieEntities); @Mapping(target = "director", expression = "java(DirectorMapper.mapper.toDirector(movieEntity.getDirectorEntity()))") @Mapping(target = "characterMovies", expression = "java(CharacterMovieMapper.mapper.toCharacterMovies(movieEntity.getCharacterMovieEntities()))") @Named("toMovieWithDirectorAndCharacterMovies") Movie toMovieWithDirectorAndCharacterMovies(MovieEntity movieEntity); Sólo nos falta cambiar el mapeador que utilizaremos en el método //find// del repositorio: @Override public Optional find(int id) { MovieEntity movieEntity = movieDAO.findById(id).orElse(null); if(movieEntity == null) { return Optional.empty(); } return Optional.of(MovieMapper.mapper.toMovieWithDirectorAndCharacterMovies(movieEntity)); } Listo. Ahora sí que debería ejecutar las sentencias SQL de búsqueda del director y personajes sólo cuando accedamos al detalle de una película. ==== Actualizaciones, inserciones y borrados ==== Para actualizar o insertar recursos, JPA utiliza el método **save()**. Lo único que tenemos que hacer en las implementaciones de los repositorios es mapear nuestros modelos de datos de la capa de dominio a entidades de JPA y guardar el recurso: @Override @Transactional public Movie insert(Movie movie) { MovieEntity movieEntity = movieDAO.save(MovieMapper.mapper.toMovieEntity(movie)); return MovieMapper.mapper.toMovieWithDirectorAndCharacterMovies(movieEntity); } En este caso, JPA devuelve el recurso recién creado/actualizado, con lo que en ambos casos podríamos devolver en el controlador dicho recurso en formato JSON (probablemente te tocará modificar el servicio y el controlador para indicar el tipo de recurso que devuelve cada método). Hay que tener en cuenta que para que todo funcione de manera correcta, el modelo de la capa de negocios debe estar completo (en nuestro caso, con el director y los personajes) para que JPA pueda establecer las relaciones entre las entidades. El método para actualizar recursos sería casi igual. Aquí podríamos optar por agrupar ambos métodos en uno (llamado save() como JPA, por ejemplo) o mantenerlos separados por si queremos realizar acciones diferentes en caso de actualización/inserción. Fíjate que añadimos la etiqueta //@Transactional// al método. Con ésto, indicamos a la aplicación que las operaciones tienen que ser atómicas: o se realizan todas, o no se realiza ninguna. De esta forma, en caso de fallo no quedará la bbdd con datos inconsistentes. El borrado es igual de sencillo, simplemente tendremos que llamar al método delete(): @Override @Transactional public void delete(Movie movie) { movieDAO.delete(MovieMapper.mapper.toMovieEntity(movie)); }