Functional filtering library for POJOs and SQL queries.
Defines the base abstraction for generic, composable and type-safe filtering operations. This operations can be used to filter POJOs/value objects, or to manage JDBC filters (WHERE clauses and parameters binding).
This library has been used in production for some time, however the API should stille be considered beta, and may be subject to change.
Right now, we have a small number of primitives and functions that rely on Functional Java for the most generic ones (callables, predicates, etc).
- New
SqlFilters#not()combinator.
- Added identity
SqlFilter:IdentitySqlFilter. - Fixed
SqlFilterscombinators (and, or) to work with both empty and one element input filters lists. - Added constat value
Filter:NonFilter. - Fixed
Filterscombinators (and, or) to work with both empty and one element input filters lists.
- Made combinators in
SqlFiltersparametric onSqlFilterimplementations.
- Moved to Functional Java from TotallyLazy, all the library revolved around functional constructs so this is a pretty breaking change.
- Added predicate
WhereClause#isEmpty(). - Exposed as public API
SqlFilters#bindParamsId(). - Constructors of
WhereClauseandParamIndexis now private, the static factory methods must be used instead. - Updated and wrote additional Javadoc comments.
- Added
Filters#or()composing operator. - Refactored
SqlFiltersto expose generic#and()and#or()operators asSqlFiltercombinators. - Renamed method:
SqlFilter#bindParameter()->SqlFilter#bindParameters().
- Renamed
SqlFilteringtoSqlFilters.
Filters#compose()has been renamed toFilters#and().- Created
WhereClause,BindParamsF,ParamIndextypes and used inSqlFilter. - Refactoring of
SqlFilter#bindParams(), which now just return a functionPair<ParamIndex, PreparedStatement> -> Pair<ParamIndex, PreparedStatement>that can be composed easily. - Created
SqlFilteringwith a couple of utility functions to composeSqlFilters.
Filter<T>is now both aPredicate<T>and aCallable1<T, Boolean>.- Implemented an AND composition of
Filter<T>inFilters.compose(Filter<T>... filters). - Simplified
Filtersfunctions implementation by using the new#compose()expression.
- Added some settings and metadata in the POM file.
- Implemented JDBC-level filtering (WHERE clause generation and values binding).
First release with a first formulation of the Filter concept and basic mechanics.
Conceptually, filtering some data can be seen as a predicate function on a value of some generic type:
filter :: a -> BoolHowever, defining filters this way means that every possible predicate from an implementation perspective is a totally different function. For example, if we have a Person type and we want to filter those values by age, matching if a person age is 19 or 20 has to be done with two different ad-hoc functions.
Clearly we can do better and use parametric predicates, obtaining a predicate by partial application (and even better with currying):
filterByAge :: Int -> Person -> Bool
filterByAgeRange :: Int -> Int -> Person -> BoolIn a Java context, we can model a filter as a partially applied function:
- Every filter is a statically typed class of objects that implements a filter function: it takes a value of a certain type and returns a boolean.
- A filter can be made parametric by requiring parameters at construction time via its constructor. This way, in some sense we are partially applying a filtering function (using the constructor parameters).
- When we have a
Filterinstance (which is just a function), we can apply it to match values of the required type. Actually,Filter<T>is indeed aF<T, Boolean>.
This way, we can write generic and type-safe filtering operations by composing filters. Both AND and OR combinators are implemented.
A filter in a JDBC context can be viewed as a pair of related operations:
- WHERE clause string generation
- Parameters binding
siftj defined an interface that specifies this contract: SqlFilter. Being an interface, there are
a couple of considerations to make:
- Every object can be a
SqlFilter, even aFilterimplementation. In the latter, both POJOs and JDBC filtering can be implemented in the same class (this is not to say that it's the right thing to do or the suggested way to use this interface). - There is no prescription on how a
SqlFiltertype has to be instantiated: we cannot possibly predict all the use cases (JOINs w/ multiple table references, columns renames, etc). So you have a lot of flexibility (and responsibility) to correctly design a viable, clean and maintainable querying strategy.
Some examples can be seen in the tests, but the gist is as follows.
Define a POJO/value object:
public class Person {
// [...]
public String getName() { /* ... */ }
public String getSurname() { /* ... */ }
public int getAge() { /* ... */ }
public Sex getSex() { /* ... */ }
}Define some filters for this type, by extending the Filter base class:
public class AgeFilter implements Filter<Person> {
private final int age;
private AgeFilter(int age) {
this.age = age;
}
public static AgeFilter ageFilter(int value) {
return new AgeFilter(value);
}
@Override
public Boolean f(Person p) {
return p.getAge() == age;
}
}Use all the filters defined this way to filter Person values by using the Filters utility class. We can apply a
single filter to a list of compatible values:
List<Person> results = Filters.filter(ageFilter(21), Arrays.asList(p1, p2));We can also apply a list of filters to a list of compatible values:
List<Person> results = Filters.filter(
Arrays.asList(ageFilter(25), sexFilter(Sex.FEMALE)),
Arrays.asList(p1, p2, p3));We can even combine filters together using the AND operator:
Filter<Person> compFilter = Filters.and(ageFilter(25), sexFilter(Sex.FEMALE));We can compose filters also with the OR operator:
Filter<Person> compFilter = Filters.or(ageFilter(25), sexFilter(Sex.FEMALE));Since these combinators operates on Filters (in other words they are functions [Filter] -> Filter), they can be freely combined to compose complex filters combinations:
Filters.or(Filters.and(ageFilter(25), sexFilter(Sex.FEMALE)),
Filters.and(ageFilter(21), sexFilter(Sex.MALE)),
ageFilter(31));Write a SqlFilter class. For example:
public class SqlNameFilter implements SqlFilter {
private String tableRef;
private final String name;
public SqlNameFilter(String tableRef, String name) {
this.tableRef = tableRef;
this.name = name;
}
@Override
public WhereClause whereClause() {
return WhereClause.whereClause(tableRef + "." + name + "=?");
}
@Override
public BindParamsF bindParameters() {
return new BindParamsF() {
@Override
public P2<ParamIndex, PreparedStatement> f(
P2<ParamIndex, PreparedStatement> p) throws SQLException {
ParamIndex index = p._1();
PreparedStatement statement = p._2();
statement.setString(index.get(), name);
return p(index.succ(), statement);
}
};
}
}Then you can use this filter to obtain a WHERE clause:
SqlFilter f = new SqlNameFilter("p", "name");
WhereClause c = f.whereClause();
// c.getClause() = "p.name=?"And to bind the parameters in a full JDBC query:
f.bindParameters().f(P.p(ParamIndex.paramIndex(4), statement));
// statement.setString(4, "name");A more complex example:
public class SqlPotentialFriendFilter implements SqlFilter {
private final Range range;
private final Sex sex;
private String tableRef;
public SqlPotentialFriendFilter(Range range, Sex sex, String tableRef) {
NotNull.check(range, tableRef);
this.range = range;
this.sex = sex;
this.tableRef = tableRef;
}
@Override
public WhereClause whereClause() {
return WhereClause.whereClause(String
.format("(%s.age BETWEEN ? AND ?) AND %s.sex=?", tableRef, tableRef));
}
@Override
public BindParamsF bindParameters() {
return new BindParamsF() {
@Override
public P2<ParamIndex, PreparedStatement> f(
P2<ParamIndex, PreparedStatement> p) throws SQLException {
ParamIndex index = p._1();
PreparedStatement statement = p._2();
statement.setInt(index.get(), range.getFrom());
statement.setInt(index.add(1).get(), range.getTo());
statement.setString(index.add(2).get(), sex.name());
return p(index.add(3), statement);
}
};
}
}You can also compose SqlFilter instances with the AND or OR operators:
SqlFilter composedFilter = SqlFilters.or(new SqlNameFilter("p", "name"),
new SqlNameFilter("p", "nomai"))A more complex example:
SqlFilter cf = SqlFilters.or(new SqlNameFilter("p", "Jerry"),
SqlFilters.and(new SqlNameFilter("p", "Mary"),
ageFilter(28)));- This component is versioned according to Semantic Versioning, using binary compatibility to classify breaking changes.
- For development, use SNAPSHOT versions.
- Whenever a new version has to be released:
- Write a new changelog section in this file.
- Change the version in the pom.xml according to the versioning policy.
- Commit.
- Tag the git commit using a tag in this form:
vX.Y.Z. - Push everything:
git push origin/master.
The public API exposed by this library (and significant for the versioning policy) is:
me.manuelp.siftj.Filteras the base class of every filter.me.manuelp.siftj.Filterswith its generic filtering functions.me.manuelp.siftj.SqlFilterswith its genericSqlFiltercomposing functions.me.manuelp.siftj.sql.*with the contract of aSqlFilterwith its supporting value types.