👨‍💻 Spring Tips: Кастомные реализации репозиториев



Spring Data упрощает создание запросов, но стандартные методы не всегда подходят под конкретные задачи. В таких случаях мы можем создать собственные реализации методов репозиториев. Например, если мы хотим, чтобы запрос формировался динамически на основе фильтра и возвращал DTO с меньшим количеством полей, нам понадобится создать fragment interface и кастомный метод с собственной реализацией.





//Кастомный интерфейс

public interface CustomizedUserRepository {

List<UserDto> findAllUsers(UserFilter filter);

}



//Реализация нашего интерфейса, которая будет использоваться Spring'ом

public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

private final EntityManager em;



public CustomizedUserRepositoryImpl(JpaContext jpaContext) {

em = jpaContext.getEntityManagerByManagedType(User.class);

}



//Реализация нашего метода

@Override

public List<UserDto> findAllUsers(UserFilter filter) {

var cb = em.getCriteriaBuilder();

var query = cb.createQuery(UserDto.class);



var root = query.from(User.class);

var emailPath = root.<String>get(User_.EMAIL);

var usernamePath = root.<String>get(User_.USERNAME);

query.multiselect(root.get(User_.ID), emailPath, usernamePath);



var predicates = new ArrayList<Predicate>();



var email = filter.email();

if (StringUtils.hasLength(email)) {

predicates.add(cb.like(cb.lower(emailPath), "%" + email.toLowerCase() + "%"));

}



var username = filter.username();

if (StringUtils.hasLength(username)) {

predicates.add(cb.like(cb.lower(usernamePath), "%" + username.toLowerCase() + "%"));

}



query.where(predicates.toArray(new Predicate[]{}));

return em.createQuery(query).getResultList();

}

}



//Использование кастомного интерфейса

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {}





Теперь при инжекции UserRepository, кастомный метод будет доступен для вызова:





List<UserDto> users = userRepository.findAllUsers(new UserFilter("Maksim", null));





Важные замечания



1. CustomizedUserRepositoryImpl — полноценный Spring-бин, поддерживающий инжекцию других бинов и специфическую функциональность (AOT, Lifecycle Callbacks и т.д.).



2. Инжекция UserRepository может привести к циклической зависимости. Чтобы избежать этого, его можно получить через ApplicationContext.getBean().



3. Spring пытается автоматически обнаружить пользовательские fragment интерфейсы, если классы следуют соглашению об именовании с постфиксом Impl. Модифицировать значение по умолчанию можно через атрибут аннотации @EnableJpaRepositories:





@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")





4. Репозитории могут включать несколько пользовательских реализаций, которые имеют более высокий приоритет, чем базовая реализация, что позволяет переопределять базовые методы. Например, создадим кастомный интерфейс и переопределим метод save:





public interface CustomizedSave<T> {

<S extends T> S save(S entity);

}



class CustomizedSaveImpl<T> implements CustomizedSave<T> {

@Override

public <S extends T> S save(S entity) {

// наша кастомная реализация

}

}





Теперь, если мы объявим следующий репозиторий:





interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {}





то при вызове метода userRepository.save(user) будет использован метод из нашей реализации CustomizedSaveImpl.



5. В примерах выше мы рассматривали Spring Data JPA, но эта концепция поддерживается для всех модулей Spring Data (MongoDB, Redis, JDBC и т.д.).



Подробнее про реализацию кастомных репозиториев читайте в документации.



#SpringBoot #SpringTips #CustomRepository