簡體   English   中英

將Spring JPA Specification應用於多個存儲庫和查詢

[英]Apply Spring JPA Specification to multiple repositories and queries

我有以下情況:

我的項目包含多個實體,每個實體都有其各自的控制器,服務和JPA存儲庫。 所有這些實體都通過“ companyUuid”屬性與特定公司關聯。

我的控制器中的每個傳入請求都將帶有一個“用戶”標頭,該標頭將向我提供發出該請求的用戶的詳細信息,包括與他關聯的公司。

我需要從標題中檢索與用戶關聯的公司,並過濾該公司的每個后續查詢,這實際上就像在每個查詢中添加WHERE companyUuid = ...

我作為解決方案所做的是用於創建Specification對象的通用函數:

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> { }

更改了“查找”調用以包括規范:

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

當然,這需要將@RequestHeader添加到控制器函數中,以使用戶進入標頭。

盡管此解決方案絕對可以正常工作,但是要對我的@RestControllers所有路由進行復制和粘貼,都需要大量復制和代碼重復。

因此,問題是:如何以一種優雅,簡潔的方式為所有控制器做到這一點?

我已經對此進行了相當多的研究,得出了以下結論:

  1. Spring JPA和Hibernate似乎沒有提供一種動態使用規范來限制所有查詢的方法(參考: 在每個Spring Jpa Repository調用中自動添加條件
  2. Spring MVC HandlerInterceptor可能有助於將User從每個請求的標頭中移出,但由於我不在此項目中使用視圖(它只是后端),因此它似乎並不適合整體使用,它不能對我的存儲庫查詢做任何事情
  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");
    }
}

這將是完美的,因為幾乎不需要對控制器,服務和存儲庫進行任何修改。 雖然,我對函數簽名有疑問。 由於我在Service中調用findAll(Pageable p) ,因此該函數的簽名已在我的建議中定義,並且無法從建議內部更改為備用版本findAll(Specification sp, Pageagle p)

您認為在這種情況下最好的方法是什么?

這是一個主意:

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實例。

當然,這是一個起點,而不是開箱即用的解決方案。

我不是Spring或Java EE用戶,但是我可以在方面方面為您提供幫助。 我也用Google搜索了一下,因為沒有導入和程序包名稱的代碼片段有點不連貫,所以我不能只復制,粘貼和運行它們。 從您在ExampleRepository擴展的JpaRepositoryJpaSpecificationExecutor的JavaDocs來看,您試圖攔截

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:另一個選擇是,如果調用代碼確實希望返回頁面對象,則在從方面建議中將列表返回之前,手動將列表轉換為相應的頁面。


由於后續問題而更新:

歐根寫道:

對於另一個實體,假設Foo ,存儲庫將是public interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> { }

好吧,接下來讓我們概括一下切入點,並假設切入點始終應以擴展有問題的兩個接口的類為目標:

@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);
}

我注釋掉的切入點部分是可選的,因為我已經通過使用建議簽名的target()參數綁定縮小了JpaRepository方法的調用。 但是,應該使用第二個within() ,以確保被攔截的類實際上也擴展了第二個接口,因此我們可以強制轉換並執行另一個方法,而不會出現任何問題。


更新2:

正如Eugen所說,如果將目標對象綁定到JpaSpecificationExecutor類型,也可以擺脫JpaSpecificationExecutor類型JpaSpecificationExecutor ,但JpaRepository是您之前在建議代碼中不需要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);
}

您可能會注意到,這看起來像是我對某個特定子接口的原始解決方案,而您是對的。 不過,我建議您更嚴格一些,不要使用最后一個選項,即使它在我的測試用例中也可以,並且您可能會滿意。

最后,我認為到目前為止,我們已經涵蓋了大多數基礎。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM