Skip to main content

Custom Constraints

The QuerySearch expanders expand infrastructure for quite a few different onion constraints. In some cases, it may however be needed to create custom constraint types. This most commonly happens in two scenarios:

  • A type of constraint in JPQL statements is not supported or cannot be generated with existing constraints.
  • A recurring composite constraint can be reused in different QuerySearch classes.

Complete constraints

A fully featured constraint consists out of two parts:

  • A class that implements the QueryConstraint interface.
  • One or more methods in the OnionConstraintFactory class, that return an instance of the constraint class, based on supplied parameters.

Constraint class

It would be preferable for a class that implements QueryConstraint, to implement a specific child interface of QueryConstraint that indicates what kind of constraint it is. There are several of these child interfaces to choose from:

InterfaceDescription
UnaryQueryConstraintA constraint that translates to a JPQL statement that only requires a single parameter (or statement).
Example: NOT X
BinaryQueryConstraintA constraint that translates to a JPQL statement that only requires a two parameters (or statements).
Example: A = B
TernaryQueryConstraintA constraint that translates to a JPQL statement that only requires a three parameters (or statements).
Example: A BETWEEN B AND C
CompositeQueryConstraintA constraint that translates to a combination of multiple JPQL statements.
Example: A OR B OR ... OR N

Currently, all expanded classes are placed in the package <component.name>.query.builder.constraint and custom constraints should also be added there (in the ext folder). Constraint classes are package-private as they should only be instantiated by classes in the OnionConstraintFactory class.

caution

It would be prudent to choose a name such as MyNewAmazingCustomConstraint, in order to avoid potential collisions with new constraint classes that may be added to the expanders in the future.

Example

In this example we will look at the AndQueryConstraint class, which is already being expanded.

AndQueryConstraint.java
package myComponent.query.builder.constraint;

import myComponent.query.builder.ParameterCounter;
import myComponent.query.builder.QueryParameter;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class AndQueryConstraint implements CompositeQueryConstraint {

private final List<QueryConstraint> childConstraints = new ArrayList<>();

@Override
public void addConstraint(QueryConstraint constraint) {
childConstraints.add(constraint);
}

@Override
public String buildConstraint(ParameterCounter parameterCounter, List<QueryParameter> parameters) {
return childConstraints.stream()
.filter(QueryConstraint::isApplicable)
.map(constraint -> String.format("(%s)", constraint.buildConstraint(parameterCounter, parameters)))
.collect(Collectors.joining(" AND "));
}

@Override
public boolean isApplicable() {
return childConstraints.stream().anyMatch(QueryConstraint::isApplicable);
}

}

All constraints most implement two methods:

  • buildConstraint() which generates the JPQL statements.
  • isApplicable() which returns true if the constraint should be added to the query for the current evaluation.

In this instance, the constraint is applicable if at least one of its child constraints is applicable. The building of the JPQL statements is simply a concatenation of the JPQL statements of all child constraints with the AND operator as a separator. For obvious reasons every child statement is also wrapped in round braces.

Because this is a CompositeQueryConstraint, this constraint also has to implement the addConstraint() method, which registers child constraints.

Factory method

To instantiate a constraint class, one or more factory methods are needed in the OnionConstraintFactory. These methods should have a simple and clean interface to define the constraints based on some parameters. The goal is to make the constraints as readable as possible.

Example

As an example, we will look at a factory method for the between() constraint:

public static QueryConstraint between(String fieldName, QueryValueType valueType, Object leftValue, Object rightValue,
QueryConstraintOption... queryOptions) {
QueryConstraintOptionSet options = makeOptions(queryOptions);

return new BetweenQueryConstraint(options, makeBoundaryConstraintData(fieldName, valueType, leftValue, rightValue));
}

This method translates its parameters into the required input to create an instance of the BetweenQueryConstraint class. The parameters are defined in an order that should appear logical to a developer.

The method can be read as: a field fieldName should have a value of type valueType that is between() the values leftValue and rightValue.

As it is always a varargs argument, if additional options are needed, these are always the last argument.

Simple constraints

Simple constraints, which are often used to in relation to activation can be creating by adding only a factory method to the OnionConstraintFactory class. It is not advisable to do this for constraints with complex logic, but for simple constraints, this can save some work. The empty constraint exists to support the creation of such constraints when applicable conditions are involved.

info

A notable example of an existing simple constraint is the whenOrElse() constraint that is already expanded. Its sole function is to add one of two constraints based on a boolean condition to switch between them. This will be evaluated when the constraint is built upon executing a QuerySearch operation.

public static QueryConstraint whenOrElse(boolean condition, Supplier<QueryConstraint> constraint,
Supplier<QueryConstraint> elseConstraint) {
return condition ? constraint.get() : elseConstraint.get();
}

The empty() constraint is used to create an additional simple derived when() constraint, there the empty constraint is the return value when the boolean condition is not met.

public static QueryConstraint when(boolean condition, Supplier<QueryConstraint> constraint) {
return whenOrElse(condition, constraint, OnionQueryConstraintFactory::empty);
}