From ab36adbbc22fcd156ddce528a9ff5e5103623342 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 20 Mar 2022 16:10:42 +0100 Subject: Use Blaze Persistence for criteria queries. Blaze Persistence is more flexible, but also less type-safe than JPA Criteria. This change explores what the changes look like and how efficient the resulting queries are. Change-Id: Ia47e4f0280d451e0381f58ece297c5bc06604289 --- .../java/eu/mulk/mulkcms2/benki/posts/Post.java | 112 ++++++++++----------- .../eu/mulk/mulkcms2/benki/posts/PostResource.java | 29 +++++- .../HibernateMetadataBuilderContributor.java | 12 +++ .../hibernate/PostMatchesWebsearchFunction.java | 11 ++ 4 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/main/java/eu/mulk/mulkcms2/common/hibernate/HibernateMetadataBuilderContributor.java create mode 100644 src/main/java/eu/mulk/mulkcms2/common/hibernate/PostMatchesWebsearchFunction.java (limited to 'src/main/java') diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java index 241f005..02a11fa 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java @@ -2,13 +2,14 @@ package eu.mulk.mulkcms2.benki.posts; import static java.util.stream.Collectors.toList; +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.CriteriaBuilderFactory; import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; import eu.mulk.mulkcms2.benki.accesscontrol.Role; import eu.mulk.mulkcms2.benki.bookmarks.Bookmark; import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; import eu.mulk.mulkcms2.benki.newsletter.Newsletter; import eu.mulk.mulkcms2.benki.users.User; -import eu.mulk.mulkcms2.benki.users.User_; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import java.time.LocalDate; import java.time.OffsetDateTime; @@ -22,13 +23,13 @@ import java.util.Objects; import java.util.Set; import java.util.TimeZone; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.json.bind.annotation.JsonbTransient; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityManager; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; @@ -45,12 +46,6 @@ import javax.persistence.MapKey; import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; import javax.persistence.Table; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.From; -import javax.persistence.criteria.JoinType; -import javax.persistence.criteria.Predicate; -import org.hibernate.Session; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; @@ -148,80 +143,76 @@ public abstract class Post> extends PanacheEntityBase { } } - protected static CriteriaQuery queryViewable( + protected static CriteriaBuilder queryViewable( Class entityClass, @CheckForNull User reader, @CheckForNull User owner, @CheckForNull Integer cursor, - CriteriaBuilder cb, + EntityManager em, + CriteriaBuilderFactory cbf, boolean forward, @CheckForNull String searchQuery) { - CriteriaQuery query = cb.createQuery(entityClass); - var conditions = new ArrayList(); + CriteriaBuilder cb = cbf.create(em, entityClass).select("post"); - From post; if (reader == null) { - post = query.from(entityClass); - var target = post.join(Post_.targets); - conditions.add(cb.equal(target, Role.getWorld())); + cb = + cb.from(entityClass, "post") + .innerJoin("post.targets", "role") + .where("'world'") + .isMemberOf("role.tags"); } else { - var root = query.from(User.class); - conditions.add(cb.equal(root, reader)); + cb = cb.from(User.class, "user").where("user").eq(reader); if (entityClass.isAssignableFrom(Post.class)) { - post = (From) root.join(User_.visiblePosts); + cb = cb.innerJoin("user.visiblePosts", "post"); } else if (entityClass.isAssignableFrom(Bookmark.class)) { - post = (From) root.join(User_.visibleBookmarks); + cb = cb.innerJoin("user.visibleBookmark", "post"); } else if (entityClass.isAssignableFrom(LazychatMessage.class)) { - post = (From) root.join(User_.visibleLazychatMessages); + cb = cb.innerJoin("user.visibleLazychatMessages", "post"); } else { throw new IllegalArgumentException(); } } - query.select(post); - post.fetch(Post_.owner, JoinType.LEFT); + cb = cb.fetch("post.owner"); if (owner != null) { - conditions.add(cb.equal(post.get(Post_.owner), owner)); + cb = cb.where("post.owner").eq(owner); } if (forward) { - query.orderBy(cb.desc(post.get(Post_.id))); + cb = cb.orderByDesc("post.id"); } else { - query.orderBy(cb.asc(post.get(Post_.id))); + cb = cb.orderByAsc("post.id"); } if (cursor != null) { if (forward) { - conditions.add(cb.le(post.get(Post_.id), cursor)); + cb = cb.where("post.id").le(cursor); } else { - conditions.add(cb.gt(post.get(Post_.id), cursor)); + cb = cb.where("post.id").gt(cursor); } } if (searchQuery != null && !searchQuery.isBlank()) { - var postTexts = post.join(Post_.texts); - var localizedSearches = - Stream.of("de", "en") - .map( - language -> - cb.isTrue( - cb.function( - "post_matches_websearch", - Boolean.class, - postTexts.get(PostText_.searchTerms), - cb.literal(language), - cb.literal(searchQuery)))) - .toArray(n -> new Predicate[n]); - conditions.add(cb.or(localizedSearches)); + cb = + cb.whereExists() + .from(PostText.class, "postText") + .where("postText.post") + .eqExpression("post") + .whereOr() + .whereExpression( + "post_matches_websearch(postText.searchTerms, 'de', :searchQueryText) = true") + .whereExpression( + "post_matches_websearch(postText.searchTerms, 'en', :searchQueryText) = true") + .endOr() + .end() + .setParameter("searchQueryText", searchQuery); } - conditions.add(cb.equal(post.get(Post_.scope), Scope.top_level)); + cb = cb.where("post.scope").eq(Scope.top_level); - query.where(conditions.toArray(new Predicate[0])); - - return query; + return cb; } public final boolean isVisibleTo(@Nullable User user) { @@ -293,13 +284,18 @@ public abstract class Post> extends PanacheEntityBase { } public static PostPage>> findViewable( - PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) { - return findViewable(postFilter, session, viewer, owner, null, null, null); + PostFilter postFilter, + EntityManager em, + CriteriaBuilderFactory cbf, + @CheckForNull User viewer, + @CheckForNull User owner) { + return findViewable(postFilter, em, cbf, viewer, owner, null, null, null); } public static PostPage>> findViewable( PostFilter postFilter, - Session session, + EntityManager em, + CriteriaBuilderFactory cbf, @CheckForNull User viewer, @CheckForNull User owner, @CheckForNull Integer cursor, @@ -316,12 +312,13 @@ public abstract class Post> extends PanacheEntityBase { default: entityClass = Post.class; } - return findViewable(entityClass, session, viewer, owner, cursor, count, searchQuery); + return findViewable(entityClass, em, cbf, viewer, owner, cursor, count, searchQuery); } protected static >> PostPage findViewable( Class entityClass, - Session session, + EntityManager em, + CriteriaBuilderFactory cbf, @CheckForNull User viewer, @CheckForNull User owner, @CheckForNull Integer cursor, @@ -332,10 +329,9 @@ public abstract class Post> extends PanacheEntityBase { Objects.requireNonNull(count); } - var cb = session.getCriteriaBuilder(); - - var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true, searchQuery); - var forwardQuery = session.createQuery(forwardCriteria); + var forwardCriteria = + queryViewable(entityClass, viewer, owner, cursor, em, cbf, true, searchQuery); + var forwardQuery = forwardCriteria.getQuery(); if (count != null) { forwardQuery.setMaxResults(count + 1); @@ -347,8 +343,8 @@ public abstract class Post> extends PanacheEntityBase { if (cursor != null) { // Look backwards as well so we can find the prevCursor. var backwardCriteria = - queryViewable(entityClass, viewer, owner, cursor, cb, false, searchQuery); - var backwardQuery = session.createQuery(backwardCriteria); + queryViewable(entityClass, viewer, owner, cursor, em, cbf, false, searchQuery); + var backwardQuery = backwardCriteria.getQuery(); backwardQuery.setMaxResults(count); var backwardResults = backwardQuery.getResultList(); if (!backwardResults.isEmpty()) { @@ -356,7 +352,7 @@ public abstract class Post> extends PanacheEntityBase { } } - var forwardResults = (List) forwardQuery.getResultList(); + var forwardResults = new ArrayList(forwardQuery.getResultList()); if (count != null) { if (forwardResults.size() == count + 1) { nextCursor = forwardResults.get(count).id; diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java index e808b9c..8de4677 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java @@ -5,6 +5,7 @@ import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.TEXT_HTML; +import com.blazebit.persistence.CriteriaBuilderFactory; import com.rometools.rome.feed.atom.Content; import com.rometools.rome.feed.atom.Entry; import com.rometools.rome.feed.atom.Feed; @@ -110,6 +111,8 @@ public abstract class PostResource { @PersistenceContext protected EntityManager entityManager; + @Inject protected CriteriaBuilderFactory criteriaBuilderFactory; + private final SecureRandom secureRandom; private final PostFilter postFilter; @@ -132,8 +135,16 @@ public abstract class PostResource { maxResults = maxResults == null ? defaultMaxResults : maxResults; @CheckForNull var reader = getCurrentUser(); - var session = entityManager.unwrap(Session.class); - var q = Post.findViewable(postFilter, session, reader, null, cursor, maxResults, searchQuery); + var q = + Post.findViewable( + postFilter, + entityManager, + criteriaBuilderFactory, + reader, + null, + cursor, + maxResults, + searchQuery); q.cacheDescriptions(); @@ -170,8 +181,16 @@ public abstract class PostResource { @CheckForNull var reader = getCurrentUser(); var owner = User.findByNickname(ownerName); - var session = entityManager.unwrap(Session.class); - var q = Post.findViewable(postFilter, session, reader, owner, cursor, maxResults, null); + var q = + Post.findViewable( + postFilter, + entityManager, + criteriaBuilderFactory, + reader, + owner, + cursor, + maxResults, + null); q.cacheDescriptions(); @@ -280,7 +299,7 @@ public abstract class PostResource { private String makeFeed( @CheckForNull User reader, @Nullable User owner, @Nullable String ownerName) throws FeedException { - var q = Post.findViewable(postFilter, entityManager.unwrap(Session.class), reader, owner); + var q = Post.findViewable(postFilter, entityManager, criteriaBuilderFactory, reader, owner); q.cacheDescriptions(); var posts = q.posts; diff --git a/src/main/java/eu/mulk/mulkcms2/common/hibernate/HibernateMetadataBuilderContributor.java b/src/main/java/eu/mulk/mulkcms2/common/hibernate/HibernateMetadataBuilderContributor.java new file mode 100644 index 0000000..31f99b2 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/common/hibernate/HibernateMetadataBuilderContributor.java @@ -0,0 +1,12 @@ +package eu.mulk.mulkcms2.common.hibernate; + +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.spi.MetadataBuilderContributor; + +public final class HibernateMetadataBuilderContributor implements MetadataBuilderContributor { + + @Override + public void contribute(MetadataBuilder metadataBuilder) { + metadataBuilder.applySqlFunction("post_matches_websearch", new PostMatchesWebsearchFunction()); + } +} diff --git a/src/main/java/eu/mulk/mulkcms2/common/hibernate/PostMatchesWebsearchFunction.java b/src/main/java/eu/mulk/mulkcms2/common/hibernate/PostMatchesWebsearchFunction.java new file mode 100644 index 0000000..9c10615 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/common/hibernate/PostMatchesWebsearchFunction.java @@ -0,0 +1,11 @@ +package eu.mulk.mulkcms2.common.hibernate; + +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.type.BooleanType; + +public final class PostMatchesWebsearchFunction extends SQLFunctionTemplate { + + public PostMatchesWebsearchFunction() { + super(BooleanType.INSTANCE, "(?1 @@ websearch_to_tsquery(language_regconfig(?2), ?3))"); + } +} -- cgit v1.2.3