From d1d782cd1bd6ac2ac6102b7ec7bea44c686f2641 Mon Sep 17 00:00:00 2001 From: Yannick Majoros Date: Tue, 7 Nov 2023 17:35:30 +0100 Subject: [PATCH] resolves #668 --- .../utils/hibernate/query/SQLExtractor.java | 46 ++++++++--- .../hibernate/query/SQLExtractorTest.java | 76 +++++++++++++++---- 2 files changed, 100 insertions(+), 22 deletions(-) diff --git a/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java b/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java index 5183c5447..5bca7aa7a 100644 --- a/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java +++ b/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java @@ -13,6 +13,9 @@ import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; import java.util.function.Supplier; /** @@ -36,20 +39,21 @@ protected SQLExtractor() { * @return the underlying SQL generated by the provided JPA query */ public static String from(Query query) { - if(query instanceof SqmInterpretationsKey.InterpretationsKeySource && - query instanceof QueryImplementor && - query instanceof QuerySqmImpl) { - QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((SqmInterpretationsKey.InterpretationsKeySource) query); - QuerySqmImpl querySqm = (QuerySqmImpl) query; + Query hibernateQuery = getHibernateQuery(query); + if (hibernateQuery instanceof SqmInterpretationsKey.InterpretationsKeySource && + hibernateQuery instanceof QueryImplementor && + hibernateQuery instanceof QuerySqmImpl) { + QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((SqmInterpretationsKey.InterpretationsKeySource) hibernateQuery); + QuerySqmImpl querySqm = (QuerySqmImpl) hibernateQuery; Supplier buildSelectQueryPlan = () -> ReflectionUtils.invokeMethod(querySqm, "buildSelectQueryPlan"); - SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) query).getSession().getFactory().getQueryEngine() + SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) hibernateQuery).getSession().getFactory().getQueryEngine() .getInterpretationCache() .resolveSelectQueryPlan(cacheKey, buildSelectQueryPlan) : (SelectQueryPlan) buildSelectQueryPlan.get(); - if(plan instanceof ConcreteSqmSelectQueryPlan) { + if (plan instanceof ConcreteSqmSelectQueryPlan) { ConcreteSqmSelectQueryPlan selectQueryPlan = (ConcreteSqmSelectQueryPlan) plan; Object cacheableSqmInterpretation = ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "cacheableSqmInterpretation"); - if(cacheableSqmInterpretation == null) { + if (cacheableSqmInterpretation == null) { DomainQueryExecutionContext domainQueryExecutionContext = DomainQueryExecutionContext.class.cast(querySqm); cacheableSqmInterpretation = ReflectionUtils.invokeStaticMethod( ReflectionUtils.getMethod( @@ -72,6 +76,30 @@ public static String from(Query query) { } } } - return ReflectionUtils.invokeMethod(query, "getQueryString"); + return ReflectionUtils.invokeMethod(hibernateQuery, "getQueryString"); + } + + /** + * Get the unproxied hibernate query underlying the provided query object. + * + * @param query JPA query + * @return the unproxied Hibernate query, or original query + */ + private static Query getHibernateQuery(Query query) { + try { + if (query instanceof QuerySqmImpl || !Proxy.isProxyClass(query.getClass())) { + return query; + } + // is proxyied, get it out + InvocationHandler invocationHandler = Proxy.getInvocationHandler(query); + Class innerClass = invocationHandler.getClass(); + Field targetField = innerClass.getDeclaredField("target"); + targetField.setAccessible(true); + return (Query) targetField.get(invocationHandler); + } catch (NoSuchFieldException exception) { + return query; // seems it cannot extract it, probably not a hibernate proxy + } catch (IllegalAccessException exception) { + throw new RuntimeException(exception); + } } } diff --git a/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java index e74d81673..8fbbe4fd9 100644 --- a/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java +++ b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java @@ -1,13 +1,24 @@ package io.hypersistence.utils.hibernate.query; import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Query; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Root; import org.junit.Test; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.time.LocalDate; import static org.junit.Assert.assertNotNull; @@ -53,24 +64,27 @@ public void testJPQL() { @Test public void testCriteriaAPI() { doInJPA(entityManager -> { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Query criteriaQuery = createTestQuery(entityManager); - CriteriaQuery criteria = builder.createQuery(PostComment.class); - - Root postComment = criteria.from(PostComment.class); - Join post = postComment.join("post"); + String sql = SQLExtractor.from(criteriaQuery); - criteria.where( - builder.like(post.get("title"), "%Java%") - ); + assertNotNull(sql); - criteria.orderBy( - builder.asc(postComment.get("id")) + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); + }); + } - Query criteriaQuery = entityManager.createQuery(criteria); + @Test + public void testCriteriaAPIWithProxy() { + doInJPA(entityManager -> { + Query criteriaQuery = createTestQuery(entityManager); + Query proxiedQuery = proxy(criteriaQuery); - String sql = SQLExtractor.from(criteriaQuery); + String sql = SQLExtractor.from(proxiedQuery); assertNotNull(sql); @@ -82,6 +96,29 @@ public void testCriteriaAPI() { }); } + private static Query proxy(Query criteriaQuery) { + return (Query) Proxy.newProxyInstance(Query.class.getClassLoader(), new Class[]{Query.class}, new HibernateLikeInvocationHandler(criteriaQuery)); + } + + private static Query createTestQuery(EntityManager entityManager) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(PostComment.class); + + Root postComment = criteria.from(PostComment.class); + Join post = postComment.join("post"); + + criteria.where( + builder.like(post.get("title"), "%Java%") + ); + + criteria.orderBy( + builder.asc(postComment.get("id")) + ); + + return entityManager.createQuery(criteria); + } + @Entity(name = "Post") @Table(name = "post") public static class Post { @@ -161,4 +198,17 @@ public PostComment setReview(String review) { return this; } } + + private static class HibernateLikeInvocationHandler implements InvocationHandler { + private final Query target; // has to be named "target" because this is how Hibernate implements it, and the extracting code has to be quite invasive to get the query from the Hibernate proxy + + public HibernateLikeInvocationHandler(Query query) { + this.target = query; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return method.invoke(target, args); + } + } }