Chapter 3. Type-Safe Querying Using Querydsl

Writing queries to access data is usually done using Java Strings. The query languages of choice have been SQL for JDBC as well as HQL/JPQL for Hibernate/JPA. Defining the queries in plain Strings is powerful but quite error-prone, as it’s very easy to introduce typos. Beyond that, there’s little coupling to the actual query source or sink, so column references (in the JDBC case) or property references (in the HQL/JPQL context) become a burden in maintenance because changes in the table or object model cannot be reflected in the queries easily.

The Querydsl project tries to tackle this problem by providing a fluent API to define these queries. The API is derived from the actual table or object model but is highly store- and model-agnostic at the same time, so it allows you to create and use the query API for a variety of stores. It currently supports JPA, Hibernate, JDO, native JDBC, Lucene, Hibernate Search, and MongoDB. This versatility is the main reason why the Spring Data project integrates with Querydsl, as Spring Data also integrates with a variety of stores. The following sections will introduce you to the Querydsl project and its basic concepts. We will go into the details of the store-specific support in the store-related chapters later in this book.

Introduction to Querydsl

When working with Querydsl, you will usually start by deriving a metamodel from your domain classes. Although the library can also use plain String literals, creating the metamodel will unlock the full power of Querydsl, especially its type-safe property and keyword references. The derivation mechanism is based on the Java 6 Annotation Processing Tool (APT), which allows for hooking into the compiler and processing the sources or even compiled classes. For details, read up on that topic in Generating the Query Metamodel. To kick things off, we need to define a domain class like the one shown in Example 3-1. We model our Customer with a few primitive and nonprimitive properties.

Example 3-1. The Customer domain class

@QueryEntity
public class Customer extends AbstractEntity {

  private String firstname, lastname;
  private EmailAddress emailAddress;
  private Set<Address> addresses = new HashSet<Address>();

  
}

Note that we annotate the class with @QueryEntity. This is the default annotation, from which the Querydsl annotation processor generates the related query class. When you’re using the integration with a particular store, the APT processor will be able to recognize the store-specific annotations (e.g., @Entity for JPA) and use them to derive the query classes. As we’re not going to work with a store for this introduction and thus cannot use a store-specific mapping annotation, we simply stick with @QueryEntity. The generated Querydsl query class will now look like Example 3-2.

Example 3-2. The Querydsl generated query class

@Generated("com.mysema.query.codegen.EntitySerializer")
public class QCustomer extends EntityPathBase<Customer> {

  public static final QCustomer customer = new QCustomer("customer");
  public final QAbstractEntity _super = new QAbstractEntity(this);

  public final NumberPath<Long> id = _super.id;
  public final StringPath firstname = createString("firstname");
  public final StringPath lastname = createString("lastname");
  public final QEmailAddress emailAddress;

  public final SetPath<Address, QAddress> addresses = 
         this.<Address, QAddress>createSet("addresses", Address.class, QAddress.class);

  
}

You can find these classes in the target/generated-sources/queries folder of the module’s sample project. The class exposes public Path properties and references to other query classes (e.g., QEmailAddress). This enables your IDE to list the available paths for which you might want to define predicates during code completion. You can now use these Path expressions to define reusable predicates, as shown in Example 3-3.

Example 3-3. Using the query classes to define predicates

QCustomer customer = QCustomer.customer;

BooleanExpression idIsNull = customer.id.isNull();
BooleanExpression lastnameContainsFragment = customer.lastname.contains("thews");
BooleanExpression firstnameLikeCart = customer.firstname.like("Cart");

EmailAddress reference = new EmailAddress("[email protected]");
BooleanExpression isDavesEmail = customer.emailAddress.eq(reference);

We assign the static QCustomer.customer instance to the customer variable to be able to concisely refer to its property paths. As you can see, the definition of a predicate is clean, concise, and—most importantly—type-safe. Changing the domain class would cause the query metamodel class to be regenerated. Property references that have become invalidated by this change would become compiler errors and thus give us hints to places that need to be adapted. The methods available on each of the Path types take the type of the Path into account (e.g., the like(…) method makes sense only on String properties and thus is provided only on those).

Because predicate definitions are so concise, they can easily be used inside a method declaration. On the other hand, we can easily define predicates in a reusable manner, building up atomic predicates and combining them with more complex ones by using concatenating operators like And and Or (see Example 3-4).

Example 3-4. Concatenating atomic predicates

QCustomer customer = QCustomer.customer;

BooleanExpression idIsNull = customer.id.isNull();

EmailAddress reference = new EmailAddress("[email protected]");
BooleanExpression isDavesEmail = customer.emailAddress.eq(reference);

BooleanExpression idIsNullOrIsDavesEmail = idIsNull.or(isDavesEmail);

We can use our newly written predicates to define a query for either a particular store or plain collections. As the support for store-specific query execution is mainly achieved through the Spring Data repository abstraction, have a look at Integration with Spring Data Repositories. We’ll use the feature to query collections as an example now to keep things simple. First, we set up a variety of Products to have something we can filter, as shown in Example 3-5.

Example 3-5. Setting up Products

Product macBook = new Product("MacBook Pro", "Apple laptop");
Product iPad = new Product("iPad", "Apple tablet");
Product iPod = new Product("iPod", "Apple MP3 player");
Product turntable = new Product("Turntable", "Vinyl player");

List<Product> products = Arrays.asList(macBook, iPad, iPod, turntable);

Next, we can use the Querydsl API to actually set up a query against the collection, which is some kind of filter on it (Example 3-6).

Example 3-6. Filtering Products using Querydsl predicates

QProduct $ = QProduct.product;
List<Product> result = from($, products).where($.description.contains("Apple")).list($);

assertThat(result, hasSize(3));
assertThat(result, hasItems(macBook, iPad, iPod));

We’re setting up a Querydsl Query using the from(…) method, which is a static method on the MiniAPI class of the querydsl-collections module. We hand it an instance of the query class for Product as well as the source collection. We can now use the where(…) method to apply predicates to the source list and execute the query using one of the list(…) methods (Example 3-7). In our case, we’d simply like to get back the Product instances matching the defined predicate. Handing $.description into the list(…) method would allow us to project the result onto the product’s description and thus get back a collection of Strings.

Example 3-7. Filtering Products using Querydsl predicates (projecting)

QProduct $ = QProduct.product;
BooleanExpression descriptionContainsApple = $.description.contains("Apple");
List<String> result = from($, products).where(descriptionContainsApple).list($.name);

assertThat(result, hasSize(3));
assertThat(result, hasItems("MacBook Pro", "iPad", "iPod"));

As we have discovered, Querydsl allows us to define entity predicates in a concise and easy way. These can be generated from the mapping information for a variety of stores as well as for plain Java classes. Querydsl’s API and its support for various stores allows us to generate predicates to define queries. Plain Java collections can be filtered with the very same API.

Generating the Query Metamodel

As we’ve just seen, the core artifacts with Querydsl are the query metamodel classes. These classes are generated via the Annotation Processing Toolkit, part of the javac Java compiler in Java 6. The APT provides an API to programmatically inspect existing Java source code for certain annotations, and then call functions that in turn generate Java code. Querydsl uses this mechanism to provide special APT processor implementation classes that inspect annotations. Example 3-1 used Querydsl-specific annotations like @QueryEntity and @QueryEmbeddable. If we already have domain classes mapped to a store supported by Querydsl, then generating the metamodel classes will require no extra effort. The core integration point here is the annotation processor you hand to the Querydsl APT. The processors are usually executed as a build step.

Build System Integration

To integrate with Maven, Querydsl provides the maven-apt-plugin, with which you can configure the actual processor class to be used. In Example 3-8, we bind the process goal to the generate-sources phase, which will cause the configured processor class to inspect classes in the src/main/java folder. To generate metamodel classes for classes in the test sources (src/test/java), add an execution of the test-process goal to the generate-test-sources phase.

Example 3-8. Setting up the Maven APT plug-in

<project >
  <build>
    <plugins>  
      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>maven-apt-plugin</artifactId>
        <version>1.0.2</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor><!-- fully-qualified processor class name --></processor>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Supported Annotation Processors

Querydsl ships with a variety of APT processors to inspect different sets of annotations and generate the metamodel classes accordingly.

QuerydslAnnotationProcessor

The very core annotation processor inspects Querydsl-specific annotations like @QueryEntity and @QueryEmbeddable.

JPAAnnotationProcessor

Inspects javax.persistence annotations, such as @Entity and @Embeddable.

HibernateAnnotationProcessor

Similar to the JPA processor but adds support for Hibernate-specific annotations.

JDOAnnotationProcessor

Inspects JDO annotations, such as @PersistenceCapable and @EmbeddedOnly.

MongoAnnotationProcessor

A Spring Data–specific processor inspecting the @Document annotation. Read more on this in The Mapping Subsystem.

Querying Stores Using Querydsl

Now that we have the query classes in place, let’s have a look at how we can use them to actually build queries for a particular store. As already mentioned, Querydsl provides integration modules for a variety of stores that offer a nice and consistent API to create query objects, apply predicates defined via the generated query metamodel classes, and eventually execute the queries.

The JPA module, for example, provides a JPAQuery implementation class that takes an EntityManager and provides an API to apply predicates before execution; see Example 3-9.

Example 3-9. Using Querydsl JPA module to query a relational store

EntityManager entityManager =  // obtain EntityManager
JPAQuery query = new JPAQuery(entityManager);

QProduct $ = QProduct.product;
List<Product> result = query.from($).where($.description.contains("Apple")).list($);

assertThat(result, hasSize(3));
assertThat(result, hasItems(macBook, iPad, iPod));

If you remember Example 3-6, this code snippet doesn’t look very different. In fact, the only difference here is that we use the JPAQuery as the base, whereas the former example used the collection wrapper. So you probably won’t be too surprised to see that there’s not much difference in implementing this scenario for a MongoDB store (Example 3-10).

Example 3-10. Using Querydsl MongoDB module with Spring Data MongoDB

MongoOperations operations =  // obtain MongoOperations
MongodbQuery query = new SpringDataMongodbQuery(operations, Product.class);

QProduct $ = QProduct.product;
List<Product> result = query.where($.description.contains("Apple").list();

assertThat(result, hasSize(3));
assertThat(result, hasItems(macBook, iPad, iPod));

Integration with Spring Data Repositories

As you just saw, the execution of queries with Querydsl generally consists of three major steps:

  1. Setting up a store-specific query instance

  2. Applying a set of filter predicates to it

  3. Executing the query instance, potentially applying projections to it

Two of these steps can be considered boilerplate, as they will usually result in very similar code being written. On the other hand, the Spring Data repository tries to help users reduce the amount of unnecessary code; thus, it makes sense to integrate the repository extraction with Querydsl.

Executing Predicates

The core of the integration is the QueryDslPredicateExecutor interface ,which specifies the API that clients can use to execute Querydsl predicates in the flavor of the CRUD methods provided through CrudRepository. See Example 3-11.

Example 3-11. The QueryDslPredicateExecutor interface

public interface QueryDslPredicateExecutor<T> {

  T findOne(Predicate predicate);

  Iterable<T> findAll(Predicate predicate);
  Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);

  Page<T> findAll(Predicate predicate, Pageable pageable);
  long count(Predicate predicate);
}

Currently, Spring Data JPA and MongoDB support this API by providing implementation classes implementing the QueryDslPredicateExecutor interface shown in Example 3-11. To expose this API through your repository interfaces, let it extend QueryDslPredicateExecutor in addition to Repository or any of the other available base interfaces (see Example 3-12).

Example 3-12. The CustomerRepository interface extending QueryDslPredicateExecutor

public interface CustomerRepository extends Repository<Customer, Long>,
                                            QueryDslPredicateExecutor<Customer> {
  
}

Extending the interface will have two important results: the first—and probably most obvious—is that it pulls in the API and thus exposes it to clients of CustomerRepository. Second, the Spring Data repository infrastructure will inspect each repository interface found to determine whether it extends QueryDslPredicateExecutor. If it does and Querydsl is present on the classpath, Spring Data will select a special base class to back the repository proxy that generically implements the API methods by creating a store-specific query instance, bind the given predicates, potentially apply pagination, and eventually execute the query.

Manually Implementing Repositories

The approach we have just seen solves the problem of generically executing queries for the domain class managed by the repository. However, you cannot execute updates or deletes through this mechanism or manipulate the store-specific query instance. This is actually a scenario that plays nicely into the feature of repository abstraction, which allows you to selectively implement methods that need hand-crafted code (see Manually Implementing Repository Methods for general details on that topic). To ease the implementation of a custom repository extension, we provide store-specific base classes. For details on that, check out the sections Repository Querydsl Integration and Mongo Querydsl Integration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.236.27