Skip to content

DATAJDBC-166 - Allow registration of RowMappers based on result type. #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,34 @@ List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper")

If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations.

==== Custom RowMapper

You can configure the `RowMapper` to use using either the `@Query(rowMapperClass = ....)` or you can register a `RowMapperMap` bean and register `RowMapper` per method return type.

[source,java]
----

@Bean
RowMapperMap rowMappers() {
return new ConfigurableRowMapperMap() //
.register(Person.class, new PersonRowMapper()) //
.register(Address.class, new AddressRowMapper());
}

----

When determining the `RowMapper` to use for a method the following steps are followed based on the return type of the method:

1. If the type is a simple type no `RowMapper` is used.
Instead the query is expected to return a single row with a single column and a conversion to the return type is applied to that value.

2. The entity classes in the `RowMapperMap` are iterated until one is found that is a superclass or interface of the return type in question.
The `RowMapper` registered for that class is used.
Iterating happens in the order of registration, so make sure to register more general types after specific ones.

If applicable wrapper type like collections or `Optional` are unwrapped.
So a return type of `Optional<Person>` will use the type `Person` in the steps above.

=== Id generation

Spring Data JDBC uses the id to identify entities but also to determine if an entity is new or already existing in the database.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<version>1.0.0.DATAJDBC-166-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
*/
package org.springframework.data.jdbc.core;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
Expand All @@ -34,12 +40,6 @@
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity.
*
Expand All @@ -57,7 +57,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
private final DataAccessStrategy accessStrategy;

public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
JdbcMappingContext context, DataAccessStrategy accessStrategy) {
JdbcMappingContext context, DataAccessStrategy accessStrategy) {

this.sqlGeneratorSource = sqlGeneratorSource;
this.operations = operations;
Expand All @@ -70,7 +70,7 @@ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedPar
* Only suitable if this is the only access strategy in use.
*/
public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
JdbcMappingContext context) {
JdbcMappingContext context) {

this.sqlGeneratorSource = sqlGeneratorSource;
this.operations = operations;
Expand Down Expand Up @@ -236,7 +236,8 @@ private <S> MapSqlParameterSource getPropertyMap(final S instance, JdbcPersisten
@SuppressWarnings("unchecked")
private <S, ID> ID getIdValueOrNull(S instance, JdbcPersistentEntity<S> persistentEntity) {

EntityInformation<S, ID> entityInformation = (EntityInformation<S, ID>) context.getRequiredPersistentEntityInformation(persistentEntity.getType());
EntityInformation<S, ID> entityInformation = (EntityInformation<S, ID>) context
.getRequiredPersistentEntityInformation(persistentEntity.getType());

ID idValue = entityInformation.getId(instance);

Expand Down Expand Up @@ -283,7 +284,7 @@ private <S> Optional<Object> getIdFromHolder(KeyHolder holder, JdbcPersistentEnt
}

public <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context.getConversions(), context, accessStrategy);
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, accessStrategy);
}

private RowMapper getMapEntityRowMapper(JdbcPersistentProperty property) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ClassGeneratingEntityInstantiator;
Expand All @@ -32,9 +36,6 @@
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
* Maps a ResultSet to an entity of type {@code T}, including entities referenced.
*
Expand All @@ -53,11 +54,11 @@ public class EntityRowMapper<T> implements RowMapper<T> {
private final DataAccessStrategy accessStrategy;
private final JdbcPersistentProperty idProperty;

public EntityRowMapper(JdbcPersistentEntity<T> entity, ConversionService conversions, JdbcMappingContext context,
DataAccessStrategy accessStrategy) {
public EntityRowMapper(JdbcPersistentEntity<T> entity, JdbcMappingContext context,
DataAccessStrategy accessStrategy) {

this.entity = entity;
this.conversions = conversions;
this.conversions = context.getConversions();
this.context = context;
this.accessStrategy = accessStrategy;

Expand Down Expand Up @@ -124,7 +125,8 @@ private <S> S readEntityFrom(ResultSet rs, PersistentProperty<?> property) {
return null;
}

S instance = instantiator.createInstance(entity, new ResultSetParameterValueProvider(rs, entity, conversions, prefix));
S instance = instantiator.createInstance(entity,
new ResultSetParameterValueProvider(rs, entity, conversions, prefix));

PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions);
Expand All @@ -139,14 +141,10 @@ private <S> S readEntityFrom(ResultSet rs, PersistentProperty<?> property) {
@RequiredArgsConstructor
private static class ResultSetParameterValueProvider implements ParameterValueProvider<JdbcPersistentProperty> {

@NonNull
private final ResultSet resultSet;
@NonNull
private final JdbcPersistentEntity<?> entity;
@NonNull
private final ConversionService conversionService;
@NonNull
private final String prefix;
@NonNull private final ResultSet resultSet;
@NonNull private final JdbcPersistentEntity<?> entity;
@NonNull private final ConversionService conversionService;
@NonNull private final String prefix;

/*
* (non-Javadoc)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository;

import org.springframework.jdbc.core.RowMapper;

/**
* A map from a type to a {@link RowMapper} to be used for extracting that type from {@link java.sql.ResultSet}s.
*
* @author Jens Schauder
*/
public interface RowMapperMap {

/**
* An immutable empty instance that will return {@literal null} for all arguments.
*/
RowMapperMap EMPTY = new RowMapperMap() {

public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {
return null;
}
};

<T> RowMapper<? extends T> rowMapperFor(Class<T> type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.config;

import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.jdbc.core.RowMapper;

/**
* A {@link RowMapperMap} that allows for registration of {@link RowMapper}s via a fluent Api.
*
* @author Jens Schauder
*/
public class ConfigurableRowMapperMap implements RowMapperMap {

private Map<Class<?>, RowMapper<?>> rowMappers = new LinkedHashMap<>();

/**
* Registers a the given {@link RowMapper} as to be used for the given type.
*
* @return this instance, so this can be used as a fluent interface.
*/
public <T> ConfigurableRowMapperMap register(Class<T> type, RowMapper<? extends T> rowMapper) {

rowMappers.put(type, rowMapper);
return this;
}

@SuppressWarnings("unchecked")
public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {

RowMapper<? extends T> candidate = (RowMapper<? extends T>) rowMappers.get(type);

if (candidate == null) {

for (Map.Entry<Class<?>, RowMapper<?>> entry : rowMappers.entrySet()) {

if (type.isAssignableFrom(entry.getKey())) {
candidate = (RowMapper<? extends T>) entry.getValue();
}
}
}

return candidate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.EntityRowMapper;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
Expand All @@ -40,13 +41,15 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {

private final JdbcMappingContext context;
private final DataAccessStrategy accessStrategy;
private final RowMapperMap rowMapperMap;
private final ConversionService conversionService;

JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context,
DataAccessStrategy accessStrategy) {
DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap) {

this.context = context;
this.accessStrategy = accessStrategy;
this.rowMapperMap = rowMapperMap;
this.conversionService = context.getConversions();
}

Expand All @@ -55,22 +58,32 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
ProjectionFactory projectionFactory, NamedQueries namedQueries) {

JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory);
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();

RowMapper<?> rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(returnedObjectType);
RowMapper<?> rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(queryMethod);

return new JdbcRepositoryQuery(queryMethod, context, rowMapper);
}

private RowMapper<?> createRowMapper(Class<?> returnedObjectType) {
private RowMapper<?> createRowMapper(JdbcQueryMethod queryMethod) {

Class<?> returnedObjectType = queryMethod.getReturnedObjectType();

return context.getSimpleTypeHolder().isSimpleType(returnedObjectType)
? SingleColumnRowMapper.newInstance(returnedObjectType, conversionService)
: new EntityRowMapper<>( //
context.getRequiredPersistentEntity(returnedObjectType), //
conversionService, //
: determineDefaultRowMapper(queryMethod);
}

private RowMapper<?> determineDefaultRowMapper(JdbcQueryMethod queryMethod) {

Class<?> domainType = queryMethod.getReturnedObjectType();

RowMapper typeMappedRowMapper = rowMapperMap.rowMapperFor(domainType);

return typeMappedRowMapper == null //
? new EntityRowMapper<>( //
context.getRequiredPersistentEntity(domainType), //
context, //
accessStrategy //
);
accessStrategy) //
: typeMappedRowMapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.data.jdbc.core.JdbcEntityTemplate;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.SimpleJdbcRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
Expand All @@ -42,6 +43,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
private final JdbcMappingContext context;
private final ApplicationEventPublisher publisher;
private final DataAccessStrategy accessStrategy;
private RowMapperMap rowMapperMap = RowMapperMap.EMPTY;

public JdbcRepositoryFactory(ApplicationEventPublisher publisher, JdbcMappingContext context,
DataAccessStrategy dataAccessStrategy) {
Expand Down Expand Up @@ -84,6 +86,10 @@ protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrate
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
}

return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy));
return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy, rowMapperMap));
}

public void setRowMapperMap(RowMapperMap rowMapperMap) {
this.rowMapperMap = rowMapperMap;
}
}
Loading