Getting started
Setting up your project
To get started, you have to add the SQL expanders as an expansion resource to your application project. This can be done either in the µRadiant, or directly in the expansion settings file.
µRadiant
- Open the model for your JEE application.
- Go the to settings tab.
- Add a new expansion resource with name
net.democritus:querysearch-expanders
and version3.3.1
.
Project files
- Open the expansion settings file for your application, which is often found in the
conf
folder in the root of your project's workspace with the nameexpansionSettings.xml
. - Add a child element to
expansionResources
as follows:<expansionResource name="net.democritus:querysearch-expanders" version="3.3.1"/>
Adding QuerySearch to an element
The option enableQuerySearch
can be used to add QuerySearch support to a DataElement.
This option is actually translated behind the scenes into a QuerySearch element, with the same name as that of the DataElement.
The expanded classes typically include the name of the QuerySearch object, such as <QuerySearch.name>QuerySearch
and <QuerySearch.name>QueryFilter
.
Why is the name of the parent DataElement not part of the expanded classes of the QuerySearch?
The parent element of a QuerySearch element is DataElement. To avoid collisions, it would make sense for the name of the parent element to be included in the name of generated classes, alongside the name of the QuerySearch element itself.
The reason for this not being the case is purely historical. Originally there was no QuerySearch metamodel and only one QuerySearch was expanded per DataElement.
Where today the name of the QuerySearch itself is used, in the past this was the name of the DataElement.
To remain backwards compatible, the enableQuerySearch
option now creates a QuerySearch element before expansion, which has the same name as the DataElement it is linked to.
A QueryFilter
class previously (in version 1.x) have been <DataElement.name>QueryFilter
and now <QuerySearch.name>QueryFilter
, which is a match with the name of the QuerySearch now being the same as that of its parent DataElement.
Using metamodel
Alternatively to using the enableQuerySearch
option, it is possible to directly make use of the QuerySearch metamodel elements.
More information can be found on the dedicated page for the metamodel.
Implementation
The two most notable classes generated for a QuerySearch are the QuerySearch
class in the data layer and the matching QueryFilter
class in the shared layer.
The purpose of the QueryFilter
POJO class, is to define fields that can be used as both enable conditions and inputs for constraints in a database query.
These filter values are translated to "if filter value is defined (not null), then add constraint that uses this value".
To enable this, boxed types are used for primitives (eg Integer
instead of int
), so the value null
can be used as an enable condition.
The actual implementation of the constraints is placed in the method QuerySearch.buildConstraints()
, which is passed the QueryFilter
object.
The constraints are implemented using the "onion constraints" methods to define them in a clean declarative manner.
The idea behind the design of QuerySearch is to be able to declaratively define combinable filters on the data in the database, that can be activated depending on filter criteria that are given. This means that typically you want to try to define the filter fields in terms of how it relates to a value in the database as a constraint parameter, rather than in terms of a specific constraint operation.
For example, if you want to verify that an integer value is inside a range, you could name them
Integer valueMin
andInteger valueMax
. You would then define a constraint as "if bothvalueMin
andvalueMax
are defined, add constraint database value is between those two values". To also perform open-ended checks, an additional constraint can be added that verifies that the value if smaller thanvalueMax
if only that one is given. The result is that the query will adapt depending on the given filtering information, rather than what constraint is requested.
If you want to verify that an integer value is inside a range, the filter fields could be defined as Integer valueBetweenCheckMin
and Integer valueBetweenCheckMax
.
This would however imply that these fields are specifically intended to drive a between constraint on that field, which is not the intended use of QuerySearch.
Example
Here we add a constraint on the name of an element and its code field.
Each constraint is only applied if the value is given and since they are added directly in the bottom and()
constraint, they are combined as a conjunction if both are given.
MyDataElementQueryFilter.java
public class MyDataElementQueryFilter implements MyDataElementElementQueryFilter {
// anchor:fields:start
// anchor:fields:end
// @anchor:fields:start
// @anchor:fields:end
// anchor:custom-fields:start
private String name;
private String code;
// anchor:custom-fields:end
// anchor:methods:start
// anchor:methods:end
// @anchor:methods:start
// @anchor:methods:end
// anchor:custom-methods:start
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
// anchor:custom-methods:end
}
MyDataElementQuerySearch.java
public class MyDataElementQuerySearch implements MyDataElementQuerySearchLocal<MyDataElementQueryFilter> {
private EntityManager entityManager;
// @anchor:fields:start
// @anchor:fields:end
// anchor:custom-fields:start
// anchor:custom-fields:end
public MyDataElementQuerySearch(EntityManager entityManager) {
this.entityManager = entityManager;
}
private void setPagingRestrictions(Paging paging, Query query) {
int rowsPerPage = paging.getRowsPerPage();
if (rowsPerPage <= 0) {
return;
}
int page = paging.getPage();
int offset = rowsPerPage * page; // 0-based page
query.setFirstResult(offset);
query.setMaxResults(rowsPerPage);
}
@SuppressWarnings("unchecked")
private SearchResult<MyDataElementData> fetchData(SearchDetails<MyDataElementQueryFilter> searchDetails, Query countQuery, Query dataQuery) {
List<Long> countList = countQuery.getResultList();
Long total = countList.get(0);
// @anchor:fetch-after-count:start
if (total == 0 || searchDetails.getPaging().getRowsPerPage() == 0) {
return SearchResult.success(Collections.emptyList(), total.intValue());
}
// @anchor:fetch-after-count:end
// anchor:custom-fetch-after-count:start
// anchor:custom-fetch-after-count:end
setPagingRestrictions(searchDetails.getPaging(), dataQuery);
List<MyDataElementData> resultData = (List<MyDataElementData>) dataQuery.getResultList();
// @anchor:fetch-after-queries:start
// @anchor:fetch-after-queries:end
// anchor:custom-fetch-after-queries:start
// anchor:custom-fetch-after-queries:end
return SearchResult.success(resultData, total.intValue());
}
public SearchResult<MyDataElementData> querySearch(ParameterContext<SearchDetails<MyDataElementQueryFilter>> searchParameter) {
SearchDetails<MyDataElementQueryFilter> searchDetails = searchParameter.getValue();
MyDataElementQueryFilter queryFilter = searchDetails.getDetails();
List<SortField> sortFields = searchDetails.getSortFields();
QueryBuilder queryBuilder = select("o")
.from("com.example.MyDataElement", "o")
.where(buildConstraints(searchParameter.construct(queryFilter)))
.orderBy(sortFields);
// @anchor:query-before-build:start
// @anchor:query-before-build:end
// anchor:custom-query-before-build:start
// anchor:custom-query-before-build:end
Query query = queryBuilder.buildQuery(entityManager), countQuery = queryBuilder.buildCountQuery(entityManager);
// @anchor:query-after-build:start
// @anchor:query-after-build:end
// anchor:custom-query-after-build:start
// anchor:custom-query-after-build:end
return fetchData(searchDetails, countQuery, query);
}
private QueryConstraint buildConstraints(ParameterContext<MyDataElementQueryFilter> filterParameter) {
MyDataElementQueryFilter filter = filterParameter.getValue();
QueryConstraint constraint = and(
// @anchor:constraints:start
// @anchor:constraints:end
// anchor:custom-constraints:start
when(filter.getName() != null, () ->
equal("o.name", filter.getName())),
when(filter.getCode() != null, () ->
equal("o.code", filter.getCode()))
// anchor:custom-constraints:end
);
// @anchor:after-constraints:start
// @anchor:after-constraints:end
// anchor:custom-after-constraints:start
// anchor:custom-after-constraints:end
return constraint;
}
// @anchor:methods:start
// @anchor:methods:end
// anchor:custom-methods:start
// anchor:custom-methods:end
}