Extending EQL
Custom value conversion
When converting an EQL query all value arguments are first converted to an EQType
representation before being converted into their respective Java type.
Actual type conversion is then done via the Spring ConversionService
.
To create a custom conversion you can simply register a Converter
that converts from the relevant EQType
to the property type.
The following table shows how EQL arguments will be converted to their respective EQType
:
Argument value | EQType |
---|---|
name |
|
'name' |
|
(name, 'name') |
- - |
users(name, 'name') |
[arguments] - - |
By default EntityModule registers id-based lookups for all its registered entities.
So supposing you have an entity User
with id 1 and you want to query on a property creator of type User
, the following query would work: creator = 1
.
When building the EntityQuery
the value 1 would be used as the id to find the User
instance, and the latter would be used as the argument for the final query.
If we want to replace the custom behavior and allow the user to be specified by username instead, we could easily register a custom converter.
public class EQValueToUserConverter implements Converter<EQValue, User>
{
...
@Override
public User convert( EQValue source ) {
return userRepository.findByUsername( source.getValue() );
}
}
...
converterRegistry.addConverter( new EQValueToUserConverter(...) );
This would allow us to execute the queries like creator = john
or creator in (john, jane)
.
Any type-specific converter will take precedence over the defaults.
NOTE The example above would only work if the username can never contain any whitespace. If it can, then we would have to specify it as a String instead and write a converter for
EQString
instead ofEQValue
.
Adding custom functions
An EQL function is represented by a unique name and can optionally take a number of arguments for its execution.
Adding custom functions is as easy as simply defining a @Component
that implements the EntityQueryFunctionHandler
interface.
All components of this type will be detected and checked when executing an EQL query.
The handler will be called with the required contextual data for the return type requested.
If you want to use a function to compare a property that has a Date
type, your function should return a Date
instance as well.
A single handler can support multiple functions and requested return types.
Simple EntityQuery function that always returns the String hello
/**
* Simple EntityQuery function that always returns the String 'hello'.
* Example eql: name = hello() or name in (hello(), 'goodbye')
*/
@Component
public class HelloFunction implements EntityQueryFunctionHandler
{
@Override
public boolean accepts( String functionName, TypeDescriptor desiredType ) {
return "hello".equals( functionName );
}
@Override
public Object apply( String functionName,
EQType[] arguments,
TypeDescriptor desiredType,
EQTypeConverter argumentConverter ) {
return "hello";
}
}
Custom EQL translation with registered fields
You can register an EntityQueryConditionTranslator
attribute on any entity property.
If a translator instance is present, it will be called during the parsing phase of an EQL statement into an EntityQuery
.
Ensuring a field search is always case insensitive
configuration
.withType( Group.class )
.properties(
props -> props.property( "name" ).attribute( EntityQueryConditionTranslator.class, EntityQueryConditionTranslator.ignoreCase() )
)
Define a search text property that actually searches on other fields
configuration.withType( Note.class )
.properties( props -> props.property( "text" )
.propertyType( String.class )
.attribute( EntityQueryConditionTranslator.class,
EntityQueryConditionTranslator.expandingOr( "name", "content" ) )
.hidden( true )
)
An The actual parsing of the query and execution of the query uses the property registry of the |
Custom EQL translation with non registered fields
In the example above we expanded (replaced and added) the text
field into name
and content
.
These three fields are all present on the entity Group
.
In some cases, it might be useful to expand a field into fields that are not present on the entity.
A use case would be if you are querying a backend and want to expand a text
field into text
and sub_text
, where sub_text
is not present on the entity itself.
You can achieve this by setting an EntityQuery
or EntityQueryCondition
as translated
.
When the translated flag is set, the DefaultEntityQueryTranslator
will not attempt to lookup fields from the PropertyRegistry
anymore.
It will then send the EntityQuery
as-is to the backend.
Define a search text property that actually searches on another field, where sub_text
is not a registered property
configuration.withType( Article.class )
.properties( props -> props.property( "text" )
.attribute( EntityQueryConditionTranslator.class, new EntityQueryConditionTranslator() {
@Override
public EntityQueryExpression translate( EntityQueryCondition condition ) {
return new EntityQueryCondition( "sub_text", condition.getOperand(), condition.getFirstArgument() ).setTranslated( true );
}
}
) );