Применение спецификации Spring JPA к нескольким репозиториям и запросам - PullRequest
0 голосов
/ 20 ноября 2018

У меня следующая ситуация:

Мой проект содержит несколько сущностей, каждая со своим соответствующим контроллером, сервисом и репозиторием JPA.Все эти объекты связаны с конкретной компанией свойством «companyUuid».

Каждый входящий запрос в моих контроллерах будет иметь заголовок «user», в котором будет указана подробная информация о пользователе, выполняющем этот запрос,включая, с какой компанией он связан.

Мне нужно извлечь из заголовка компанию, связанную с пользователем, и отфильтровать каждый последующий запрос этой компании, что по сути будет похоже на добавление WHERE companyUuid = ... к каждому запросу.

В качестве решения я применил универсальную функцию для создания объекта спецификации:

public class CompanySpecification {

public static <T> Specification<T> fromCompany(String companyUuid) {
    return (e, cq, cb) -> cb.equal(e.get("companyUuid"), companyUuid);
}}

Реализовал репозиторий следующим образом:

public interface ExampleRepository extends JpaRepository<Example, Long>, JpaSpecificationExecutor<Example> { }

Изменены вызовы "find"включить спецификацию:

exampleRepository.findAll(CompanySpecification.fromCompany(companyUuid), pageRequest);

Конечно, для добавления пользователя в заголовок необходимо добавить @RequestHeader к функциям контроллера.

Хотя это решение работает абсолютно нормально, оно быдля выполнения всех маршрутов моего @RestControllers.

требуются большие объемы копирования и повторения кода.О, вопрос в том, как я могу сделать это элегантно и чисто для всех моих контроллеров?

Я уже довольно много исследовал это и пришел к следующим выводам:

  1. Spring JPA и Hibernate, по-видимому, не обеспечивают способ динамического использования спецификации для ограничения всех запросов (ссылка: Автоматически добавляйте критерии при каждом вызове репозитория Spring Jpa )
  2. SpringMVC HandlerInterceptor может помочь в выводе пользователя из заголовка в каждом запросе, но, похоже, он не подходит в целом, так как я не использую представления в этом проекте (это просто бэкэнд), и он может 'ничего не делать с моими запросами к репозиторию
  3. Spring AOP показался мне отличным вариантом, и я попробовал.Мое намерение состояло в том, чтобы сохранить все вызовы репозитория, как они были, и добавить Спецификацию к вызову репозитория.Я создал следующее @Aspect:
@Aspect
@Component
public class UserAspect {

    @Autowired(required=true)
    private HttpServletRequest request;

    private String user;

    @Around("execution(* com.example.repository.*Repository.*(..))")
    public Object filterQueriesByCompany(ProceedingJoinPoint jp) throws Throwable {
        Object[] arguments = jp.getArgs();
        Signature signature = jp.getSignature();

        List<Object> newArgs = new ArrayList<>();
        newArgs.add(CompanySpecification.fromCompany(user));

        return jp.proceed(newArgs.toArray());
    }

    @Before("execution(* com.example.controller.*Controller.*(..))")
    public void getUser() {
        user = request.getHeader("user");
    }
}

Это сработало бы отлично, поскольку для контроллеров, служб и репозиториев практически не потребовалось бы никаких изменений.Хотя у меня была проблема с сигнатурой функции.Так как я звоню findAll(Pageable p) в моем Сервисе, подпись функции уже определена в моем совете, и я не могу перейти к альтернативной версии findAll(Specification sp, Pageagle p) изнутри рекомендации.

Что выдумаете, будет ли лучший подход в этой ситуации?

Ответы [ 2 ]

0 голосов
/ 11 декабря 2018

Я не пользователь Spring или Java EE, но я могу помочь вам с аспектом.Я тоже немного погуглил, потому что ваши фрагменты кода без импорта и имен пакетов немного противоречивы, поэтому я не могу просто скопировать, вставить и запустить их.Судя по JavaDocs для JpaRepository и JpaSpecificationExecutor , которые вы расширяете в своем ExampleRepository, вы пытаетесь перехватить

Page<T> PagingAndSortingRepository.findAll(Pageable pageable)

(унаследовано от JpaRepository) и вместо этого позвоните

List<T> JpaSpecificationExecutor.findAll(Specification<T> spec, Pageable pageable)

, верно?

Таким образом, теоретически мы можем использовать это знание в наших советах и ​​советах, чтобы быть более безопасными для типов и избегать уродливых приемов отражения,Единственная проблема здесь в том, что перехваченный вызов возвращает Page<T>, а метод, который вы хотите вызвать, вместо этого возвращает List<T>.Вызывающий метод, безусловно, ожидает первого, а не второго, если только вы не всегда используете Iterable<T>, который является суперинтерфейсом для обоих рассматриваемых интерфейсов.Или, может быть, вы просто игнорируете возвращаемое значение?Если вы не ответите на этот вопрос или не покажете, как вы изменили свой код, чтобы сделать это, вам будет сложно действительно ответить на ваш вопрос.

Так что давайте просто предположим, что возвращаемый результат либо игнорируется, либо обрабатывается как Iterable,Тогда ваша пара точек / советов выглядит так:

@Around("execution(* findAll(*)) && args(pageable) && target(exampleRepository)")
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, ExampleRepository exampleRepository) throws Throwable {
  return exampleRepository.findAll(CompanySpecification.fromCompany(user), pageable);
}

Я проверил, все работает.Я также думаю, что это немного более элегантно, безопасно для типов и читабельно, чем то, что вы пробовали или что было предложено Eugen.

PS: Другой вариант - вручную преобразовать список в соответствующую страницу перед возвратом изсовет аспекта, если вызывающий код действительно ожидает, что объект страницы будет возвращен.


Обновление в связи с дополнительным вопросом:

Eugen wrote:

Для другой сущности, скажем, Foo, хранилище будет public interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> { }

Хорошо, тогда давайте просто обобщим pointcut и предположим, что он всегда должен нацеливаться на классыкоторые расширяют оба рассматриваемых интерфейса:

@Around(
  "execution(* findAll(*)) && " +
  "args(pageable) && " + 
  "target(jpaRepository) && " +
  //"within(org.springframework.data.jpa.repository.JpaRepository+) && " +
  "within(org.springframework.data.jpa.repository.JpaSpecificationExecutor+)"
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaRepository jpaRepository) throws Throwable {
  return ((JpaSpecificationExecutor) jpaRepository)
    .findAll(CompanySpecification.fromCompany(user), pageable);
}

Часть зареза, которую я закомментировал, является необязательной, поскольку я сужаюсь до вызовов JpaRepository методов уже через target() привязку параметра с использованием сигнатуры совета.Однако следует использовать второй within(), чтобы убедиться, что перехваченный класс действительно расширяет второй интерфейс, чтобы мы могли без проблем приводить и выполнять другой метод.


Обновление 2:

Как сказал Евгений, вы также можете избавиться от приведения, если привязаете целевой объект к типу JpaSpecificationExecutor - но только если вам не нужен JpaRepository вкод вашего совета до этого.В противном случае вам придется разыграть другой путь.Здесь кажется, что это действительно не нужно, поэтому его идея делает решение более скудным и выразительным.Спасибо за вклад.: -)

@Around(
  "target(jpaSpecificationExecutor) && " +
  "execution(* findAll(*)) && " +
  "args(pageable) && " + 
  "within(org.springframework.data.jpa.repository.JpaRepository+)"
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

Или же, если вы не хотите сливаться execution() с within() (дело вкуса):

@Around(
  "target(jpaSpecificationExecutor) && " +
  "execution(* org.springframework.data.jpa.repository.JpaRepository+.findAll(*)) && " +
  "args(pageable)" 
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

Менее безопасно для типов,но также вариант, если вы считаете, что других классов с подписью * findAll(Pageable) нет:

@Around("target(jpaSpecificationExecutor) && execution(* findAll(*)) && args(pageable)")
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

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

Наконец, я думаю, что мы рассмотрели большинство основк настоящему времени.

0 голосов
/ 20 ноября 2018

Вот идея:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
public class UserAspect {

    @Around("execution(* com.example.repository.*Repository.findAll())")
    public Object filterQueriesByCompany(ProceedingJoinPoint jp) throws Throwable {
        Object target = jp.getThis();
        Method method = target.getClass().getMethod("findAll", Specification.class);
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return method.invoke(target, CompanySpecification.fromCompany(request.getHeader("user")));
    }

}

Вышеупомянутый аспект перехватывает методы findAll() из репозитория и вместо продолжения вызова заменяет другой вызов метода findAll(Specification).Обратите внимание, как я получаю экземпляр HttpServletRequest.

Конечно, это отправная точка, а не готовое решение.

...