Skip to main content

Implementing Query Search

The two most notable classes generated for a QuerySearch are the QuerySearch class in the [data layerLayerLogical separation of functionality within a single system/application, focused on code organization.] (/docs/reference-guides/ns-applicationApplicationThe overarching model container grouping all components, domain logic, and configurations for an enterprise solution./layersLayerLogical separation of functionality within a single system/application, focused on code organization./#logic-data-layersLayerLogical separation of functionality within a single system/application, focused on code organization.) and the matching QueryFilter class in the shared layer.

The purpose of the QueryFilter POJO class is to define fieldsFieldA property of a DataElement expanded to a database column and user interface input element. 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 a constraint that uses this value". To enable this, boxed types are used for primitives (e.g., 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.

note

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 fieldsFieldA property of a DataElement expanded to a database column and user interface input element. 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 and Integer valueMax. You would then define a constraint as "if both valueMin and valueMax 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 than valueMax 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.

Anti-Pattern Example

If you want to verify that an integer value is inside a range, the filter fieldsFieldA property of a DataElement expanded to a database column and user interface input element. could be defined as Integer valueBetweenCheckMin and Integer valueBetweenCheckMax. This would however imply that these fieldsFieldA property of a DataElement expanded to a database column and user interface input element. are specifically intended to drive a between constraint on that fieldFieldA property of a DataElement expanded to a database column and user interface input element., which is not the intended use of QuerySearch.

Example​

Here we add a constraint on the name of an element and its code fieldFieldA property of a DataElement expanded to a database column and user interface input element.. 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

}