背景
??Spring Data Jpa 雖然可以減少代碼中Sql的數(shù)量嫌变,但其在復(fù)雜查詢中略顯乏力集索。網(wǎng)上很多文章都采用Java代碼的形式去實(shí)現(xiàn)復(fù)雜查詢,但這樣一來Sql的效率變得不可控羡棵。也有文章采用@Query 注解去執(zhí)行JPQL原生SQL,本人在使用過程中也傾向于這種方式她紫。
??但有時(shí)采用@Query方式,框架無法正常返回我們需要的類型屿讽。比如復(fù)雜查詢后后分頁昭灵,Repository方法的返回寫的是Page<User>吠裆,然而真正執(zhí)行后返回的類型卻變成了Page<Object[]>。究竟原因烂完,分析源碼后發(fā)現(xiàn)PagedExecution并未對(duì)執(zhí)行結(jié)果進(jìn)行處理试疙。
通過對(duì)Spring-Data-Jpa源碼的分析,發(fā)現(xiàn)在RepositoryFactorySupport.java中的getRepository方法(第124行)抠蚣,提供了鉤子方法使得我們可以在自己的RepositoryProxyPostProcessor中向目標(biāo)Repository注入MethodInterceptor(方法攔截器)祝旷。
public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {
...
Object target = this.getTargetRepository(information);
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(new Class[]{repositoryInterface, Repository.class});
...
Iterator var8 = this.postProcessors.iterator();
while(var8.hasNext()) {
RepositoryProxyPostProcessor processor = (RepositoryProxyPostProcessor)var8.next();
processor.postProcess(result, information);
}
...
result.addAdvice(new RepositoryFactorySupport.QueryExecutorMethodInterceptor(information, customImplementation, target));
return result.getProxy(this.classLoader);
}
本文將介紹通過自定義RepositoryProxyPostProcessor向Repository(我們應(yīng)用中聲明的Repository)中注入自定義MethodInterceptor以使得@Qurey復(fù)雜查詢能支持分頁P(yáng)OJO的返回。
1.編寫業(yè)務(wù)Repository嘶窄,并繼承JpaRepository
繼承JpaRepository使得返回結(jié)果支持分頁
@Repository
public interface UserRepository extends JpaRepository<User, String> {
/**
* 返回指定部門下邊的用戶
*
* @param officeId
* @param pageable
* @return
*/
@Query(value = "SELECT user.id, user.name FROM User user, Office office WHERE user.officeId = office.id AND office.id = ?1 ")
Page<User> getUserInOffice(String officeId, Pageable pageable);
}
上邊的查詢還是比較簡(jiǎn)單的怀跛,這只是為了作為例子好理解。如果需要你可以將Sql改得更加復(fù)雜柄冲。注意吻谋,我采用的是JPQL而非Native Sql。啟動(dòng)單元測(cè)試調(diào)用上述方法现横,結(jié)果的確返回了Page對(duì)象滨溉,但是當(dāng)你page.getContent()時(shí),會(huì)發(fā)現(xiàn)返回的結(jié)果為L(zhǎng)ist<Object[]>长赞。
接下來進(jìn)入主題
2.自定義MethodInterceptor晦攒,將content由List<Object[]>轉(zhuǎn)為L(zhǎng)ist<T>
繼承MethodInterceptor,重寫invoke方法執(zhí)行其他代理獲得Jpql返回結(jié)果集后得哆,將List<Object[]>轉(zhuǎn)為L(zhǎng)ist<T>脯颜。
public class JpqlBeanMethodInterceptor implements MethodInterceptor {
/**
* 用于存放QueryMethod對(duì)應(yīng)的字段和返回類型信息
*/
private Map<Method, SelectAlias> selectAlias = new HashMap<>();
public JpqlBeanMethodInterceptor(RepositoryInformation repositoryInformation) {
Iterator<Method> iterable = repositoryInformation.getQueryMethods().iterator();
SqlParser sqlParser = new DefaultSqlParser();
while (iterable.hasNext()) {
Method method = iterable.next();
Query query = method.getAnnotation(Query.class);
if (query == null || query.nativeQuery()) {
continue;
}
//獲取返回類型
Class clazz = getGenericReturnClass(method);
if (clazz == null) {
continue;
}
SelectAlias alias = sqlParser.getAlias(query.value(), clazz);
if (alias == null) {
continue;
}
selectAlias.put(method, alias);
}
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//執(zhí)行方法獲得結(jié)果
Object obj = invocation.proceed();
if (!checkCanConvert(obj)) {
return obj;
}
SelectAlias alias = selectAlias.get(invocation.getMethod());
if (alias == null) {
return obj;
}
List content = getPageContent((PageImpl) obj);
convert(content, alias);
return obj;
}
//由于篇幅原因只貼出部分代碼
...
}
上述代碼由于篇幅原因只貼出部分,后續(xù)整理完代碼后將共享出來贩据。代碼中sqlParser采用jsqlparser從Sql中取出返回的字段解析后轉(zhuǎn)換為SelectAlia栋操。注意這部分代碼是在構(gòu)造方法中執(zhí)行的,后續(xù)代碼決定了這部分代碼只會(huì)在Spring Boot啟動(dòng)的時(shí)候每個(gè)Repository執(zhí)行一次饱亮,以提高執(zhí)行效率矾芙。
3.自定義RepositoryProxyPostProcessor
在postProcess時(shí)向Repository注入JpqlBeanMethodInterceptor
public class JpqlBeanPostProcessor implements RepositoryProxyPostProcessor {
@Override
public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
factory.addAdvice(new JpqlBeanMethodInterceptor(repositoryInformation));
}
}
4.自定義JpaRepositoryFactoryBean
創(chuàng)建RepositoryFactory時(shí),向其加入我們自定義的RepositoryProxyPostProcessor
public class GmRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends JpaRepositoryFactoryBean<T, S, ID> {
public GmRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
jpaRepositoryFactory.addRepositoryProxyPostProcessor(new JpqlBeanPostProcessor());
return jpaRepositoryFactory;
}
}
這一步大家應(yīng)該很熟悉了近上,寫過公用Repository的朋友應(yīng)該都知道其作用剔宪。不過注意一下,我們?cè)诜祷豃paRepositoryFactory前壹无,要將我們的RepositoryProxyPostProcessor 加進(jìn)postProcessors中葱绒,不然前邊就白做了。
5.讓我們的JpaRepositoryFactoryBean起作用
這一步大家應(yīng)該也比較熟悉了斗锭,直接上代碼地淀。
@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = GmRepositoryFactoryBean.class)
@EnableSpringDataWebSupport
public class JpaDataConfig {
}
注意要放到項(xiàng)目目錄下或者在Application把這個(gè)文件的包加到BasePackages中,反正就是要讓Spring Boot啟動(dòng)的時(shí)候掃的到就對(duì)啦岖是。
最后
再次啟動(dòng)單元測(cè)試帮毁,重新執(zhí)行g(shù)etUserInOffice方法实苞,可以看到返回的Page中的content已經(jīng)正常返回User的List了,大功告成烈疚!經(jīng)測(cè)試黔牵,其執(zhí)行速度與原先的返回Object[]基本保持一致,有時(shí)甚至更快胞得。這個(gè)我也不知道為什么會(huì)更快,可能與網(wǎng)絡(luò)因素有關(guān)吧屹电,畢竟我在測(cè)試的時(shí)候數(shù)據(jù)庫不在本地阶剑。。危号。