SELECT {alias} FROM {getPersistentClass} {alias} WHERE {filter1} AND {filter2}...
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.
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.
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)".
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.
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}...
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
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.
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"); // relative ordering of sort fields this.sortCurrentAvailability = sortCurrentAvailability; // set the ordering }
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).
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(); // call various setters on criteria CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria); generator.setAuthorizationResourceFragment(...); // do not return what users shouldn't see EntityManager em = ...; // injected in the SLSB context 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();
TODO
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 ...
SubjectCriteria criteria = new SubjectCriteria(); criteria.addFilterFirstName("joe"); // filter by subjects whose first name is "joe" criteria.fetchRoles(true); // fetch all role entities for each returned subject criteria.addSortName(PageOrdering.ASC); // order by username ascending CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria); System.out.println(generator.getQueryString(false)); // get data query string System.out.println(generator.getQueryString(true)); // get count query string
ResourceCriteria criteria = new ResourceCriteria(); criteria.addFilterResourceCategory(ResourceCategory.SERVER); criteria.addFilterName("marques"); criteria.fetchAgent(true); criteria.addSortResourceTypeName(PageOrdering.ASC); criteria.setCaseSensitive(true); // all string filters will require an exact match criteria.setFiltersOptional(true); // return category or name matches CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria); System.out.println(generator.getQueryString(false)); // get data query string System.out.println(generator.getQueryString(true)); // get count query string
AlertCriteria criteria = new AlertCriteria(); criteria.addFilterStartTime(System.currentTimeMillis() - 24*60*60*1000); // last 24 hrs criteria.addFilterResourceIds(1, 2, 3); criteria.addSortPriority(PageOrdering.DESC); // sort by priority first, HIGH->LOW criteria.addSortCtime(PageOrdering.DESC); // then by ctime, most->least recent criteria.setPaging(0, 100); // only the last 20 alerts // setAuthorizationResourceFragments tells the generator how to join to resources // to filter results down to what the user is authorized to see only CriteriaQueryGenerator generator = new CriteriaQueryGenerator(criteria); generator.setAuthorizationResourceFragment(AuthorizationTokenType.RESOURCE, "definition.resource", 1); System.out.println(generator.getQueryString(false)); // get data query string System.out.println(generator.getQueryString(true)); // get count query string