Friday, September 09, 2011

Generic database records in Java

I haven't posted code in too long. In part I am enjoying work so much these days I don't need this blog as an outlet for my creativity. Still, I feel a touch of guilt.

In a trivial context this came up: How to represent a database record efficiently and generically in Java? By efficiently I mean close to the efficiency of standard Java beans. By generically I mean without the custom writing of standard Java beans. The traditional map representation is certainly generic and simple to write but is not efficient in time or space.

With one condition a nice solution arises: generically applies only to compile-time; that is, I know at compile-time the types and labels of columns read from the database record. That solution: EnumMap.

Some code:

interface Field<R, X extends Exception> {
    <T> T get(final R set) throws X;
}

class EnumRecord<R, X extends Exception,
                E extends Enum<E> & Field<R, X>>
            implements Field<E, RuntimeException> {
    private Map<E, Object> fields;

    EnumRecord(Class<E> enumType, R set) throws X {
        this(enumType, getKeyUniverse(enumType), set);
    }

    EnumRecord(Class<E> enumType, E[] values, R set)
            throws X {
        fields = new EnumMap<E, Object>(enumType);

        for (E field : values)
            fields.put(field, field.get(set));
    }

    EnumRecord(Class<E> enumType, Iterable<E> values, R set)
            throws X {
        fields = new EnumMap<E, Object>(enumType);

        for (E field : values)
            fields.put(field, field.get(set));
    }

    @Override
    public final <T> T get(E value) {
        return (T) fields.get(value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (null == o || getClass() != o.getClass())
            return false;

        EnumRecord detail = (EnumRecord) o;

        return fields.equals(detail.fields);
    }

    @Override
    public int hashCode() {
        return fields.hashCode();
    }

    private static <K extends Enum<K>>
            K[] getKeyUniverse(Class<K> enumType) {
        // Copied shamelessly from EnumMap
        return SharedSecrets.getJavaLangAccess()
                .getEnumConstantsShared(enumType);
    }
}

class ResultSetRecord<E extends Enum<E>
            & Field<ResultSet, SQLException>>
        extends EnumRecord<ResultSet, SQLException, E> {
    ResultSetRecord(Class<E> enumType, ResultSet set)
            throws SQLException {
        super(enumType, set);
    }

    ResultSetRecord(Class<E> enumType, E[] values, ResultSet set)
            throws SQLException {
        super(enumType, values, set);
    }

    ResultSetRecord(Class<E> enumType, Iterable<E> values,
                ResultSet set) throws SQLException {
        super(enumType, values, set);
    }
}

enum SampleSQLEnum
        implements Field<ResultSet, SQLException> {
    nick_name() {
        @Override
        public String get(ResultSet set) throws SQLException {
            return getString(set);
        }
    },
    lucky_number() {
        @Override
        public Integer get(final ResultSet set)
                throws SQLException {
            return getInteger(set);
        }
    };

    @Override
    public abstract <T> T get(final ResultSet set)
            throws SQLException;

    protected String getString(ResultSet set)
            throws SQLException {
        String value = set.getString(name()).trim();
        return isBlank(value) ? null : value;
    }

    protected Integer getInteger(ResultSet set)
            throws SQLException {
        int value = set.getInt(name());
        return set.wasNull() ? null : value;
    }
}

class SampleRecord
        extends ResultSetRecord<SampleSQLEnum> {
    SampleRecord(ResultSet set) throws SQLException {
        super(SampleSQLEnum.class, SampleSQLEnum.values(), set);
    }


    String nick_name() {
        return get(SampleSQLEnum.nick_name);
    }

    Integer luck_number() {
        return get(SampleSQLEnum.lucky_number);
    }
}

public class SampleRecordMain {
    public static void main(final String... args)
            throws SQLException {
        SampleRecord record = new SampleRecord(readFromSomewhere());
        System.out.println(record.nick_name());
        System.out.println(record.luck_number());
    }

    private static ResultSet readFromSomewhere() {
        throw null;
    }
}

UPDATE: Some have had trouble importing SharedSecrets. I used it as a convenience to avoid reflection. As an alternative you could reflect over the enum to implement getKeyUniverse yourself.

Post a Comment