Jdbi and Inheritance: Conditional Mapping?

798 views Asked by At

I have a single table called Tags that stores a "Tag" as a row, regardless of what specific subclass they represent. Some rows represent modbus tags, some snmp, some other protocols. All classes inheriting from Tag store their data in this one table, and unused columns simply contain null values.

At the moment, I have DAO methods like, getAllModBusTags() which contains an instruction mapToBean(ModBusTag.class). Eventually all of the subclasses of Tag are fetched from the database (one fetch per protocol) and then added to an ArrayList of the supertype Tag.

My question is, is there a simple means with Jdbi to perform conditional mapping of rows so that if a row contains a specific value, it is mapped to ModBusTag.class but if a row contains a different value it is mapped to SNMPTag.class, and so on and so forth?

My end goal is to have a single select statement that fetches every tag from the database, automaps to the correct bean on a row by row basis and then stores all of these subclass beans in a List of the supertype Tag.

Example Method for Single Type:

@Override
public List<SNMPTag> getSNMPTags(){
    try(Handle handle = daoFactory.getDataSourceController().open()) {
        return handle.createQuery("SELECT * FROM dbo.Tags WHERE Active = 1 AND Protocol = 'SNMP'")
                .mapToBean(SNMPTag.class)
                .list();
    }
    catch(Exception e){
        if(sysconfig.getVerbose()){ e.printStackTrace(); }
    }
    return null;
}

Some bad pseudocode to indicate what I want to do:

@Override
public List<Tag> getAllTags(){
    try(Handle handle = daoFactory.getDataSourceController().open()) {
        return handle.createQuery("SELECT * FROM dbo.Tags WHERE Active = 1")
        .mapRows(row -> row.Protocol.equals("SNMP").mapToBean(SNMPTag.class)
        .mapRows(row -> row.Protocol.equals("ModBus").mapToBean(ModBusTag.class)
        //etc
        .list();
    }
    catch(Exception e){
        if(sysconfig.getVerbose()){ e.printStackTrace(); }
    }
    return null;
}
1

There are 1 answers

1
Yurii Melnychuk On BEST ANSWER

You can use RowMapper with some amount of custom code to achieve what you need, we successfully use such approach in our project. Here is simplified general example of this technique:

public class PolymorphicRowMapper implements RowMapper<Parent> {

    @Override
    public Parent map(ResultSet rs, StatementContext ctx) throws SQLException {
        Type type = Type.valueOf(rs.getString("type"));
        if (type == Type.A) {
            return mapTo(rs, ctx, ChildA.class);
        } else if (type == Type.B) {
            return mapTo(rs, ctx, ChildB.class);
        }
        throw new IllegalStateException("Could not resolve mapping strategy for object");
    }

    private static <T extends Parent> T mapTo(
        ResultSet rs,
        StatementContext ctx,
        Class<T> targetClass
    ) throws SQLException {
        return ctx.getConfig().get(Mappers.class)
            .findFor(targetClass)
            .orElseThrow(() ->
                new NoSuchMapperException(String.format("No mapper registered for %s class", targetClass))
            )
            .map(rs, ctx);
    }

}

public static void main(String[] args) {
    var jdbi = Jdbi.create("...")
        .registerRowMapper(BeanMapper.factory(ChildA.class))
        .registerRowMapper(BeanMapper.factory(ChildB.class));
    try (Handle handle = jdbi.open()) {
        handle.createQuery("SELECT * FROM table")
            .map(new PolymorphicRowMapper());
    }
}

public enum Type {
    A, B
}

public abstract class Parent {
    final Type type;

    protected Parent(final Type type) {
        this.type = type;
    }
}

public class ChildA extends Parent {

    public ChildA() {
        super(Type.A);
    }

}

public class ChildB extends Parent {

    public ChildB() {
        super(Type.B);
    }

}