Querydsl vs. JPA Criteria, Part 2: Metamodel
In part two of a series dedicated to the Querydsl framework, this tutorial demonstrates how to use a metamodel with JPA Criteria and Querydsl.
Join the DZone community and get the full member experience.
Join For FreeThis is the second article in my series dedicated to the Querydsl framework. I planned to shed light on the custom queries, as promised in the first article, but I decided to explain the metamodel usage first in order to simplify the explanation later on.
So far, this series contains these articles:
- Introduction
- Metadata (this article)
In This Article, You Will Learn:
- What is the JPA Metamodel?
- Using metadata with JPA Criteria
- Using metadata with Querydsl
What Is the JPA Metamodel?
The Canonical Metamodel used by JPA was introduced in order to tackle the following issues:
- Provide an easy way to offer all available attributes: We can use auto-completion provided by our IDE to find the desired attribute.
- Make refactoring easier: We can easily identify all the affected queries when we change any attribute's name.
Basically, we want to avoid literal values in our queries and rely on a generated type-safe metamodel class. The goal of the JPA Static Metamodel Generator is to automatically generate at the build time the Metamodel classes from our entities. Therefore, we can keep our queries up-to-date.
Note: Read another good explanation of a need for the JPA Metamodel.
JPA Criteria
Let's start with JPA Criteria as it represents a traditional approach.
Recapitulation
Let's use a findAllCitiesBy
method to search cities by some attributes like this:
public List<City> findAllCitiesBy(@NonNull String cityName, @NonNull String cityState, @NonNull String countryName) {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(City.class);
Root<City> cityRoot = query.from(City.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.like(cityRoot.get("name"), cityName));
predicates.add(cb.like(cityRoot.get("state"), cityState));
predicates.add(cb.equal(cityRoot.get("country").get("name"), cb.literal(countryName)));
query.where(predicates.toArray(new Predicate[0]));
return em.createQuery(query).getResultList();
}
Note: This method was introduced in the "Custom Queries" chapter in the introductory article of this series, as linked above.
We can see several literal values on lines 7-9 (e.g., name
or state
). Our goal is to get rid of them and use the generated metamodel instead.
Maven Configuration
First of all, we need to add the hibernate-jpamodelgen
dependency to our Maven project (pom.xml
). We can find the latest available version in the Maven Central repository.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.6.11.Final</version>
</dependency>
Note: The version is not the latest one (as you can see by the provided link) because we use the version governed by Spring Boot. Honestly, we can skip it entirely. The version is mentioned here just for clarity.
Generated Metamodel
The hibernate-jpamodelgen
dependency generates classes with the "_" suffix. In our case, we have City_
class defined as:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(City.class)
public abstract class City_ {
public static volatile SingularAttribute<City, Country> country;
public static volatile SingularAttribute<City, String> name;
public static volatile SingularAttribute<City, Long> id;
public static volatile SingularAttribute<City, String> state;
public static final String COUNTRY = "country";
public static final String NAME = "name";
public static final String ID = "id";
public static final String STATE = "state";
}
Now, we can either switch from literal to constant (e.g., COUNTRY
) or use type-safe references (e.g., country
).
Usage
Let's see some very similar options for using the generated metamodel.
Constant for a Literal
The easiest option is just to replace the literal value with the generated one (see lines 7-9).
public List<City> findAllCitiesBy(@NonNull String cityName, @NonNull String cityState, @NonNull String countryName) {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(City.class);
Root<City> cityRoot = query.from(City.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.like(cityRoot.get(City_.NAME), cityName));
predicates.add(cb.like(cityRoot.get(City_.STATE), cityState));
predicates.add(cb.equal(cityRoot.get(City_.COUNTRY).get(Country_.NAME), cb.literal(countryName)));
query.where(predicates.toArray(new Predicate[0]));
return em.createQuery(query).getResultList();
}
Constant With a Type
We can also use a type-safe way with constants using, for example, the SingularAttribute
type.
public List<City> findAllCitiesBy(@NonNull String cityName, @NonNull String cityState, @NonNull String countryName) {
...
predicates.add(cb.like(cityRoot.get(City_.name), cityName));
predicates.add(cb.like(cityRoot.get(City_.state), cityState));
predicates.add(cb.equal(cityRoot.get(City_.country).get(Country_.name), cb.literal(countryName)));
...
}
Note: There are more types for other cases, but it's out of the scope of this article.
Static Import
We can even simplify it with static imports.
import static com.github.aha.sat.jpa.city.City_.country;
import static com.github.aha.sat.jpa.city.City_.name;
import static com.github.aha.sat.jpa.city.City_.state;
public List<City> findAllCitiesBy(@NonNull String cityName, @NonNull String cityState, @NonNull String countryName) {
...
predicates.add(cb.like(cityRoot.get(name), cityName));
predicates.add(cb.like(cityRoot.get(state), cityState));
predicates.add(cb.equal(cityRoot.get(country).get(Country_.name), cb.literal(countryName)));
...
}
Note: We must be very careful about collisions. As you can see, we need to keep Country_.name
. There cannot be two static imports for the name
.
Querydsl
The Querydsl framework uses a type-safe approach by default. Therefore, we don't need to do anything in order to use it.
Generated Metamodel
The Querydsl produces classes with the "Q" prefix. In our case, we have QCountry
class, now with all the attributes and static variable country
for simplified usage.
/**
* QCountry is a Querydsl query type for Country
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QCountry extends EntityPathBase<Country> {
private static final long serialVersionUID = 1155880497L;
public static final QCountry country = new QCountry("country");
public final ListPath<com.github.aha.sat.jpa.city.City, com.github.aha.sat.jpa.city.QCity> cities = this.<com.github.aha.sat.jpa.city.City, com.github.aha.sat.jpa.city.QCity>createList("cities", com.github.aha.sat.jpa.city.City.class, com.github.aha.sat.jpa.city.QCity.class, PathInits.DIRECT2);
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath name = createString("name");
public QCountry(String variable) {
super(Country.class, forVariable(variable));
}
public QCountry(Path<? extends Country> path) {
super(path.getType(), path.getMetadata());
}
public QCountry(PathMetadata metadata) {
super(Country.class, metadata);
}
}
Usage
We can use the generated metamodel in several similar ways as well.
Variable
The official documentation recommends using a variable (see line 2).
public List<Country> findAllCountriesBy(@NonNull String cityName, @NonNull String cityState) {
var city = QCity.city;
return new JPAQuery<Country>(em)
.select(city.country)
.from(city)
.where(city.name.like(cityName)
.and(city.state.like(cityState)))
.fetch();
}
See Querying JPA for more information.
Full Path
Another option is to use it directly with a full path (see QCity.city
).
public List<Country> findAllCountriesBy(@NonNull String cityName, @NonNull String cityState) {
return new JPAQuery<Country>(em)
.select(QCity.city.country)
.from(QCity.city)
.where(QCity.city.name.like(cityName)
.and(QCity.city.state.like(cityState)))
.fetch();
}
Warning: This approach is ok unless we need to use another root for the same entity.
Static Import
The last option, my preferred one, is using a static import. This way our code looks similar to SQL.
import static com.github.aha.sat.jpa.city.QCity.city;
public List<Country> findAllCountriesBy(@NonNull String cityName, @NonNull String cityState) {
return new JPAQuery<Country>(em)
.select(city.country)
.from(city)
.where(city.name.like(cityName)
.and(city.state.like(cityState)))
.fetch();
}
Conclusion
This article has covered what the JPA metamodel is and how to use it in our project. First, the metamodel for JPA Criteria was explained. Next, we explained the metamodel usage with the Querydsl framework. Both technologies have several ways to use the generated metamodel. It's up to the developer's preference which one fits best.
The complete source code presented above is available in my GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments