Skip to content

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) ​

java
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):

java
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:

java
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:

java
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 ​

java
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:

java
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.

Released under the MIT License. Made with ☕, 💡 and lots of Java love.