-
Notifications
You must be signed in to change notification settings - Fork 38.6k
Closed
Labels
in: dataIssues in data modules (jdbc, orm, oxm, tx)Issues in data modules (jdbc, orm, oxm, tx)type: enhancementA general enhancementA general enhancement
Milestone
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 (
Lines 199 to 206 in d2ea5b4
@SuppressWarnings("unchecked") @Override public <T> MappedQuerySpec<T> query(Class<T> mappedClass) { RowMapper<?> rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key -> BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) : new SimplePropertyRowMapper<>(mappedClass)); return query((RowMapper<T>) rowMapper); } - SimplePropertyRowMapper is supposed to be able to take a conversion service, but currently requires quite a bit of boilerplate to leverage via Spring Boot -
Lines 92 to 120 in d2ea5b4
/** * Create a new {@code SimplePropertyRowMapper}. * @param mappedClass the class that each row should be mapped to */ public SimplePropertyRowMapper(Class<T> mappedClass) { this(mappedClass, DefaultConversionService.getSharedInstance()); } /** * Create a new {@code SimplePropertyRowMapper}. * @param mappedClass the class that each row should be mapped to * @param conversionService a {@link ConversionService} for binding * JDBC values to bean properties */ public SimplePropertyRowMapper(Class<T> mappedClass, ConversionService conversionService) { Assert.notNull(mappedClass, "Mapped Class must not be null"); Assert.notNull(conversionService, "ConversionService must not be null"); this.mappedClass = mappedClass; this.conversionService = conversionService; this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); int paramCount = this.mappedConstructor.getParameterCount(); this.constructorParameterNames = (paramCount > 0 ? BeanUtils.getParameterNames(this.mappedConstructor) : new String[0]); this.constructorParameterTypes = new TypeDescriptor[paramCount]; for (int i = 0; i < paramCount; i++) { this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); } }
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
Metadata
Metadata
Assignees
Labels
in: dataIssues in data modules (jdbc, orm, oxm, tx)Issues in data modules (jdbc, orm, oxm, tx)type: enhancementA general enhancementA general enhancement