I'm playing around with lambdas and got into my head that I wanted to try creating a simple db/object mapper as a part of the learning.
Yes, there are plenty of frameworks that already do this, but this is more about learning and the problem I've run into is technical.
First, I wanted to define all mapping logic in an enum.
It started out plain and simple with just a bunch of field names:
enum ThingColumn {
id, language;
}
That let me create the following method (implementation not relevant) which gives api user compile check on columns:
public Collection<Thing> findAll(ThingColumn... columns);
After that I wanted to define more rules in the enum, specifically how results are mapped from a java.sql.ResultSet
to my Thing
class.
Starting out simple I created a functional interface:
@FunctionalInterface
static interface ThingResultMapper {
void map(Thing to, ResultSet from, String column) ;
}
and added it to the enum:
enum ThingColumn {
id((t, rs, col) -> t.setId(rs.getLong(col))),
language((t, rs, col) ->t.setLanguage(rs.getString(col)));
ThingColumn(ThingResultMapper mapper){..}
}
I created a mapResultSetRow
method which uses the lambdas from the enum to extract data from the ResultSet
:
public Thing mapResultSetRow(ResultSet rs, ThingColumn... fields) {
Thing t = new Thing();
Stream.of(fields)
.forEach(f -> f.getMapper().map(t, rs, f.name()));
return t;
}
The above findAll
could then use the mapResultSetRow
to apply relevant mappers to the ResultSet
. Nice and tidy.
Almost anyway. I think the enum is quite ugly and contains a lot of boiler plate with that lambda you have to put in for every mapping. Ideally I would like to do this instead:
enum ThingColumn {
id(ResultSet::getLong, Thing::setId),
language(ResultSet::getString, Thing::setLanguage);
}
However that does of course not compile and now I'm stuck, problems with non-static/static.. I'll break it down a little first by removing some noise:
enum ThingColumn {
id(ResultSet::getLong); // <<- compile error
ThingColumn(Function<String,?> resultSetExtractor) {..}
}
Compile error: Cannot make a static reference to the non-static method getLong(String) from the type ResultSet
.
I suppose what I want is either not possible to do, or possible by changing the signature of the labmda in the enum's constructor.
I found a similar issue in this question: Limits of static method references in Java 8 where Dmitry Ginzburg's answer (scroll down, not accepted as correct answer) outlines some issues, however no solution.
Thank you for reading so far :)
Any thoughts?
The first example will not work as you need to deal with checked SQLException
. This can be easily fixed though. First, declare this exception on your functional interface:
@FunctionalInterface
static interface ThingResultMapper {
void map(Thing to, ResultSet from, String column) throws SQLException;
}
Second, instead of getMapper
create a map
method in the enum which handles it:
enum ThingColumn {
id((t, rs, col) -> t.setId(rs.getLong(col))),
language((t, rs, col) ->t.setLanguage(rs.getString(col)));
private ThingResultMapper mapper;
ThingColumn(ThingResultMapper mapper){
this.mapper = mapper;
}
public void map(Thing to, ResultSet from) {
try {
mapper.map(to, from, name());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Now you can use it without problems:
public Thing mapResultSetRow(ResultSet rs, ThingColumn... fields) {
Thing t = new Thing();
Stream.of(fields).forEach(f -> f.map(t, rs));
return t;
}
The problem with second approach is that you have different data types (Long
, String
, etc.). To solve this you will need a functional interface to match ResultSet::getLong
, etc. method references:
@FunctionalInterface
static interface ResultGetter<T> {
T get(ResultSet from, String column) throws SQLException;
}
The parameters are ResultSet
itself (this
object, as ResultSet.getLong
-like methods are non-static) and the column. The resulting type may differ, so it's generic.
For Thing
setters you can use standard BiConsumer<Thing, T>
type. Also you will need a generic parameterized constructor (yes, they exist!). This constructor will create another function of type BiConsumer<Thing, ResultSet>
which can be used in map
method.
Here's the full code (mapResultSetRow
method is the same as above):
@FunctionalInterface
static interface ResultGetter<T> {
T get(ResultSet from, String column) throws SQLException;
}
enum ThingColumn {
id(ResultSet::getLong, Thing::setId),
language(ResultSet::getString, Thing::setLanguage);
private final BiConsumer<Thing, ResultSet> mapper;
<T> ThingColumn(ResultGetter<T> getter, BiConsumer<Thing, T> setter) {
this.mapper = (t, rs) -> {
try {
setter.accept(t, getter.get(rs, name()));
} catch (SQLException e) {
throw new RuntimeException(e);
}
};
}
public void map(Thing to, ResultSet from) {
this.mapper.accept(to, from);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With