JPA Criteria Metamodel Generation and Usage Guide
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, I’m going to show you how to generate the JPA Metamodel using Hibernate, and how you can use it to write typesafe Criteria API queries.
Domain Model
Let’s assume our application uses the following Post
, PostComment
, PostDetails
, and Tag
entities, which form a one-to-many, one-to-one, and many-to-many table relationships:
How to generate the JPA Criteria Metamodel
The hibernate-jpamodelgen
tool provided by Hibernate ORM can be used to scan the project entities and generate the JPA Criteria Metamodel. All you need to do is add the following dependency to your project:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>${hibernate.version}</version> </dependency>
Now, when the project is compiled, you can see that in the target
folder, the following Java classes are generated:
> tree target/generated-sources/ target/generated-sources/ └── annotations └── com └── vladmihalcea └── book └── hpjp └── hibernate ├── forum │ ├── PostComment_.java │ ├── PostDetails_.java │ ├── Post_.java │ └── Tag_.java
Tag entity Metamodel
If the Tag
entity is mapped as follows:
@Entity @Table(name = "tag") public class Tag { @Id private Long id; private String name; //Getters and setters omitted for brevity }
The Tag_
Metamodel class is generated like this:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") @StaticMetamodel(Tag.class) public abstract class Tag_ { public static volatile SingularAttribute<Tag, String> name; public static volatile SingularAttribute<Tag, Long> id; public static final String NAME = "name"; public static final String ID = "id"; }
The SingularAttribute
is used for the basic id
and name
Tag
JPA entity attributes.
Post entity Metamodel
The Post
entity is mapped like this:
@Entity @Table(name = "post") public class Post { @Id private Long id; private String title; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private List<PostComment> comments = new ArrayList<>(); @OneToOne( mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY ) @LazyToOne(LazyToOneOption.NO_PROXY) private PostDetails details; @ManyToMany @JoinTable( name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private List<Tag> tags = new ArrayList<>(); //Getters and setters omitted for brevity }
The Post
entity has two basic attributes, id
and title
, a one-to-many comments
collection, a one-to-one details
association, and a many-to-many tags
collection.
The Post_
Metamodel class is generated as follows:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") @StaticMetamodel(Post.class) public abstract class Post_ { public static volatile ListAttribute<Post, PostComment> comments; public static volatile SingularAttribute<Post, PostDetails> details; public static volatile SingularAttribute<Post, Long> id; public static volatile SingularAttribute<Post, String> title; public static volatile ListAttribute<Post, Tag> tags; public static final String COMMENTS = "comments"; public static final String DETAILS = "details"; public static final String ID = "id"; public static final String TITLE = "title"; public static final String TAGS = "tags"; }
The basic id
and title
attributes, as well as the one-to-one details
association, are represented by a SingularAttribute
while the comments
and tags
collections are represented by the JPA ListAttribute
.
PostDetails entity Metamodel
The PostDetails
entity is mapped like this:
@Entity @Table(name = "post_details") public class PostDetails { @Id @GeneratedValue private Long id; @Column(name = "created_on") private Date createdOn; @Column(name = "created_by") private String createdBy; @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "id") private Post post; //Getters and setters omitted for brevity }
All entity attributes are going to be represented by the JPA SingularAttribute
in the associated PostDetails_
Metamodel class:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") @StaticMetamodel(PostDetails.class) public abstract class PostDetails_ { public static volatile SingularAttribute<PostDetails, Post> post; public static volatile SingularAttribute<PostDetails, String> createdBy; public static volatile SingularAttribute<PostDetails, Long> id; public static volatile SingularAttribute<PostDetails, Date> createdOn; public static final String POST = "post"; public static final String CREATED_BY = "createdBy"; public static final String ID = "id"; public static final String CREATED_ON = "createdOn"; }
PostComment entity Metamodel
The PostComment
is mapped as follows:
@Entity @Table(name = "post_comment") public class PostComment { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; //Getters and setters omitted for brevity }
And, all entity attributes are represented by the JPA SingularAttribute
in the associated PostComments_
Metamodel class:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") @StaticMetamodel(PostComment.class) public abstract class PostComment_ { public static volatile SingularAttribute<PostComment, Post> post; public static volatile SingularAttribute<PostComment, String> review; public static volatile SingularAttribute<PostComment, Long> id; public static final String POST = "post"; public static final String REVIEW = "review"; public static final String ID = "id"; }
Using the JPA Criteria Metamodel
Without the JPA Metamodel, a Criteria API query that needs to fetch the PostComment
entities filtered by their associated Post
title would look like this:
CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<PostComment> query = builder.createQuery(PostComment.class); Root<PostComment> postComment = query.from(PostComment.class); Join<PostComment, Post> post = postComment.join("post"); query.where( builder.equal( post.get("title"), "High-Performance Java Persistence" ) ); List<PostComment> comments = entityManager .createQuery(query) .getResultList();
Notice that we used the post
String literal when creating the Join
instance, and we used the title
String literal when referencing the Post
title
.
The JPA Metamodel allows us to avoid hard-coding entity attributes, as illustrated by the following example:
CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<PostComment> query = builder.createQuery(PostComment.class); Root<PostComment> postComment = query.from(PostComment.class); Join<PostComment, Post> post = postComment.join(PostComment_.post); query.where( builder.equal( post.get(Post_.title), "High-Performance Java Persistence" ) ); List<PostComment> comments = entityManager .createQuery(query) .getResultList();
Writing JPA Criteria API queries is much easier if you are using a code completion tool like Codota. Check out this article for more details about the Codota IDE plugin.
Or, let’s say we want to fetch a DTO projection while filtering the Post
title
and the PostDetails
createdOn
attributes.
We can use the Metamodel when creating the join attributes, as well as when building the DTO projection column aliases or when referencing the entity attributes we need to filter:
CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<Object[]> query = builder.createQuery(Object[].class); Root<PostComment> postComment = query.from(PostComment.class); Join<PostComment, Post> post = postComment.join(PostComment_.post); query.multiselect( postComment.get(PostComment_.id).alias(PostComment_.ID), postComment.get(PostComment_.review).alias(PostComment_.REVIEW), post.get(Post_.title).alias(Post_.TITLE) ); query.where( builder.and( builder.like( post.get(Post_.title), "%Java Persistence%" ), builder.equal( post.get(Post_.details).get(PostDetails_.CREATED_BY), "Vlad Mihalcea" ) ) ); List<PostCommentSummary> comments = entityManager .createQuery(query) .unwrap(Query.class) .setResultTransformer(Transformers.aliasToBean(PostCommentSummary.class)) .getResultList();
Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The JPA Metamodel is very useful as it allows us to build typesafe Criteria API queries. Generating the Metamodel is straightforward, as Hibernate offers the jpamodelgen
tool for this purpose.
If we rename an entity attribute we no longer have to worry that we missed updating a query because the project is not even going to compile if the Metamodel changes and queries reference an old JPA entity representation.
