5

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?

1
  • Based on this Oracle tutorial, I would assume that ResultSet::getLong expresses a single method interface that takes a ResultSet instance, like Object f(ResultSet rs, String col). At the very least this wouldn't fit into a Function<String,?>. I wouldn't have expected that compile error from it, though. Commented Aug 20, 2015 at 0:05

1 Answer 1

5

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); } } 
Sign up to request clarification or add additional context in comments.

2 Comments

Wow! It's amazing to be always learning new things in Java. Generic parameterized constructors are a nice and (almost) unknown feature. Are they legal for enums only or can they also be used in classes? Thanks for this answer!
@FedericoPeraltaSchaffner, they can be used for classes as well.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.