Programming

Querydsl vs. JPA Criteria, Part 2: Metamodel

Spread the love


This 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:

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.



Source link

Related Articles

Leave a Reply

Your email address will not be published.

Back to top button