Curso Spring Data JPA. 15: repositorios personalizados (métodos con cuerpo)

logo spring

Spring Data en una herramienta irresistible por la simplicidad de sus repositorios: interfaces con métodos sin cuerpo. Esto nos ahorra mucho código.

¿Y si las capacidades de esos métodos fueran insuficientes? Necesitaremos escribir código. ¡Ningún problema! En este breve capítulo te explico cómo incorporar cualquier método con cuerpo a una interfaz-repositorio. Así, seráz capaz de programar lo que quieras sin salir de Spring Data.



Contenido

  1. Caso de uso
  2. Agregar un método con cuerpo a un repositorio concreto
    1. El método
    2. Los pasos
  3. Código de ejemplo


Caso de uso

Imagina que quieres construir consultas programáticas con jOOQ, API Criteria, etcétera, y que Spring Data carece de un mecanismo de integración o bien este no contempla lo que quieres hacer. O quizás necesites crear un método que precise del gestor de entidades de JPA (la interfaz EntityManager) o la API SpringJdbcTemplate. En definitiva, tienes que programar esas acciones en métodos con cuerpo, pues no te sirven los métodos de los repositorios (consultas derivadas, @Query, @Procedure…).

Una posible solución. Como cualquier interfaz, un repositorio admite métodos default. En el capítulo 9 usé el siguiente método para superar un problemilla de rendimiento del mecanismo de paginación de JPA con las reuniones de tipo JOIN FETCH:

public interface ConfederationPagingRepository extends Repository<Confederation, Long> {    

  default Page<Confederation> findAllWithCountryPagingTwoQueries(Pageable pageable) {
        Page<Long> idsPage = findConfederationsWithCountry(pageable);
        List<Confederation> confederationsPage = findAllConfederation(idsPage.getContent(), idsPage.getSort());
        return new PageImpl<>(confederationsPage, pageable, idsPage.getTotalElements());
    }

Aunque esta estrategia funcionó, resulta limitante. En una interfaz no podemos inyectar, por ejemplo, el gestor de entidades. Podríamos dárselo como un parámetro al método de la interfaz que lo requiera, pero resulta inapropiado desde el punto de vista del diseño. El código de acceso a los datos debe encapsularse en las clases correspondientes, las únicas que deberían llamar a los métodos de EntityManager.

Entonces, ¿qué hacemos?

Pues ir por libre. Nos olvidamos de Spring Data y creamos una clase de tipo DAO. Los métodos de los DAO encapsulan las operaciones que interactúan con la base de datos; por tanto, las clases DAO interpretan el mismo papel que los repositorios. En ellas inyectamos el gestor de entidades, o cualquier bean de Spring que sea preciso, e implementamos cualquier operación sin las restricciones (y los beneficios) de los repositorios.

El precio de esta solución es la incoherencia, ya que los métodos de persistencia se reparten entre las clases DAO y los repositorios. Si bien podemos vivir con ello, lo ideal sería que los repositorios contengan todos esos métodos.

Por fortuna, es posible agregar a un repositorio los métodos de un bean de Spring. Y además resulta sencillo con las instrucciones que voy a darte.

Agregar un método con cuerpo a un repositorio concreto

El método

Incorporemos a un repositorio para la clase de entidad Country un método que actualiza la población de un país:

@Transactional
public boolean updateCountryPopulation(Long id, Integer population) {
    Country country = em.find(Country.class, id);
    if (country != null) {
        country.setPopulation(population);
        return true;
    }
    return false;
}

Observa que conseguimos lo mismo con una sentencia JPQL de tipo UPDATE, como la del capítulo 6. Sin embargo, nos fastidia el inconveniente que expliqué en ese capítulo: la actualización se lleva a cabo en la base de datos, sin afectar a las entidades ya registradas en los contextos existentes de JPA.

También podríamos crear la operación en una clase de servicio, obteniendo la entidad deseada con un repositorio y cambiando en ella la población. Es justo lo que hace la prueba CountryCrudRepositoryTest# testUpdatePopulation. No obstante, por el motivo que sea, decidimos ofrecer esta operación con un método de un repositorio.

Los pasos

¿Cómo se añade el método anterior a una interfaz-repositorio? Sigue estos pasos:

  1. Crea una interfaz con la definición del método:
package com.danielme.springdatajpa.repository.custom;

public interface CountryCustomRepository {

    boolean updateCountryPopulation(Long countryId, Integer population);

}

Elije el nombre que consideres apropiado. La costumbre es que contenga la palabra Custom (’personalizado’).

  1. Impleméntala en una clase que tenga el nombre de la interfaz más el sufijo Impl:
package com.danielme.springdatajpa.repository.custom;

import com.danielme.springdatajpa.model.entity.Country;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.transaction.annotation.Transactional;

public class CountryCustomRepositoryImpl implements  CountryCustomRepository {

    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public boolean updateCountryPopulation(Long countryId, Integer population) {
        Country country = em.find(Country.class, countryId);
        if (country != null) {
            country.setPopulation(population);
            return true;
        }
        return false;
    }

}

Esta clase es un bean de Spring, así que inyecta en ella lo que necesites. Eso sí, no la marques con un estereotipo, como @Repository o @Component, con el fin de indicar que se trata de un bean. Deja que Spring se encarge de ello.

Sin entrar en detalles, aprovecho el ejemplo para recordarte que debes inyectar el gestor de entidades con la anotación @PersistenceContext de JPA. No uses @Autowired o @Inject.

  1. Agrega el método updateCountryPopulation a un repositorio con tan solo extender de la interfaz CountryCustomRepository:
package com.danielme.springdatajpa.repository.custom;

import com.danielme.springdatajpa.model.entity.Country;
import org.springframework.data.repository.Repository;

public interface CountryExtendedRepository 
      extends Repository<Country, Long>, CountryCustomRepository {
    
    Optional<Country> findById(Long id);

}

¡Listo! CountryExtendedRepository es un repositorio que ofrece los métodos de CountryCustomRepository. Por supuesto, en él puedes crear todos los métodos adicionales que quieras, así como heredar de otras interfaces. De hecho CountryExtendedRepository ya hereda de dos: CountryCustomRepository y Repository.

Esta prueba testea updateCountryPopulation:

@SpringBootTest
class CountryExtendedRepositoryTest {

    @Autowired
    CountryExtendedRepository countryRepository;

    @Test
    void testUpdateCountryPopulation() {
        int newPopulation = 47_558_630;
        boolean wasUpdated = countryRepository.updateCountryPopulation(SPAIN_ID, newPopulation);

        assertThat(wasUpdated).isTrue();
        Optional<Country> spain = countryRepository.findById(SPAIN_ID);
        assertThat(spain)
                .isNotEmpty()
                .map(Country::getPopulation).get().isEqualTo(newPopulation);
    }

El siguiente diagrama muestra con claridad la relación entre los componentes implicados.

Spring Data JPA custom repository

Código de ejemplo

El proyecto, explicado en el capítulo dos, se encuentra en GitHub. Si necesitas ayuda para obtener y configurar proyectos alojados en GitHub, consulta este tutorial.



Otros tutoriales relacionados con Spring

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Spring Boot: Gestión de errores en aplicaciones web y REST

Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas

Spring JDBC Template: simplificando el uso de SQL

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.