Кастомные методы в JPA репозиториях

от автора

Рассмотрим варианты реализации кастомных методов в репозиториях Spring Data JPA.
Большая часть информации для статьи взята из документации.

Допустим у нас есть такая сущность и репозиторий для нее:

@Entity @Table(name = "posts") @Data public class Post {      @Id     @GeneratedValue     private Long id;      private String title; }
public interface PostRepository extends JpaRepository<Post, Long> { }

Будем добавлять кастомный метод, который находит сущности по их id с сортировкой и графом загрузки:

List<Post> findAllByIdIn(Collection<Long> ids, Sort sort, EntityGraph<?>);

Добавление метода для одного репозитория

Чтобы добавить метод в один репозиторий нужно просто создать новый интерфейс с нужным методом и сделать его реализацию:

public interface PostCustomRepository {     List<Post> findAllByIdIn(Collection<Long> ids, Sort sort, EntityGraph<?> entityGraph); }
public class PostCustomRepositoryImpl implements PostCustomRepository {      @Override     public List<Post> findAllByIdIn(Collection<Long> ids, Sort sort, EntityGraph<?> entityGraph) {         // TODO     } }

Далее нужно унаследовать оригинальный репозиторий от кастомного интерфейса:

public interface PostRepository extends JpaRepository<Post, Long>, PostCustomRepository { }

После этого мы сможем вызывать кастомный метод через интерфейс оригинального репозитория:

@Service @RequiredArgsConstructor public class PostService {      @PersistenceContext     private EntityManager em;      private final PostRepository postRepo;      @Transactional(readOnly = true)     public List<Post> findAllByIdIn(Collection<Long> ids) {         return postRepo.findAllByIdIn(ids, Sort.by("id"), em.getEntityGraph("just_an_example"));     } }

Реализация кастомного метода

Теперь давайте реализуем кастомный метод:

import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; import org.hibernate.jpa.SpecHints; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.stereotype.Component;  import java.util.Collection; import java.util.List;  @Component public class PostCustomRepositoryImpl implements PostCustomRepository {      @PersistenceContext     private EntityManager em;      @Override     public List<Post> findAllByIdIn(Collection<Long> ids, Sort sort, EntityGraph<?> entityGraph) {         CriteriaBuilder cb = em.getCriteriaBuilder();         CriteriaQuery<Post> cq = cb.createQuery(Post.class);         Root<Post> root = cq.from(Post.class);          cq.select(root)                 .where(root.get("id").in(ids))                 .orderBy(QueryUtils.toOrders(sort, root, cb));          return em.createQuery(cq).setHint(SpecHints.HINT_SPEC_FETCH_GRAPH, entityGraph).getResultList();     } }

Обратите внимание на 29 строчку, нам не нужно мапить Sort к Order вручную, мы пользуемся утильным классом из data jpa, но тут есть нюанс.
Спринговый Sort поддерживает указание приоритета для налов (ORDER BY id NULLS FIRS/LAST). Например, мы можем явно указать, что налы должны идти в конце:

 // Sort.Order и Order из JPA это разные классы, просто называются одинаково  Sort.Order order = new Sort.Order(Sort.Direction.ASC, "id", Sort.NullHandling.NULLS_LAST);  Sort sort = Sort.by(order);

Order из JPA поддерживает указание приоритета только с версии 3.2, на которую спринг еще не перешел, поэтому при мапинге информация о приоритете налов просто проигнорируется.
Это можно исправить, используя чистый хибернейт, а не JPA, либо через костыли. Но поддержка новой версии JPA должна появится уже в spring data jpa 4.

Следующие разделы актуальны только для версии 3.4.

Как реализовать такую же функциональность в предыдущих версиях, смотрите в документации.

Изменение названия кастомного репозитория (3.4)

Спринг находит реализацию кастомного репозитория по постфиксу Impl в названии, при этом кастомные реализация и интерфейс должны быть в одном пакете.Если эти правила не соблюдены, при запуске проекта будет выброшено исключение.

Но можно явно указать соответствие интерфейса и реализации в файле /src/main/resources/META-INF/spring.factories

Например:

com.example.demo.post.PostCustomRepository=com.example.demo.post.DefaultPostCustomRepository

Общая реализация для всех репозиториев

Допустим, мы хотим, добавить метод findAllByIdIn во все репозитории, но каждый раз писать кастомную реализацию не очень хочется.Рассмотрим, как можно написать общую реализацию для всех репозиториев.

Создадим интерфейс:

public interface IdRepository<T, ID> {     List<T> findAllByIdIn(Collection<ID> ids, Sort sort, EntityGraph<?> entityGraph); }

Реализация:

import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; import org.hibernate.jpa.SpecHints; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.repository.core.RepositoryMethodContext; import org.springframework.data.repository.core.support.RepositoryMetadataAccess; import org.springframework.stereotype.Component;  import java.util.Collection; import java.util.List;  @Component public class IdRepositoryImpl<T, ID> implements IdRepository<T, ID>, RepositoryMetadataAccess {      @PersistenceContext     private EntityManager em;      @Override     public List<T> findAllByIdIn(Collection<ID> ids, Sort sort, EntityGraph<?> entityGraph) {         CriteriaBuilder cb = em.getCriteriaBuilder();         Class<T> domainType = (Class<T>) RepositoryMethodContext.getContext().getMetadata().getDomainType();         CriteriaQuery<T> cq = cb.createQuery(domainType);         Root<T> root = cq.from(domainType);          cq.select(root)                 .where(root.get("id").in(ids))                 .orderBy(QueryUtils.toOrders(sort, root, cb));          return em.createQuery(cq).setHint(SpecHints.HINT_SPEC_FETCH_GRAPH, entityGraph).getResultList();     } }

Мы реализуем RepositoryMetadataAccess — это интерфейс-маркер, позволяющий нам получать информацию о сущности, с которой работает репозиторий, с помощью RepositoryMethodContext.

После этого любой репозиторий, который наследуется от IdRepository, сможет использовать findAllByIdIn:

public interface PostRepository extends JpaRepository<Post, Long>, IdRepository<Post, Long> { }

👨‍💻 Джуниор


ссылка на оригинал статьи https://habr.com/ru/articles/863234/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *