From 19096d6f5d139fa4c554310f3d246676b5b17d4a Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Tue, 19 Sep 2023 20:46:37 +0300 Subject: [PATCH] Add QueryStackTraceLogger that allows you to locate the source of an SQL query executed by Hibernate #653 --- .../query/QueryStackTraceLogger.java | 82 +++++++++ .../query/QueryStackTraceLoggerTest.java | 160 ++++++++++++++++++ .../utils/hibernate/util/AbstractTest.java | 4 - .../query/QueryStackTraceLogger.java | 82 +++++++++ .../query/QueryStackTraceLoggerTest.java | 151 +++++++++++++++++ .../utils/hibernate/util/AbstractTest.java | 10 -- .../query/QueryStackTraceLogger.java | 82 +++++++++ .../query/QueryStackTraceLoggerTest.java | 153 +++++++++++++++++ .../utils/hibernate/util/AbstractTest.java | 10 -- .../query/QueryStackTraceLogger.java | 82 +++++++++ .../query/QueryStackTraceLoggerTest.java | 151 +++++++++++++++++ .../utils/hibernate/util/AbstractTest.java | 10 -- .../query/QueryStackTraceLogger.java | 82 +++++++++ .../query/QueryStackTraceLoggerTest.java | 153 +++++++++++++++++ .../utils/hibernate/util/AbstractTest.java | 10 -- 15 files changed, 1178 insertions(+), 44 deletions(-) create mode 100644 hypersistence-utils-hibernate-5/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java create mode 100644 hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java create mode 100644 hypersistence-utils-hibernate-52/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java create mode 100644 hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java create mode 100644 hypersistence-utils-hibernate-55/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java create mode 100644 hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java create mode 100644 hypersistence-utils-hibernate-60/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java create mode 100644 hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java create mode 100644 hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java create mode 100644 hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java diff --git a/hypersistence-utils-hibernate-5/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java b/hypersistence-utils-hibernate-5/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java new file mode 100644 index 000000000..34ce193b4 --- /dev/null +++ b/hypersistence-utils-hibernate-5/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java @@ -0,0 +1,82 @@ +package io.hypersistence.utils.hibernate.query; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link QueryStackTraceLogger} allows you to log the stack trace that + * executed a given SQL query. + * + * @author Vlad Mihalcea + * @since 3.5.3 + */ +public class QueryStackTraceLogger implements StatementInspector { + + public static final String ORG_HIBERNATE ="org.hibernate"; + + public static String TAB = "\t"; + public static String NEW_LINE = System.getProperty("line.separator"); + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryStackTraceLogger.class); + + private final String packageNamePrefix; + + public QueryStackTraceLogger(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.debug( + "This SQL query: [\n\t{}\n]\nwas generated by Hibernate like this: [\n{}\n]", + sql, + String.join( + NEW_LINE, + stackTraceElementsUpTo(packageNamePrefix) + .stream() + .map(e -> TAB + e.toString()) + .collect(Collectors.toList()) + ) + ); + return null; + } + + /** + * Filter the stack trace based up to the provided package name prefix + * + * @param endPackageNamePrefix package name to match the {@link StackTraceElement} + * @return the {@link StackTraceElement} up to the matching the provided package name + */ + private List stackTraceElementsUpTo(String endPackageNamePrefix) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + List filteredStackTraceElements = new ArrayList<>(); + boolean startPackageMatched = false; + boolean endPackageMatched = false; + for (StackTraceElement stackTraceElement : stackTraceElements) { + String className = stackTraceElement.getClassName(); + if(!startPackageMatched) { + if(className.startsWith(ORG_HIBERNATE)) { + startPackageMatched = true; + } else { + continue; + } + } + if(!className.contains(endPackageNamePrefix)) { + if(!endPackageMatched) { + filteredStackTraceElements.add(stackTraceElement); + } else { + break; + } + } else if(!endPackageMatched) { + endPackageMatched = true; + filteredStackTraceElements.add(stackTraceElement); + } + } + return filteredStackTraceElements; + } +} diff --git a/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java b/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..664f86597 --- /dev/null +++ b/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java @@ -0,0 +1,160 @@ +package io.hypersistence.utils.hibernate.query; + +import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.util.transaction.JPATransactionFunction; +import org.junit.Test; + +import javax.persistence.*; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Root; +import java.time.LocalDate; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new QueryStackTraceLogger("io.hypersistence.utils.hibernate.query") + ); + } + + @Test + public void testJPQL() { + doInJPA(new JPATransactionFunction() { + + @Override + public Void apply(EntityManager entityManager) { + entityManager.createQuery( + "select " + + " count(p) as postCount " + + "from " + + " Post p ", Tuple.class) + .getResultList(); + + return null; + } + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(new JPATransactionFunction() { + + @Override + public Void apply(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")) + ); + + entityManager.createQuery(criteria).getResultList(); + return null; + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java b/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java index e97bcc796..378c69dfe 100644 --- a/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java +++ b/hypersistence-utils-hibernate-5/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java @@ -51,10 +51,6 @@ public abstract class AbstractTest { - static { - Thread.currentThread().setName("Alice"); - } - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { diff --git a/hypersistence-utils-hibernate-52/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java b/hypersistence-utils-hibernate-52/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java new file mode 100644 index 000000000..34ce193b4 --- /dev/null +++ b/hypersistence-utils-hibernate-52/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java @@ -0,0 +1,82 @@ +package io.hypersistence.utils.hibernate.query; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link QueryStackTraceLogger} allows you to log the stack trace that + * executed a given SQL query. + * + * @author Vlad Mihalcea + * @since 3.5.3 + */ +public class QueryStackTraceLogger implements StatementInspector { + + public static final String ORG_HIBERNATE ="org.hibernate"; + + public static String TAB = "\t"; + public static String NEW_LINE = System.getProperty("line.separator"); + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryStackTraceLogger.class); + + private final String packageNamePrefix; + + public QueryStackTraceLogger(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.debug( + "This SQL query: [\n\t{}\n]\nwas generated by Hibernate like this: [\n{}\n]", + sql, + String.join( + NEW_LINE, + stackTraceElementsUpTo(packageNamePrefix) + .stream() + .map(e -> TAB + e.toString()) + .collect(Collectors.toList()) + ) + ); + return null; + } + + /** + * Filter the stack trace based up to the provided package name prefix + * + * @param endPackageNamePrefix package name to match the {@link StackTraceElement} + * @return the {@link StackTraceElement} up to the matching the provided package name + */ + private List stackTraceElementsUpTo(String endPackageNamePrefix) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + List filteredStackTraceElements = new ArrayList<>(); + boolean startPackageMatched = false; + boolean endPackageMatched = false; + for (StackTraceElement stackTraceElement : stackTraceElements) { + String className = stackTraceElement.getClassName(); + if(!startPackageMatched) { + if(className.startsWith(ORG_HIBERNATE)) { + startPackageMatched = true; + } else { + continue; + } + } + if(!className.contains(endPackageNamePrefix)) { + if(!endPackageMatched) { + filteredStackTraceElements.add(stackTraceElement); + } else { + break; + } + } else if(!endPackageMatched) { + endPackageMatched = true; + filteredStackTraceElements.add(stackTraceElement); + } + } + return filteredStackTraceElements; + } +} diff --git a/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java b/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..fb70fbe8e --- /dev/null +++ b/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java @@ -0,0 +1,151 @@ +package io.hypersistence.utils.hibernate.query; + +import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import javax.persistence.*; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Root; +import java.time.LocalDate; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new QueryStackTraceLogger("io.hypersistence.utils.hibernate.query") + ); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultList(); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(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")) + ); + + entityManager.createQuery(criteria).getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java b/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java index cf84550bf..f86e70484 100644 --- a/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java +++ b/hypersistence-utils-hibernate-52/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java @@ -48,16 +48,6 @@ public abstract class AbstractTest { - static { - Thread.currentThread().setName("Alice"); - } - - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { - Thread bob = new Thread(r); - bob.setName("Bob"); - return bob; - }); - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); private EntityManagerFactory emf; diff --git a/hypersistence-utils-hibernate-55/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java b/hypersistence-utils-hibernate-55/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java new file mode 100644 index 000000000..34ce193b4 --- /dev/null +++ b/hypersistence-utils-hibernate-55/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java @@ -0,0 +1,82 @@ +package io.hypersistence.utils.hibernate.query; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link QueryStackTraceLogger} allows you to log the stack trace that + * executed a given SQL query. + * + * @author Vlad Mihalcea + * @since 3.5.3 + */ +public class QueryStackTraceLogger implements StatementInspector { + + public static final String ORG_HIBERNATE ="org.hibernate"; + + public static String TAB = "\t"; + public static String NEW_LINE = System.getProperty("line.separator"); + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryStackTraceLogger.class); + + private final String packageNamePrefix; + + public QueryStackTraceLogger(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.debug( + "This SQL query: [\n\t{}\n]\nwas generated by Hibernate like this: [\n{}\n]", + sql, + String.join( + NEW_LINE, + stackTraceElementsUpTo(packageNamePrefix) + .stream() + .map(e -> TAB + e.toString()) + .collect(Collectors.toList()) + ) + ); + return null; + } + + /** + * Filter the stack trace based up to the provided package name prefix + * + * @param endPackageNamePrefix package name to match the {@link StackTraceElement} + * @return the {@link StackTraceElement} up to the matching the provided package name + */ + private List stackTraceElementsUpTo(String endPackageNamePrefix) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + List filteredStackTraceElements = new ArrayList<>(); + boolean startPackageMatched = false; + boolean endPackageMatched = false; + for (StackTraceElement stackTraceElement : stackTraceElements) { + String className = stackTraceElement.getClassName(); + if(!startPackageMatched) { + if(className.startsWith(ORG_HIBERNATE)) { + startPackageMatched = true; + } else { + continue; + } + } + if(!className.contains(endPackageNamePrefix)) { + if(!endPackageMatched) { + filteredStackTraceElements.add(stackTraceElement); + } else { + break; + } + } else if(!endPackageMatched) { + endPackageMatched = true; + filteredStackTraceElements.add(stackTraceElement); + } + } + return filteredStackTraceElements; + } +} diff --git a/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java b/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..2f9f87b6b --- /dev/null +++ b/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java @@ -0,0 +1,153 @@ +package io.hypersistence.utils.hibernate.query; + +import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import javax.persistence.*; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Root; +import java.time.LocalDate; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new QueryStackTraceLogger("io.hypersistence.utils.hibernate.query") + ); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultList(); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(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")) + ); + + entityManager.createQuery(criteria).getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java b/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java index cf84550bf..f86e70484 100644 --- a/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java +++ b/hypersistence-utils-hibernate-55/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java @@ -48,16 +48,6 @@ public abstract class AbstractTest { - static { - Thread.currentThread().setName("Alice"); - } - - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { - Thread bob = new Thread(r); - bob.setName("Bob"); - return bob; - }); - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); private EntityManagerFactory emf; diff --git a/hypersistence-utils-hibernate-60/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java b/hypersistence-utils-hibernate-60/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java new file mode 100644 index 000000000..34ce193b4 --- /dev/null +++ b/hypersistence-utils-hibernate-60/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java @@ -0,0 +1,82 @@ +package io.hypersistence.utils.hibernate.query; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link QueryStackTraceLogger} allows you to log the stack trace that + * executed a given SQL query. + * + * @author Vlad Mihalcea + * @since 3.5.3 + */ +public class QueryStackTraceLogger implements StatementInspector { + + public static final String ORG_HIBERNATE ="org.hibernate"; + + public static String TAB = "\t"; + public static String NEW_LINE = System.getProperty("line.separator"); + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryStackTraceLogger.class); + + private final String packageNamePrefix; + + public QueryStackTraceLogger(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.debug( + "This SQL query: [\n\t{}\n]\nwas generated by Hibernate like this: [\n{}\n]", + sql, + String.join( + NEW_LINE, + stackTraceElementsUpTo(packageNamePrefix) + .stream() + .map(e -> TAB + e.toString()) + .collect(Collectors.toList()) + ) + ); + return null; + } + + /** + * Filter the stack trace based up to the provided package name prefix + * + * @param endPackageNamePrefix package name to match the {@link StackTraceElement} + * @return the {@link StackTraceElement} up to the matching the provided package name + */ + private List stackTraceElementsUpTo(String endPackageNamePrefix) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + List filteredStackTraceElements = new ArrayList<>(); + boolean startPackageMatched = false; + boolean endPackageMatched = false; + for (StackTraceElement stackTraceElement : stackTraceElements) { + String className = stackTraceElement.getClassName(); + if(!startPackageMatched) { + if(className.startsWith(ORG_HIBERNATE)) { + startPackageMatched = true; + } else { + continue; + } + } + if(!className.contains(endPackageNamePrefix)) { + if(!endPackageMatched) { + filteredStackTraceElements.add(stackTraceElement); + } else { + break; + } + } else if(!endPackageMatched) { + endPackageMatched = true; + filteredStackTraceElements.add(stackTraceElement); + } + } + return filteredStackTraceElements; + } +} diff --git a/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java b/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..d467336d2 --- /dev/null +++ b/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java @@ -0,0 +1,151 @@ +package io.hypersistence.utils.hibernate.query; + +import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +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.time.LocalDate; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new QueryStackTraceLogger("io.hypersistence.utils.hibernate.query") + ); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultList(); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(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")) + ); + + entityManager.createQuery(criteria).getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java b/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java index 672c68a65..a90b40969 100644 --- a/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java +++ b/hypersistence-utils-hibernate-60/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java @@ -49,16 +49,6 @@ public abstract class AbstractTest { - static { - Thread.currentThread().setName("Alice"); - } - - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { - Thread bob = new Thread(r); - bob.setName("Bob"); - return bob; - }); - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); private EntityManagerFactory emf; diff --git a/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java b/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java new file mode 100644 index 000000000..34ce193b4 --- /dev/null +++ b/hypersistence-utils-hibernate-62/src/main/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLogger.java @@ -0,0 +1,82 @@ +package io.hypersistence.utils.hibernate.query; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link QueryStackTraceLogger} allows you to log the stack trace that + * executed a given SQL query. + * + * @author Vlad Mihalcea + * @since 3.5.3 + */ +public class QueryStackTraceLogger implements StatementInspector { + + public static final String ORG_HIBERNATE ="org.hibernate"; + + public static String TAB = "\t"; + public static String NEW_LINE = System.getProperty("line.separator"); + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryStackTraceLogger.class); + + private final String packageNamePrefix; + + public QueryStackTraceLogger(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.debug( + "This SQL query: [\n\t{}\n]\nwas generated by Hibernate like this: [\n{}\n]", + sql, + String.join( + NEW_LINE, + stackTraceElementsUpTo(packageNamePrefix) + .stream() + .map(e -> TAB + e.toString()) + .collect(Collectors.toList()) + ) + ); + return null; + } + + /** + * Filter the stack trace based up to the provided package name prefix + * + * @param endPackageNamePrefix package name to match the {@link StackTraceElement} + * @return the {@link StackTraceElement} up to the matching the provided package name + */ + private List stackTraceElementsUpTo(String endPackageNamePrefix) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + List filteredStackTraceElements = new ArrayList<>(); + boolean startPackageMatched = false; + boolean endPackageMatched = false; + for (StackTraceElement stackTraceElement : stackTraceElements) { + String className = stackTraceElement.getClassName(); + if(!startPackageMatched) { + if(className.startsWith(ORG_HIBERNATE)) { + startPackageMatched = true; + } else { + continue; + } + } + if(!className.contains(endPackageNamePrefix)) { + if(!endPackageMatched) { + filteredStackTraceElements.add(stackTraceElement); + } else { + break; + } + } else if(!endPackageMatched) { + endPackageMatched = true; + filteredStackTraceElements.add(stackTraceElement); + } + } + return filteredStackTraceElements; + } +} diff --git a/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..279407581 --- /dev/null +++ b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/query/QueryStackTraceLoggerTest.java @@ -0,0 +1,153 @@ +package io.hypersistence.utils.hibernate.query; + +import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +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.time.LocalDate; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new QueryStackTraceLogger("io.hypersistence.utils.hibernate.query") + ); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultList(); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(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")) + ); + + entityManager.createQuery(criteria).getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java index 672c68a65..a90b40969 100644 --- a/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java +++ b/hypersistence-utils-hibernate-62/src/test/java/io/hypersistence/utils/hibernate/util/AbstractTest.java @@ -49,16 +49,6 @@ public abstract class AbstractTest { - static { - Thread.currentThread().setName("Alice"); - } - - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { - Thread bob = new Thread(r); - bob.setName("Bob"); - return bob; - }); - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); private EntityManagerFactory emf;