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:
Interface | Description |
---|---|
UnaryQueryConstraint | A constraint that translates to a JPQL statement that only requires a single parameter (or statement). Example: NOT X |
BinaryQueryConstraint | A constraint that translates to a JPQL statement that only requires a two parameters (or statements). Example: A = B |
TernaryQueryConstraint | A constraint that translates to a JPQL statement that only requires a three parameters (or statements). Example: A BETWEEN B AND C |
CompositeQueryConstraint | A 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.
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.
In this example we will look at the AndQueryConstraint
class, which is already being expanded.
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.
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 typevalueType
that isbetween()
the valuesleftValue
andrightValue
.
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.
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);
}