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
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 @@
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.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.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 @@
}
}
- protected static <T extends Post> CriteriaQuery<T> queryViewable(
+ protected static <T extends Post> CriteriaBuilder<T> queryViewable(
Class<T> entityClass,
@CheckForNull User reader,
@CheckForNull User owner,
@CheckForNull Integer cursor,
- CriteriaBuilder cb,
+ EntityManager em,
+ CriteriaBuilderFactory cbf,
boolean forward,
@CheckForNull String searchQuery) {
- CriteriaQuery<T> query = cb.createQuery(entityClass);
- var conditions = new ArrayList<Predicate>();
+ CriteriaBuilder<T> cb = cbf.create(em, entityClass).select("post");
- From<?, T> 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<?, T>) root.join(User_.visiblePosts);
+ cb = cb.innerJoin("user.visiblePosts", "post");
} else if (entityClass.isAssignableFrom(Bookmark.class)) {
- post = (From<?, T>) root.join(User_.visibleBookmarks);
+ cb = cb.innerJoin("user.visibleBookmark", "post");
} else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
- post = (From<?, T>) 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 static PostPage<Post<? extends PostText<?>>> 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<Post<? extends PostText<?>>> findViewable(
PostFilter postFilter,
- Session session,
+ EntityManager em,
+ CriteriaBuilderFactory cbf,
@CheckForNull User viewer,
@CheckForNull User owner,
@CheckForNull Integer cursor,
@@ -316,12 +312,13 @@
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 <T extends Post<? extends PostText<?>>> PostPage<T> findViewable(
Class<? extends T> entityClass,
- Session session,
+ EntityManager em,
+ CriteriaBuilderFactory cbf,
@CheckForNull User viewer,
@CheckForNull User owner,
@CheckForNull Integer cursor,
@@ -332,10 +329,9 @@
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 @@
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 @@
}
}
- var forwardResults = (List<T>) forwardQuery.getResultList();
+ var forwardResults = new ArrayList<T>(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_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 @@
@PersistenceContext protected EntityManager entityManager;
+ @Inject protected CriteriaBuilderFactory criteriaBuilderFactory;
+
private final SecureRandom secureRandom;
private final PostFilter postFilter;
@@ -132,8 +135,16 @@
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 @@
@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 @@
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))");
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index bbffd39..ee95952 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,5 +1,3 @@
-quarkus.log.console.color = true
-
quarkus.log.level = INFO
#quarkus.log.category."org.hibernate".level = INFO
#quarkus.log.category."io.quarkus.oidc".level = FINEST
@@ -16,6 +14,8 @@
quarkus.datasource.jdbc.max-size = 8
quarkus.datasource.jdbc.min-size = 0
+quarkus.hibernate-orm.metadata-builder-contributor = eu.mulk.mulkcms2.common.hibernate.HibernateMetadataBuilderContributor
+
quarkus.liquibase.migrate-at-start = true
%dev.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mulkcms