Open
Description
Enhancement Description: allow JdbcClient to convert custom object types in records. for example, in postgres you may have a jsonb and you'd want to be able to convert that into something so that you can parse it onto your record class while fetching:
record CustomRecord(JsonNode jsonbColumn) {}
jdbcClient.sql("select jsonb_column from some_table").query(CustomRecord.class).list();
code references:
- currently only NamedParameterJdbcTemplate is passed to JdbcClient:
- jdbcClient instantiates its own SimplePropertyRowMapper's ( )
- SimplePropertyRowMapper is supposed to be able to take a conversion service, but currently requires quite a bit of boilerplate to leverage via Spring Boot -
possible solution:
- refactor DefaultJdbcClient constructors
from their current form
class DefaultJdbcClient {
private final JdbcOperations classicOps;
private final NamedParameterJdbcOperations namedParamOps;
private final Map<Class<?>, RowMapper<?>> rowMapperCache = new ConcurrentHashMap<>();
public DefaultJdbcClient(DataSource dataSource) {
this.classicOps = new JdbcTemplate(dataSource);
this.namedParamOps = new NamedParameterJdbcTemplate(this.classicOps);
}
public DefaultJdbcClient(JdbcOperations jdbcTemplate) {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
this.classicOps = jdbcTemplate;
this.namedParamOps = new NamedParameterJdbcTemplate(jdbcTemplate);
}
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
this.classicOps = jdbcTemplate.getJdbcOperations();
this.namedParamOps = jdbcTemplate;
}
}
to this pattern:
class DefaultJdbcClient {
private final JdbcOperations classicOps;
private final NamedParameterJdbcOperations namedParamOps;
private final ConversionService conversionService;
private final Map<Class<?>, RowMapper<?>> rowMapperCache = new ConcurrentHashMap<>();
public DefaultJdbcClient(DataSource dataSource) {
this(new JdbcTemplate(dataSource));
}
public DefaultJdbcClient(JdbcOperations jdbcTemplate) {
this(new NamedParameterJdbcTemplate(jdbcTemplate));
}
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) {
this(jdbcTemplate.getJdbcOperations(), jdbcTemplate, DefaultConversionService.getSharedInstance());
}
public DefaultJdbcClient(
JdbcOperations jdbcOperations,
NamedParameterJdbcOperations namedParameterJdbcOperations,
ConversionService conversionService
) {
Assert.notNull(jdbcOperations, "jdbcOperations must not be null");
Assert.notNull(namedParameterJdbcOperations, "namedParameterJdbcOperations must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
this.classicOps = jdbcOperations;
this.namedParamOps = namedParameterJdbcOperations;
this.conversionService = conversionService;
}
}
- add a JdbcClient.create method that is just going to be kept in sync with the bottom constructor (just allow user to pass all components)
- pass this conversion service where appropriate to simple property row mapper constructors et al
- change the AutoConfiguration class from
@Bean
JdbcClient jdbcClient(NamedParameterJdbcTemplate jdbcTemplate) {
return JdbcClient.create(jdbcTemplate);
}
to
@Bean
JdbcClient jdbcClient(NamedParameterJdbcTemplate jdbcTemplate, /*kosher?*/ @Nullable ConversionService conversionService) {
return JdbcClient.create(jdbcTemplate, java.util.Objects.requireNonNullElseGet(conversionService, DefaultConversionService::getSharedInstance));
}
benefits
you can now register converters as beans and they will be able to be used for jdbcClient queries.
considerations
- not sure how ok it is to rely on the conversion service bean as I understand its intent was for converting http requests/responses, hence why i made it an optional dependency and defaulted it to:
java.util.Objects.requireNonNullElseGet(conversionService, DefaultConversionService::getSharedInstance)
. - alternative to consider is just to write boilerplate, eg. by creating your own row mappers - https://stackoverflow.com/a/78655612
Activity