Рассмотрим варианты реализации кастомных методов в репозиториях 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<?> 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 строчку (.orderBy(QueryUtils.toOrders(sort, root, cb));
), нам не нужно мапить 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.
Как реализовать такую же функциональность в предыдущих версиях, смотрите в документации.
Изменение названия кастомного репозитория
Спринг находит реализацию кастомного репозитория по постфиксу 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> {
}
?? Джуниор