Overview
The Criteria API in RHQ is somewhat similar in principal to the Criteria API in Hibernate. They both provide an object-oriented approach to building queries. However, whereas Hibernate's solution must remain generic so that it can be used against any object model, RHQ's solution can and is very specific to its domain model.
Core Functionality
Pagination
There are two methods to manipulate the pagination.
1) criteria.setPaging(int pageNumber, int pageSize)
This strategy is generally used when you are a remote client talking over web services or via the CLI.
2) criteria.setPageControl(PageControl pc)
This strategy is used by the RHQ GUI. There are mechanisms to extract the pagination and sorting information from tabular displays and create PageControl objects. These objects can then be set directly on the Criteria object, which means that any calls to setPaging(pageNumber, pageSize) and addSortField(fieldName) will be ignored.
Filter Semantics
To simplify the Criteria API, it only supports two methods for result filtering - conjunctive (AND) or disjunctive (OR). In other words, all of the filters you set on each Criteria object must either be AND'ed together or OR'ed with one another - you can not mix and match these styles. The AND-style is the default, but you can switch to the OR style by calling "criteria.setFiltersOptional(true)".
Comparison Operators
All data types except string (Boolean, Long, Integer, even Enum) will use exact matching. Strings, on the other hand, have two ways their matching algorithms can be modified:
1) Strict
By default, strings will be matched using 'like' comparison. Calling "criteria.setStrict(true)" will turn it into an equal comparison.
2) Case-sensitivity
By defaults, strings will be matched insensitively. Calling "criteria.setCaseSensitive(true)" will turn it into a case-sensitive match.
Projection
In RHQ, each Criteria object corresponds to the entity returned by "criteria.getPersistentClass()". Thus, when you use that Criteria object to search, it will return a list of those entities.
However, sometimes you only need to return a fraction of that data...and sometimes you need to augment your entities with additional data. In both of these cases, the CriteriaQueryGenerator allows you to set a new select expression list. By default, the generated query will have the form:
SELECT {alias}
FROM {getPersistentClass} {alias}
WHERE {filter1} AND {filter2}...
But by setting the projection, you can alter what data gets returned by the generated query:
SELECT {projection}
FROM {getPersistentClass} {alias}
WHERE {filter1} AND {filter2}...
Extensible Functionality
Filtering
By default, the private fields with the naming convention filterXXX map to the XXX field on the corresponding entity returned from getPersistentClass on the Criteria. However, you can have any number of "virtual fields" which don't implicitly map to any real field on the entity. In cases like this, the CriteriaQueryGenerator doesn't know how to generate the JPQL fragment if that field is set with data, so you need to explicitly provide an 'override' for it. The generated query will have the form:
SELECT {alias | projection}
FROM {getPersistentClass} {alias}
WHERE {filter1} AND {filter2}...
This 'override' fragment therefore needs to be a valid conditional term in JPQL, with one caveat. Instead of using the style ":colonPrefixedArgument" to denote where the filter value should be inserted, all you have to use is '?'.
Take, for instance, AlertCriteria. If I wanted to be able to find all alerts by type then I would:
1) add "private String filterResourceTypeName;" to the field declarations
2) expose a setter method for it
3) add "filterOverrides.put("resourceTypeName", "alertDefinition.resource.resourceType.name like ?");" to the constructor
Notice how if we map "filterResourceTypeName" from step (1) back to the "filterXXX" convention explained above, then the underlying field name (in this case a virtual field) becomes "resourceTypeName". Thus, "resourceTypeName" becomes the key into the map in step (3) that you're supplying the JPQL override fragment for.
There's virtually no limit to what you can do with overrides because you can use anything that is a valid conditional expression including:
- between expressions
- collection member expressions (entityExpression [not] member [of] collectionValueExpression)
- comparison expressions
- empty collection comparison expressions
- exists expressions
- in-clause expressions
- like expressions
- null comparison expressions
Fetching
The RHQ domain model generally (but not always) marks entity relationships as lazy. This allows for queries to be efficient and only return the minimal amount of data necessary by default. However, there are instances where you want that additional data:
- you might want all the members of some group
- you might want all the configuration history for some resource
- you might want all the individual results for some compatible group operation
Fetching is the way you can return additional data with an Criteria-based query. The framework will make sure to load and return that entity-related data, for each element in the result set.
If, when search for roles, I wanted to be able to fetch all the subject under each role I would do:
1) add "private boolean fetchSubjects;" to the field declarations
2) expose a setter method for it
Currently, there is no way to add fetch methods for data that isn't directly associated with the entity represented by the Criteria object.
Sorting
By default, the private fields with the naming convention sortXXX map to the XXX field on the corresponding entity returned from getPersistentClass on the Criteria. However, you can have any number of "virtual fields" which don't implicitly map to any real field on the entity. In cases like this, the CriteriaQueryGenerator doesn't know how to generate the JPQL fragment if that field is set with data, so you need to explicitly provide an 'override' for it. The generated query will have the form:
SELECT {alias | projection}
FROM {getPersistentClass} {alias}
...
ORDER BY {order1 ASC|DESC}, {order2 ASC|DESC}
This 'override' fragment therefore needs to be a valid path expression in JPQL, with one caveat.
Take, for instance, ResourceCriteria. If I wanted to be able to sort resources by their current availability then I would:
1) add "private PageOrdering sortCurrentAvailability;" to the field declarations
2) expose a setter method for it (see below)
3) add "sortOverrides.put("currentAvailability", "currentAvailability.availabilityType");" to the constructor
public void addSortCurrentAvailability(PageOrdering sortCurrentAvailability) {
addSortField("currentAvailability"); this.sortCurrentAvailability = sortCurrentAvailability; }
Notice how if we map "sortCurrentAvailability" from step (1) back to the "sortXXX" convention explained above, then the underlying field name (in this case a virtual field) becomes "currentAvailability". Thus, "currentAvailability" becomes the key into the map in step (3) that you're supplying the JPQL override fragment for.
The actual ordering information for this virtual field - ascending or descending - is taken care of by the value of the PageOrdering object passed to the method in (2).
Query Generation
The abstract Criteria class is only half of the dynamic query equation - it holds all the data but, by itself, does nothing with it. The other half concerns how the information set on the Criteria objects is leverage to generate the effective JPQL statement. The general usage is as follows:
SomeEntityCriteria criteria = SomeEntityCriteria();
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria);
generator.setAuthorizationResourceFragment(...);
EntityManager em = ...; Query query = generator.getQuery(em);
Query countQuery = generator.getCountQuery(em);
long count = (Long) countQuery.getSingleResult();
List<SomeEntity> results = query.getResultList();
PageList<SomeEntity> results = null;
results = return new PageList<SomeEntity>(results, (int) count, generator.getPageControl());
return results;
However, nearly everything you see above is boilerplate. In other words, nearly the exact same lines will be repeated over and over again each time you need to create a generator to build the query for the data contained in some Criteria object. As a result, we've created a class that simplifies the use:
SomeEntityCriteria criteria = new SomeEntityCriteria();
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria);
if (authorizationManager.isInventoryManager(subject) == false) {
generator.setAuthorizationResourceFragment(
CriteriaQueryGenerator.AuthorizationTokenType.RESOURCE,
subject.getId());
}
CriteriaQueryRunner<SomeEntity> queryRunner =
new CriteriaQueryRunner(criteria, generator, entityManager);
return queryRunner.execute();
Authorization
TODO
Data Query / Count Query
The paginated displays used in the RHQ user interface require the size of the entire data set to be known, even though only one page of data will be displayed at a time. This is required to either:
1) conditionally render pagination controls - whether forward/backward should be enabled, which page number you're on, the size of the visible page
2) for scrollable tables, the relative size and placement of the scrollbar
To support this, the CriteriaQueryGenerator has two generation strategies encompassed in a single method "generator.getQueryString(boolean countQuery)".
If "false" is passed, you get the paginated, sorted data results:
SELECT {alias | projection}
FROM {getPersistentClass} {alias}
LEFT JOIN FETCH ...
WHERE ...
ORDER BY ...
If "true" is passed, you get a version of the query that gets the size of the entire result set:
SELECT COUNT({alias})
FROM {getPersistentClass} {alias}
WHERE ...
Usage
Searching for subjects
SubjectCriteria criteria = new SubjectCriteria();
criteria.addFilterFirstName("joe"); criteria.fetchRoles(true); criteria.addSortName(PageOrdering.ASC);
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria);
System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true));
Searching for resources
ResourceCriteria criteria = new ResourceCriteria();
criteria.addFilterResourceCategory(ResourceCategory.SERVER);
criteria.addFilterName("marques");
criteria.fetchAgent(true);
criteria.addSortResourceTypeName(PageOrdering.ASC);
criteria.setCaseSensitive(true); criteria.setFiltersOptional(true);
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria);
System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true));
Searching for alerts
AlertCriteria criteria = new AlertCriteria();
criteria.addFilterStartTime(System.currentTimeMillis() - 24*60*60*1000); criteria.addFilterResourceIds(1, 2, 3);
criteria.addSortPriority(PageOrdering.DESC); criteria.addSortCtime(PageOrdering.DESC); criteria.setPaging(0, 100);
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria);
generator.setAuthorizationResourceFragment(AuthorizationTokenType.RESOURCE, "definition.resource", 1);
System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true));
CLI Examples
Criteria Search using the CLI