© Joseph B. Ottinger, Jeff Linwood and Dave Minter 2016

Joseph B. Ottinger, Jeff Linwood and Dave Minter, Beginning Hibernate, 10.1007/978-1-4842-2319-2_10

10. Advanced Queries Using Criteria

Joseph B. Ottinger, Jeff Linwood2 and Dave Minter3

(1)Youngsville, North Carolina, USA

(2)Austin, Texas, USA

(3)London, UK

Hibernate provides three different ways to retrieve data. We have already discussed HQL and the use of native SQL queries; now we add criteria.

A criteria query uses a model of the information you’re interested in finding. For example, if you were interested in Customer objects whose first name matched “John,” you’d build a Customer object, set the first name to “John,” and use that model object for the query.

This lets you build nested, structured query expressions in Java, providing a compile-time syntax checking that is not possible with a query language like HQL or SQL. The Criteria API also includes query by example (QBE) functionality. This lets you supply example objects that contain the properties you would like to retrieve instead of having to step-by-step spell out the components of the query. It also includes projection and aggregation methods, including counts.

Hibernate used to support two access paths for the Criteria API: a native criteria API and the criteria API exposed by JPA. In Hibernate 5, the native criteria API has been deprecated, so for this chapter we’re going to switch to the JPA implementation of the Criteria API as implemented by Hibernate, employing the sample object model established in the previous chapter. (We are, however, going to move it to a new package, from chapter09.model to chapter10.model and, obviously, switch it to JPA.)

The criteria API can be very (very) powerful, but it has weaknesses as well – and whether you should use it depends on a number of factors, not the least of which is this: “are you able to understand it or not?” Before we try to address its use, let’s take a look at it first.

Using the Criteria API

The Criteria API uses quite a few concepts in order to build an object graph for a query. They include:

  • A CriteriaBuilder

  • A typed CriteriaQuery (where the “typed” refers to the returned object’s class type)

  • A root of a graph to build the query to contain the criteria by which objects are selected

  • A set of restrictions to limit the objects selected

  • A “metamodel” that represents the queried class types for easy and convenient reference

Let’s take a look at the simplest form of a Criteria query, first.

We’re going to reuse our object model from Chapter 9, so we’ll have Software, Product, and Supplier entities. We’re going to switch to JPA, so our database configuration needs to be META-INF/persistence.xml instead of our typical hibernate.cfg.xml. That should look like this:

<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://java.sun.com/xml/ns/persistence"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
              http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="chapter10">
        <class>chapter10.model.Product</class>
        <class>chapter10.model.Supplier</class>
        <class>chapter10.model.Software</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:./db10"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

The next thing we’d like to do is create a test class – in the book’s source code, it’s called chapter10.QueryTest– and we’re going to create a utility method to help hide a lot of our EntityManager-related boilerplate. This method will accept a lambda1 (a discretely runnable bit of code) and will call the lambda with a valid EntityManager as an argument. This is not a great method – we’re writing it for our own example, and it’s okay in this case that it doesn’t do a lot of error checking.

public class QueryTest {
    private void doWithEntityManager(Consumer<EntityManager> command) {
        EntityManager em = JPASessionUtil.getEntityManager("chapter10");
        em.getTransaction().begin();


        command.accept(em);
        if (em.getTransaction().isActive() &&
                !em.getTransaction().getRollbackOnly()) {
            em.getTransaction().commit();
        } else {
            em.getTransaction().rollback();
        }


        em.close();
    }
}

The next task is to write methods that load and clear some data. This will also conveniently show how to use the doWithEntityManager() method . Functionally, they will be exactly the same as the populateData() and cleanup() methods we saw in Chapter 9; the only difference is that it uses a lambda to avoid the manual EntityManager initialization.2

@BeforeMethod
public void populateData() {
    doWithEntityManager((em) -> {
        Supplier supplier = new Supplier("Hardware, Inc.");
        supplier.getProducts().add(
                new Product(supplier, "Optical Wheel Mouse", "Mouse", 5.00));
        supplier.getProducts().add(
                new Product(supplier, "Trackball Mouse", "Mouse", 22.00));
        em.persist(supplier);


        supplier = new Supplier("Hardware Are We");
        supplier.getProducts().add(
                new Software(supplier, "SuperDetect", "Antivirus", 14.95, "1.0"));
        supplier.getProducts().add(
                new Software(supplier, "Wildcat", "Browser", 19.95, "2.2"));
        supplier.getProducts().add(
                new Product(supplier, "AxeGrinder", "Gaming Mouse", 42.00));
        supplier.getProducts().add(
                new Product(supplier, "I5 Tablet", "Computer", 849.99));
        supplier.getProducts().add(
                new Product(supplier, "I7 Desktop", "Computer", 1599.99));
        em.persist(supplier);
    });
}


@AfterMethod
public void cleanup() {
    doWithEntityManager((em) -> {
        em.createQuery("delete from Software").executeUpdate();
        em.createQuery("delete from Product").executeUpdate();
        em.createQuery("delete from Supplier").executeUpdate();
    });
}

It’s finally time for us to look at an actual Criteria query. Our first query will be very simple, with no restrictions whatsoever and no reference to a metamodel. In JPQL or HQL, it’s going to be the equivalent of “from Product.” The operative line for building the query is the criteria.select(root); call, which maps to a SQL select; it tells the Criteria API what the primary type of the query should be.

@Test
public void testSimpleCriteriaQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);


        assertEquals(em.createQuery(criteria).getResultList().size(), 7);
    });
}

This code acquires a CriteriaBuilder, then builds an object graph around Product; since there are no restrictions (apart from “must be a Product”), it effectively queries the database for all Product objects, including Software objects (since they’re types of Products).

Adding restrictions is most easily done with a “metamodel,” a way of referring to the attributes in an entity. Normally, a metamodel is generated as part of the build process for a project, so we need to modify our pom.xml.3 Luckily, the metamodel generator is an annotation processor, so adding a dependency to the build is all we need to do:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>${hibernate-core.version}</version>
</dependency>

When you run this example with our sample data, you will get all objects that are instances of the Product class. Note that this includes any instances of the Software class because they are derived from Product.

Moving on from this simple example, we will add constraints to our criteria queries so we can whittle down the result set.

Using Restrictions with Criteria

We’ve already seen how the select() method serves as the root of the data model returned from the query. To add restrictions, we use the where() method, and add in restrictions. This method accepts either an Expression<Boolean> (something that indicates on a record-by-record basis whether the entity should be included in the results) or an array of Predicate references,4 which will serve the same purpose.

Let’s take a look at a simple query, one that looks for all Product records that have a description that exactly matches “Mouse.” We’re going to use our metamodel for Product (which gives us an easy way to refer to attributes of a given Product), and we’re also going to add a where() call that restricts the query to products that have a specific value – in this case, “Mouse.”

We are using a parameter in our query for a few reasons. One is that we might actually want to reuse this criteria query at some point… but the main one is that this helps prevent SQL injection attacks, because there’s no guarantee that the JPA implementation will sanitize the query before it is issued to the database. With the parameter, your data will be sanitized by the JDBC driver, and there’s not a potential attack vector.

@Test
public void testSimpleEQQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(
                builder.equal(
                        root.get(Product_.description),
                        builder.parameter(String.class, "description")
                )
        );


        criteria.select(root);

        assertEquals(em
                .createQuery(criteria)
                .setParameter("description", "Mouse")
                .getResultList()
                .size(), 2);
    });
}

Next, we search for products that do not have the name “Mouse.” For this, we would use the notEqual() method on the CriteriaBuilder class to obtain a not-equal restriction, and again, note the use of builder.parameter() to give us the chance to sanitize inputs:

@Test
public void testSimpleNEQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(
                builder.notEqual(
                        root.get(Product_.description),
                        builder.parameter(String.class, "description")
                )
        );


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("description", "Mouse")
                .getResultList().size(), 5);
    });
}
Tip

You cannot use the not-equal restriction to retrieve records with a NULL value in the database for that property (in SQL, and therefore in Hibernate, NULL represents the absence of data, and so cannot be compared with data). If you need to retrieve objects with NULL properties, you will have to use the isNull() restriction, which we discuss further on in the chapter. You can combine the two with an OR logical expression, which we also discuss later in the chapter.

Instead of searching for exact matches, we can retrieve all objects that have a property matching part of a given pattern. To do this, we need to create an SQL LIKE clause, with the like() method. The pattern we would match against would use % as a wildcard (supplied as a parameter), such that all matching records would have the last five characters of the description matching “Mouse.” Our query code would look like this:

@Test
public void testSimpleLikeQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(builder.like(
                root.get(Product_.description),
                builder.parameter(String.class, "description")));


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("description", "%Mouse")
                .getResultList().size(), 3);
    });
}

Note that the query in testSimpleLikeQuery() is case sensitive.5 If we want to not be case sensitive, we need to add more to our expression, by chaining the metamodel reference inside a CriteriaBuilder.lower() call. For safety, we also convert our matching expression – the “%mOUse” – to lowercase as well as part of the query, to coerce both expressions to the same format before evaluation.

@Test
public void testSimpleLikeIgnoreCaseQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(builder.like(
                builder.lower(root.get(Product_.description)),
                builder.lower(builder.parameter(String.class, "description")))
        );


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("description", "%mOUse")
                .getResultList().size(), 3);
    });
}

The isNull() and isNotNull() restrictions allow you to do a search for objects that have (or do not have) null property values. This is easy to demonstrate:

@Test
public void testNotNullQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(builder.isNull(
                builder.lower(root.get(Product_.description))));


        criteria.select(root);

        assertEquals(em.createQuery(criteria).getResultList().size(), 0);
    });
}

Several of the restrictions are useful for doing math comparisons. The greater-than comparison is gt() (with an alias of greaterThan()) the greater-than-or-equal-to comparison is greaterThanOrEqualTo(), the less-than comparison is lt() (with a similar alias of lessThan()) and the less-than-or-equal-to comparison is lessThanOrEqualTo(). We can do a quick retrieval of all products with prices over $25 like this:

@Test
public void testGTQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(builder.greaterThan(root.get(Product_.price),
                builder.parameter(Double.class, "price")));


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("price", 25.0)
                .getResultList().size(), 3);
    });
}

Likewise, the less-than-or-equal-to form is identical, but uses a different expression method:

@Test
public void testLTEQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(builder.lessThanOrEqualTo(root.get(Product_.price),
                builder.parameter(Double.class, "price")));


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("price", 25.0)
                .getResultList().size(), 4);
    });
}

Moving on, we can start to do more complicated queries with the Criteria API. For example, we can combine AND and OR restrictions in logical expressions. All we need to do is use the CriteriaBuilder.and() method, and pass it multiple evaluation expressions, like so:

@Test
public void testANDQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(
                builder.and(
                        builder.lessThanOrEqualTo(
                                root.get(Product_.price),
                                builder.parameter(Double.class, "price")
                        ),
                        builder.like(
                                builder.lower(
                                        root.get(Product_.description)
                                ),
                                builder.lower(
                                        builder.parameter(String.class, "description")
                                )
                        )
                )
        );


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("price", 10.0)
                .setParameter("description", "%mOUse")
                .getResultList().size(), 1);
    });
}

If we want to have two restrictions that return objects that satisfy either or both of the restrictions, we need to use the or() method from CriteriaBuilder, as we see in this query where we’re looking for any mouse or anything under $15.00, which matches one other product, our antivirus software:

@Test
public void testORQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(
                builder.or(
                        builder.lessThanOrEqualTo(
                                root.get(Product_.price),
                                builder.parameter(Double.class, "price")
                        ),
                        builder.like(
                                builder.lower(
                                        root.get(Product_.description)
                                ),
                                builder.lower(
                                        builder.parameter(String.class, "description")
                                )
                        )
                )
        );


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("price", 10.0)
                .setParameter("description", "%mOUse")
                .getResultList().size(), 3);
    });
}

This form of logical expression evaluation can be incredibly powerful. For example, if we wanted a more complex expression, we’d simply chain everything together in nested groups of or(), and(), not(), and so forth and so on. Let’s take one more look, and build a query where we’re interested in any mouse that’s less than $25.00 or anything over $999.6 It’s actually quite simple, although the code looks verbose:

@Test
public void testDisjunction() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);


        Metamodel m = em.getMetamodel();
        EntityType<Product> product = m.entity(Product.class);
        Root<Product> root = criteria.from(product);
        criteria.select(root);
        criteria.where(
                builder.or(
                        builder.and(
                                builder.lessThanOrEqualTo(
                                        root.get(Product_.price),
                                        builder.parameter(Double.class, "price_1")
                                ),
                                builder.like(
                                        builder.lower(
                                                root.get(Product_.description)
                                        ),
                                        builder.lower(
                                                builder.parameter(String.class, "desc_1")
                                        )
                                )
                        ),
                        builder.and(
                                builder.greaterThan(
                                        root.get(Product_.price),
                                        builder.parameter(Double.class, "price_2")
                                ),
                                builder.like(
                                        builder.lower(
                                                root.get(Product_.description)
                                        ),
                                        builder.lower(
                                                builder.parameter(String.class, "desc_2")
                                        )
                                )
                        )
                )
        );


        criteria.select(root);

        assertEquals(em.createQuery(criteria)
                .setParameter("price_1", 25.00)
                .setParameter("desc_1", "%mOUse")
                .setParameter("price_2", 999.0)
                .setParameter("desc_2", "Computer")
                .getResultList().size(), 3);
    });
}

Paging Through the Result Set

One common application pattern that criteria can address is pagination through the result set of a database query. When we say pagination , we mean an interface in which the user sees part of the result set at a time, with navigation to go forward and backward through the results – you can easily imagine a Swing application or a web application that shows only the first 20 or 50 records in a giant data set, rather than displaying 51,000 records at once. A naïve pagination implementation might load the entire result set into memory for each navigation action, and would usually lead to atrocious performance.7

If you are programming directly to the database, you would typically use proprietary database SQL or database cursors to support paging. Since the Criteria API uses the JPA Query mechanism , setFirstResult() and setMaxResults() are both available.

@Test
public void testPagination() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);
        TypedQuery<Product> query=em.createQuery(criteria);
        query.setFirstResult(2);
        query.setMaxResults(2);


        assertEquals(query.getResultList().size(), 2);
    });
}

As you can see, this would make paging through the result set easy. You can increase the first result you return (for example, from 1, to 21, to 41, etc.) to page through the result set. If you have only one result in your result set, the Criteria API has a shortcut method for obtaining just that object.

Obtaining a Unique Result

Sometimes you know you are going to return only zero or one object from a given query. This could be because you are calculating an aggregate (like COUNT, which we discuss later) or because your restrictions naturally lead to a unique result — when selecting upon a property under a unique constraint, for example. You may also limit the results of any result set to just the first result, using the setMaxResults() method discussed earlier. In any of these circumstances, if you want to obtain a single Object reference instead of a List, the getSingleResult() method on the Query returns an object or null. If there is more than one result, the getSingleResult() method throws a NonUniqueResultException.

Here, we see two tests: one demonstrates a query that, given our data, will return a single result, and the other demonstrates another query that returns more than one result, therefore throwing an exception (which our test expects).

@Test
public void testUniqueResult() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);
        criteria.where(builder.equal(root.get(Product_.price),
                builder.parameter(Double.class, "price")));


        assertEquals(em.createQuery(criteria)
                .setParameter("price", 14.95)
                .getSingleResult().getName(), "SuperDetect");
    });
}


@Test(expectedExceptions = NonUniqueResultException.class)
public void testUniqueResultWithException() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);
        criteria.where(builder.greaterThan(root.get(Product_.price),
                builder.parameter(Double.class, "price")));


        Product p = em.createQuery(criteria)
                .setParameter("price", 14.95)
                .getSingleResult();
        fail("Should have thrown an exception");
    });
}

Again, we stress that you need to make sure that your query returns only one or zero results if you use the getSingleResult() method . Otherwise, Hibernate will throw a NonUniqueResultException exception, which may not be what you would expect — Hibernate does not just pick the first result and return it.8

Sorting the Query’s Results

Sorting the query’s results works much the same way with criteria as it would with HQL or SQL. The Criteria API provides the CriteriaQuery.orderBy() method , which accepts a number of Order objects – created by the CriteriaBuilder object, via the asc() or desc() methods – and uses them to order the results. Here’s an example:

@Test
public void testOrderedQuery() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);


        criteria.orderBy(builder.asc(root.get(Product_.description)));

        List<Product> p=em.createQuery(criteria).getResultList();
        assertEquals(p.size(), 7);
        assertTrue(p.get(0).getPrice()<p.get(1).getPrice());
    });
}

If you want more order criteria, you’d simply add the additional orderings to the orderBy() method:

criteria.orderBy(
        builder.asc(root.get(Product_.description)),
        builder.asc(root.get(Product_.name))
        );

Associations

Associations are supported through the join() method on the Root class. You can add a join in both the select() and the where() expressions, depending on what you want to do.

Let’s see this in action. Let’s say that we’d like to get the Product entities for a specific Supplier. Since Product has a reference to Supplier, one way of doing this is to simply select products where the supplier name matches our query.

In HQL, it’d be pretty easy:

select p.* from Product p where p.supplier.name=:name

With the Criteria API, we build everything as if it were an ordinary query for Product. However, the where() method looks a little different: we add a join() and specify the field we’re testing against, as part of our equality test.

@Test
public void testGetProductsForSupplierFromProduct() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root);
        criteria.where(builder.equal(
                root.join(Product_.supplier).get(Supplier_.name),
                builder.parameter(String.class, "supplier_name"))
        );


        List<Product> p = em.createQuery(criteria)
                .setParameter("supplier_name", "Hardware Are We")
                .getResultList();


        assertEquals(p.size(), 5);
    });
}

There are other forms of this query, though. What if we wanted to start from Supplier, and grab the Product list that way? In this case, our Root is built from Supplier, and our select() specifies that we want the result of a join. Here’s the code:

@Test
public void testGetProductsForSupplierFromSupplier() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
        Root<Supplier> root = criteria.from(Supplier.class);
        criteria.select(root.join(Supplier_.products));
        criteria.where(builder.equal(
                root.get(Supplier_.name),
                builder.parameter(String.class, "supplier_name"))
        );
        TypedQuery<Product> query = em.createQuery(criteria);
        query.setParameter("supplier_name", "Hardware Are We");


        List<Product> p = query.getResultList();

        assertEquals(p.size(), 5);
    });
}

We can use the same concept to get all Supplier entities who sell inexpensive devices… in this case, we’ll say we’re interested in any Supplier who offers goods for less than $5. We want the result of the query to be a Supplier, but the Root of our query is a Product. Our selection says that we want all distinct Supplier entities who fit the query for the Product.

Note how we use the .distinct(true) method to prevent seeing a duplicate Supplier – which is exactly what we’d get if a Supplier had multiple products for less than $7.

@Test
public void testGetSuppliersWithProductUnder7() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Supplier> criteria = builder.createQuery(Supplier.class);
        Root<Product> root = criteria.from(Product.class);
        criteria.select(root.get(Product_.supplier))
                .distinct(true);
        criteria.where(builder.lessThanOrEqualTo(root.get(Product_.price),
                builder.parameter(Double.class, "price")));


        assertEquals(em.createQuery(criteria)
                .setParameter("price", 7.0)
                .getResultList().size(), 1);
    });
}

Projections and Aggregates

Instead of working with objects from the result set, you can treat the results from the result set as a set of rows and columns, also known as a projectionof the data. This is similar to how you would use data from a SELECT query with JDBC; also, Hibernate supports properties, aggregate functions, and the GROUP BY clause.

When you use a projection, you’re changing the result type of the query. We’ve been using entities as the result types, but with projections, we are specifying different columns to return, so we may not have an actual entity. First, we’ll create a top-level container object – SupplierResult – to hold the data from our projection, and tell the Criteria API how to construct it.

Note that this can’t be an inner class – because inner class’s construction rules are different, and if it’s an inner class, the Criteria API will not be able to find the constructor.

package chapter10;

class SupplierResult {
    String name;
    long count;


    public SupplierResult(String name, Long count) {
        this.name = name;
        this.count = count;
    }
}

Now let’s walk through our test.

@Test
public void testProjection() {
    doWithEntityManager((em) -> {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<SupplierResult> criteria = builder.createQuery(SupplierResult.class);
        Root<Supplier> root = criteria.from(Supplier.class);
        criteria.select(builder.construct(
                SupplierResult.class,
                root.get(Supplier_.name),
                builder.count(root.join(Supplier_.products))
        ))
                .groupBy(root.get(Supplier_.name))
                .distinct(true)
                .orderBy(builder.asc(root.get(Supplier_.name)));


        List<SupplierResult> supplierData = em.createQuery(criteria).getResultList();
        assertEquals(supplierData.get(0).count, 5);
        assertEquals(supplierData.get(0).name, "Hardware Are We");
        assertEquals(supplierData.get(1).count, 2);
        assertEquals(supplierData.get(1).name, "Hardware, Inc.");
    });
}

We are stating that our result will be a class, SupplierResult. Our query will start at the Supplier type (and that’s our Root reference). Our select() method looks a little odd, but here’s what it’s doing: the construct() method will construct an object as the result of every expression in our query. We supply the type to construct (“SupplierResult.class”) and then the arguments for the constructor – a String (the name of the Supplier) and then a count of all of the Product references for that Supplier.

We use groupBy() because of the aggregate query over Products. We use orderBy() because we want the results back in deterministic order for our assertions.

Projections can be amazingly powerful, of course; our alternative to this projection is walking through every Product and running the statistics ourselves. Even though gathering the counts and other possible values is very easily done in code (it’d just be Supplier.getProducts.size(), after all) that would still require transferring all of the Product data to the JVM, where the projection is able to return a much smaller amount of data, managed by the database.

Therefore, it’s not necessary to use projections, but they’re too useful to ignore for efficiency’s sake.

Should You Use the Criteria API?

The Criteria API is a nice way to programmatically build a valid query. It’s largely typesafe when using the metamodel aspect, and the fluent aspect of building a query lends itself to a construction process that we really can’t show here; one can imagine building a Criteria query bit by bit, adding refinements piece by piece for a very flexible API.

However, as you’ve probably also noticed, there’s a fairly steep learning curve,9 and the API can be very verbose.

So is it worth it? It’s hard to say, honestly, without a specific use case. It’s probably worthwhile to have in your toolbox just in case, but chances are that HQL and JPQL are going to be the first choices for retrieving data, because they’re the closest declarative model to the actual source of the data – SQL.

Summary

Some developers prefer the Criteria Query API, as it offers compile-time syntax checking and a way to build a query at runtime. However, it is verbose and can be arcane when expressing some query forms.

In the next chapter, we discuss the use of Hibernate filters to restrict the range of data against which queries are applied.

Footnotes

2 Astute readers will notice that Chapter 9 didn’t actually do much Session management at all; before the test methods ran, a Session was created and stored in an instance-level attribute. That’s not a very nice way to do Session management; this is much better. Plus, lambdas are fun.

3 Strictly speaking, you can build a metamodel yourself. However, it should be kept in sync with the entity classes themselves, and it’s certainly eligible for generation – and if we can get the computer to maintain the metamodel, we should.

4 A Predicate is a functional interface that has a single method – test() – that determines fitness for a match, so it serves the same purpose as our Expression<Boolean> would.

5 If the query expression were “%mOUse” – as in the next example – it wouldn’t match “Mouse” because the cases of the content do not match. Since user input tends to not conform to what database developers would like, it’s probably wise to sanitize this somewhat.

6 We are apparently feeling rather thrifty or we’re trying to throw money around, apparently.

7 When naïve pagination would not lead to atrocious performance, chances are the result set is small enough that you wouldn’t need pagination in the first place.

8 Thus, it’s about a unique result and not just a single result. You have to enforce uniqueness if it’s not already present. If we’d called setMaxResults(1) in the testUniqueResultWithException() test, no exception would have been thrown.

9 It is a steep learning curve; we’ve only scratched the surface of the Criteria API. There is a lot to the API, and it could easily fill a book all by itself.

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

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