Flexible Filtering in LibEntity
LibEntity's filter system lets you define flexible, type-safe filters for your entities—independent of any database technology. You can use these filters to build queries for SQL, NoSQL, in-memory collections, REST APIs, or any other persistence mechanism.
Why Use Filter Definitions?
- Separation of Concerns: Filters are plain Java objects (POJOs), decoupled from persistence details.
- Reusability: The same filter can be translated into SQL, MongoDB, or even in-memory Java streams.
- Type Safety: Compile-time checks prevent common mistakes.
- Extensibility: You can add custom comparators or logic easily.
1. Define a Filter Class (No jOOQ Required)
public class UserFilter {
public Integer age;
public String name;
public List<String> roles;
public RangeFilter<Integer> ageRange;
}
2. Define a FilterDefinition
This maps filter fields to supported comparators (EQ, GT, IN, etc):
import com.libentity.core.filter.FilterDefinition;
import com.libentity.core.filter.FieldFilterType;
FilterDefinition<UserFilter> definition = new FilterDefinition<>(
"UserFilter",
UserFilter.class,
Map.of(
"age", Set.of(FieldFilterType.EQ, FieldFilterType.GT, FieldFilterType.LT),
"name", Set.of(FieldFilterType.EQ),
"roles", Set.of(FieldFilterType.IN),
"ageRange", Set.of(FieldFilterType.GT, FieldFilterType.LT, FieldFilterType.EQ)
)
);
3. Use the Filter in Any Backend
You can now translate the filter to any query language. For example, to filter a Java list:
public List<User> filterUsers(List<User> users, UserFilter filter, FilterDefinition<UserFilter> def) {
return users.stream()
.filter(user -> filter.age == null || user.getAge().equals(filter.age))
.filter(user -> filter.name == null || user.getName().equals(filter.name))
.filter(user -> filter.roles == null || filter.roles.contains(user.getRole()))
// Add more conditions as needed
.toList();
}
You could also implement translation to MongoDB queries, REST API parameters, etc.
4. jOOQ Example (Manual Translation)
If you want to use your filter with jOOQ, you can manually map fields:
import org.jooq.Field;
import org.jooq.Condition;
import org.jooq.impl.DSL;
Field<Integer> AGE_FIELD = DSL.field("age", Integer.class);
Field<String> NAME_FIELD = DSL.field("name", String.class);
Map<String, Field<?>> fieldMapping = Map.of(
"age", AGE_FIELD,
"name", NAME_FIELD
);
UserFilter filter = new UserFilter();
filter.age = 30;
Condition condition = DSL.trueCondition();
if (filter.age != null) {
condition = condition.and(AGE_FIELD.eq(filter.age));
}
if (filter.name != null) {
condition = condition.and(NAME_FIELD.eq(filter.name));
}
// Use this condition in your jOOQ query
5. Advanced: Automatic jOOQ Integration
If you want to avoid manual mapping, use the jooq-support
module and annotation processor. This will generate meta-classes and helper methods for you (see the jooq-support docs for details).
6. Virtual Fields
Sometimes you want to filter by a value that doesn't directly map to a database column, or that depends on the current user or context. These are called virtual fields.
A virtual field lets you expose a filter property (e.g., submittedByMe
) that is translated into a query condition at runtime, using custom logic.
Example: Virtual Field in a Filter
public class InvoiceFilter {
// ... other filter fields ...
// This field is not a direct DB column
private Boolean submittedByMe;
}
How to Map Virtual Fields
You provide a custom mapper (e.g., for jOOQ) that translates the virtual field into a query condition. For example, to map submittedByMe
to a condition on the current user's ID:
public class InvoiceFilterVirtualMapperFactoryImpl implements InvoiceFilterJooqMetaVirtualMapperFactory {
private final String userId;
public InvoiceFilterVirtualMapperFactoryImpl(String userId) {
this.userId = userId;
}
@Override
public VirtualConditionMapper<InvoiceFilter> getSubmittedByMeMapper(List<Comparator> comparators) {
if (comparators.contains(Comparator.EQ)) {
return f -> INVOICE.EMPLOYEE_ID.eq(userId);
}
return f -> null;
}
}
This lets you write filters like { submittedByMe: true }
and have them automatically translated into employee_id = :userId
in your query.
Why Virtual Fields?
- Personalized Queries: e.g., "only my data"
- Derived or Computed Values: e.g., "overdue" (due date < today)
- Multi-table/Join Logic: e.g., filter by a property in a related entity
How to Use
- Define the virtual field in your filter class
- Register a custom mapper that translates the field to a query condition
- Use as normal in your filter logic—works for jOOQ, in-memory, or any backend
Summary
- Filters are plain Java objects—not tied to any database.
- FilterDefinition describes what fields and comparators are supported.
- You control how filters are translated: SQL, NoSQL, REST, or in-memory.
- jOOQ integration is optional—you can use filters anywhere!
For more, see the examples and the jooq-support module.