BeanPropertyRowMapper is ill-suited to the task, however, since that requires a no-arg constructor that records don't have and since the properties in records don't look like normal JavaBean properties - they're int foo()/foo(int) instead of int getFoo()/setFoo(int).

Comment From: joshlong

OK, I managed to get a working prototype going here.

Given a record Reservation:

record Reservation(Integer id, String firstName) {
}

...and a SQL table RESERVATION with column first_name and id, the following RowMapper<T> implementation works.

The problem is that there are no setters for records. The only way to initialize record is to use a constructor, and there may be many records in a given record class.

This class makes no attempt to use a ConversionService yet.



/**
    * Maps data from a JDBC {@code ResultSet} into Java 14 record objects of type T.
    *
    * @author <a href="mailto:josh@joshlong.com">Josh Long</a>
    */
class RecordRowMapper<T> implements RowMapper<T> {

    private final Log logger = LogFactory.getLog(getClass());
    private final Map<String, RecordComponent> mappedFields = new HashMap<>();
    private final List<String> parameterNames = new ArrayList<>();
    private final Constructor<?> constructor;

    /**
        * Instantiate a new instance using the default constructor for a class.
        */
    public RecordRowMapper(Class<T> tClass) {
        this(tClass.getConstructors()[0]);
    }

    /**
        * If there's ambiguity as to which constructor to invoke, provide one explicitly.
        */
    public RecordRowMapper(Constructor<?> constructor) {
        Assert.notNull(constructor, "the Constructor reference should not be null");
        this.constructor = constructor;

        for (RecordComponent recordComponent : constructor.getDeclaringClass().getRecordComponents()) {
            this.mappedFields.put(normalizeName(recordComponent.getName()), recordComponent);
        }

        for (Parameter parameter : constructor.getParameters()) {
            parameterNames.add(parameter.getName());
        }
    }

    @Override
    public T mapRow(ResultSet rs, int rowNum) throws SQLException {
        ResultSetMetaData metaData = rs.getMetaData();
        Map<String, Object> objectProperties = new HashMap<>();
        int columnCount = metaData.getColumnCount();
        for (int index = 1; index <= columnCount; index++) {
            String column = JdbcUtils.lookupColumnName(metaData, index);
            RecordComponent recordComponent = this.mappedFields.get(normalizeName(column));
            if (null != recordComponent) {
                if (rowNum == 0 && logger.isDebugEnabled()) {
                    logger.debug("Mapping column '" + column + "' to property '" + recordComponent.getName() +
                        "' of type '" + ClassUtils.getQualifiedName(recordComponent.getDeclaringRecord()) + "'");
                }
                objectProperties.put(recordComponent.getName(), getColumnValue(rs, index, recordComponent.getType()));
            }
        }
        try {
            return (T) this.constructor.newInstance(
                this.parameterNames.stream().map(objectProperties::get).toArray()
            );
        }
        catch (Exception e) {
            ReflectionUtils.rethrowRuntimeException(e);
        }
        return null;
    }

    private String normalizeName(String name) {
        return name.toLowerCase().replaceAll("_", "");
    }

    private Object getColumnValue(ResultSet rs, int index, Class<?> clzz) throws SQLException {
        return JdbcUtils.getResultSetValue(rs, index, clzz);
    }
}

So, it's possible to make it work. Is there any appetite to support something like this in Spring Framework now, or at some point in the future?

Comment From: jhoeller

Good idea! We got related enhancement requests for our constructor-bound web data binding already where we're also aiming for proper record class support in 5.3.

Comment From: jhoeller

I've addressed this with a general DataClassRowMapper which combines bean property capabilities with constructor-based binding, depending on the given mapped class, similar to our data class binding in web handler methods. This is based on constructor parameter naming with no reference to record-specific reflection APIs, so it'll work with similarly styled classes as well.