Chapter 18. The Infinispan Query DSL

18.1. The Infinispan Query DSL

The Infinispan Query DSL provides an unified way of querying a cache. It can be used in Library mode for both indexed and indexless queries, as well as for Remote Querying (via the Hot Rod Java client). The Infinispan Query DSL allows queries without relying on Lucene native query API or Hibernate Search query API.

Indexless queries are only available with the Infinispan Query DSL for both the JBoss Data Grid remote and embedded mode. Indexless queries do not require a configured index (see Enabling Infinispan Query DSL-based Queries). The Hibernate Search/Lucene-based API cannot use indexless queries.

18.2. Creating Queries with Infinispan Query DSL

The new query API is located in the org.infinispan.query.dsl package. A query is created with the assistance of the QueryFactory instance, which is obtained using Search.getQueryFactory(). Each QueryFactory instance is bound to the one cache instance, and is a stateless and thread-safe object that can be used for creating multiple parallel queries.

The Infinispan Query DSL uses the following steps to perform a query.

  1. A query is created by invocating the from(Class entityType) method, which returns a QueryBuilder object that is responsible for creating queries for the specified entity class from the given cache.
  2. The QueryBuilder accumulates search criteria and configuration specified through invoking its DSL methods, and is used to build a Query object by invoking the QueryBuilder.build() method, which completes the construction. The QueryBuilder object cannot be used for constructing multiple queries at the same time except for nested queries, however it can be reused afterwards.
  3. Invoke the list() method of the Query object to execute the query and fetch the results. Once executed, the Query object is not reusable. If new results must be fetched, a new instance must be obtained by calling QueryBuilder.build().
Important

A query targets a single entity type and is evaluated over the contents of a single cache. Running a query over multiple caches, or creating queries targeting several entity types is not supported.

18.3. Enabling Infinispan Query DSL-based Queries

In library mode, running Infinispan Query DSL-based queries is almost identical to running Lucene-based API queries. Prerequisites are:

  • All libraries required for Infinispan Query on the classpath. Refer to the Administration and Configuration Guide for details.
  • Indexing enabled and configured for caches (optional). Refer to the Administration and Configuration Guide for details.
  • Annotated POJO cache values (optional). If indexing is not enabled, POJO annotations are also not required and are ignored if set. If indexing is not enabled, all fields that follow JavaBeans conventions are searchable instead of only the fields with Hibernate Search annotations.

18.4. Running Infinispan Query DSL-based Queries

Once Infinispan Query DSL-based queries have been enabled, obtain a QueryFactory from the Search in order to run a DSL-based query.

Obtain a QueryFactory for a Cache

In Library mode, obtain a QueryFactory as follows:

QueryFactory qf = org.infinispan.query.Search.getQueryFactory(cache)

Constructing a DSL-based Query

import org.infinispan.query.Search;
import org.infinispan.query.dsl.QueryFactory;
import org.infinispan.query.dsl.Query;

QueryFactory qf = Search.getQueryFactory(cache);
Query q = qf.from(User.class)
    .having("name").eq("John")
    .build();
List list = q.list();
assertEquals(1, list.size());
assertEquals("John", list.get(0).getName());
assertEquals("Doe", list.get(0).getSurname());

When using Remote Querying in Remote Client-Server mode, the Search object resides in package org.infinispan.client.hotrod. See the example in Performing Remote Queries via the Hot Rod Java Client for details.

It is also possible to combine multiple conditions with boolean operators, including sub-conditions. For example:

Combining Multiple Conditions

Query q = qf.from(User.class)
    .having("name").eq("John")
    .and().having("surname").eq("Doe")
    .and().not(qf.having("address.street").like("%Tanzania%")
    .or().having("address.postCode").in("TZ13", "TZ22"))
    .build();

This query API simplifies the way queries are written by not exposing the user to the low level details of constructing Lucene query objects. It also has the benefit of being available to remote Hot Rod clients.

The following example shows how to write a query for the Book entity.

Querying the Book Entity

import org.infinispan.query.Search;
import org.infinispan.query.dsl.*;

// get the DSL query factory, to be used for constructing the Query object:
QueryFactory qf = Search.getQueryFactory(cache);
// create a query for all the books that have a title which contains the word "engine":
Query query = qf.from(Book.class)
    .having("title").like("%engine%")
    .build();
// get the results
List<Book> list = query.list();

18.5. Projection Queries

In many cases returning the full domain object is unnecessary, and only a small subset of attributes are desired by the application. Projection Queries allow a specific subset of attributes (or attribute paths) to be returned. If a projection query is used then the Query.list() will not return the whole domain entity (List<Object>), but instead will return a List<Object[]>, with each entry in the array corresponding to a projected attribute.

To define a projection query use the select(…​) method when building the query, as seen in the following example:

Retrieving title and publication year

// Match all books that have the word "engine" in their title or description
// and return only their title and publication year.
Query query = queryFactory.from(Book.class)
    .select(Expression.property("title"), Expression.property("publicationYear"))
    .having("title").like("%engine%")
    .or().having("description").like("%engine%")
    .build();

// results.get(0)[0] contains the first matching entry's title
// results.get(0)[1] contains the first matching entry's publication year
List<Object[]> results = query.list();

18.6. Grouping and Aggregation Operations

The Infinispan Query DSL has the ability to group query results according to a set of grouping fields and construct aggregations of the results from each group by applying an aggregation function to the set of values. Grouping and aggregation can only be used with projection queries.

The set of grouping fields is specified by calling the method groupBy(field) multiple times. The order of grouping fields is not relevant.

All non-grouping fields selected in the projection must be aggregated using one of the grouping functions described below.

Grouping Books by author and counting them

Query query = queryFactory.from(Book.class)
    .select(Expression.property("author"), Expression.count("title"))
    .having("title").like("%engine%")
    .groupBy("author")
    .build();

// results.get(0)[0] will contain the first matching entry's author
// results.get(0)[1] will contain the first matching entry's title
List<Object[]> results = query.list();

Aggregation Operations

The following aggregation operations may be performed on a given field:

  • avg() - Computes the average of a set of Numbers, represented as a Double. If there are no non-null values the result is null instead.
  • count() - Returns the number of non-null rows as a Long. If there are no non-null values the result is 0 instead.
  • max() - Returns the greatest value found in the specified field, with a return type equal to the field in which it was applied. If there are no non-null values the result is null instead.

    Note

    Values in the given field must be of type Comparable, otherwise an IllegalStateException will be thrown.

  • min() - Returns the smallest value found in the specified field, with a return type equal to the field in which it was applied. If there are no non-null values the result is null instead.

    Note

    Values in the given field must be of type Comparable, otherwise an IllegalStateException will be thrown.

  • sum() - Computes and returns the sum of a set of Numbers, with a return type dependent on the indicated field’s type. If there are no non-null values the result is null instead.

    The following table indicates the return type based on the specified field.

    Table 18.1. Sum Return Type

    Field TypeReturn Type

    Integral (other than BigInteger)

    Long

    Floating Point

    Double

    BigInteger

    BigInteger

    BigDecimal

    BigDecimal

Projection Query Special Cases

The following cases items describe special use cases with projection queries:

  • A projection query in which all selected fields are aggregated and none is used for grouping is legal. In this case the aggregations will be computed globally instead of being computed per each group.
  • A grouping field can be used in an aggregation. This is a degenerated case in which the aggregation will be computed over a single data point, the value belonging to current group.
  • A query that selects only grouping fields but no aggregation fields is legal.

Evaluation of grouping and aggregation queries

Aggregation queries can include filtering conditions, like usual queries, which may be optionally performed before and after the grouping operation.

All filter conditions specified before invoking the groupBy method will be applied directly to the cache entries before the grouping operation is performed. These filter conditions may refer to any properties of the queried entity type, and are meant to restrict the data set that is going to be later used for grouping.

All filter conditions specified after invoking the groupBy method will be applied to the projection that results from the grouping operation. These filter conditions can either reference any of the fields specified by groupBy or aggregated fields. Referencing aggregated fields that are not specified in the select clause is allowed; however, referencing non-aggregated and non-grouping fields is forbidden. Filtering in this phase will reduce the amount of groups based on their properties.

Ordering may also be specified similar to usual queries. The ordering operation is performed after the grouping operation and can reference any fields that are allowed for post-grouping filtering as described earlier.

18.7. Using Named Parameters

Instead of creating a new query for every request it is possible to include parameters in the query which may be replaced with each execution. This allows a query to be defined a single time and adjust variables in the query as needed.

Parameters are defined when the query is created by using the Expression.param(…​) operator on the right hand side of any comparison operator from the having(…​):

Defining Named Parameters

import org.infinispan.query.Search;
import org.infinispan.query.dsl.*;
[...]

QueryFactory queryFactory = Search.getQueryFactory(cache);
// Defining a query to search for various authors
Query query = queryFactory.from(Book.class)
    .select("title")
    .having("author").eq(Expression.param("authorName"))
    .build()
[...]

Setting the values of Named Parameters

By default all declared parameters are null, and all defined parameters must be updated to non-null values before the query must be executed. Once the parameters have been declared they may then be updated by invoking either setParameter(parameterName, value) or setParameters(parameterMap) on the query with the new values; in addition, the query does not need to be rebuilt. It may be executed again after the new parameters have been defined.

Updating Parameters Individually

[...]
query.setParameter("authorName","Smith");

// Rerun the query and update the results
resultList = query.list();
[...]

Updating Parameters as a Map

[...]
parameterMap.put("authorName","Smith");

query.setParameters(parameterMap);

// Rerun the query and update the results
resultList = query.list();
[...]