Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using method references with parameters

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?

like image 845
Geir Avatar asked Aug 19 '15 23:08

Geir


1 Answers

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);
    }
}
like image 196
Tagir Valeev Avatar answered Oct 15 '22 07:10

Tagir Valeev