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.
-
A query is created by invocating the
from(Class entityType)
method, which returns aQueryBuilder
object that is responsible for creating queries for the specified entity class from the given cache. -
The
QueryBuilder
accumulates search criteria and configuration specified through invoking its DSL methods, and is used to build aQuery
object by invoking theQueryBuilder.build()
method, which completes the construction. TheQueryBuilder
object cannot be used for constructing multiple queries at the same time except for nested queries, however it can be reused afterwards. -
Invoke the
list()
method of theQuery
object to execute the query and fetch the results. Once executed, theQuery
object is not reusable. If new results must be fetched, a new instance must be obtained by callingQueryBuilder.build()
.
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 ofNumber
s, represented as aDouble
. If there are no non-null values the result is null instead. -
count()
- Returns the number of non-null rows as aLong
. 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.NoteValues in the given field must be of type
Comparable
, otherwise anIllegalStateException
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.NoteValues in the given field must be of type
Comparable
, otherwise anIllegalStateException
will be thrown.sum()
- Computes and returns the sum of a set ofNumber
s, 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 Type Return 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(); [...]