diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java index 1fac8e179b..75a2c8ff56 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java +++ b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java @@ -34,6 +34,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Mark Paluch + * @author Fei Dong * @see JdbcConfiguration */ @Target(ElementType.TYPE) @@ -97,4 +98,17 @@ * for {@code PersonRepositoryImpl}. */ String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the name of the {@link NamedParameterJdbcOperations} bean definition to be used to create repositories + * discovered through this annotation. Defaults to {@code namedParameterJdbcTemplate}. + */ + String jdbcOperationsRef() default ""; + + + /** + * Configures the name of the {@link DataAccessStrategy} bean definition to be used to create repositories + * discovered through this annotation. Defaults to {@code defaultDataAccessStrategy} if existed. + */ + String dataAccessStrategyRef() default ""; } diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java index e2e5916291..5e2dfd53c4 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java @@ -15,19 +15,41 @@ */ package org.springframework.data.jdbc.repository.config; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * {@link org.springframework.data.repository.config.RepositoryConfigurationExtension} extending the repository * registration process by registering JDBC repositories. * * @author Jens Schauder + * @author Fei Dong */ public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { + private ConfigurableListableBeanFactory listableBeanFactory; /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() @@ -36,6 +58,7 @@ public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtens public String getModuleName() { return "JDBC"; } + /* * (non-Javadoc) @@ -55,4 +78,87 @@ protected String getModulePrefix() { return getModuleName().toLowerCase(Locale.US); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + if (registry instanceof ConfigurableListableBeanFactory) { + this.listableBeanFactory = (ConfigurableListableBeanFactory) registry; + } + } + + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) + */ + @Override + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { + resolveReference(builder, source, "jdbcOperationsRef", "jdbcOperations", NamedParameterJdbcOperations.class, + true); + resolveReference(builder, source, "dataAccessStrategyRef", "dataAccessStrategy", DataAccessStrategy.class, + false); + } + + private void resolveReference(BeanDefinitionBuilder builder, RepositoryConfigurationSource source, + String attributeName, String propertyName, Class classRef, boolean required) { + Optional beanNameRef = source.getAttribute(attributeName).filter(StringUtils::hasText); + + String beanName = beanNameRef.orElseGet(() -> { + if (this.listableBeanFactory != null) { + List beanNames = Arrays.asList(listableBeanFactory.getBeanNamesForType(classRef)); + Map bdMap = beanNames.stream() + .collect(Collectors.toMap(Function.identity(), listableBeanFactory::getBeanDefinition)); + + if (beanNames.size() > 1) { + // determine primary + + Map primaryBdMap = bdMap.entrySet().stream() + .filter(e -> e.getValue().isPrimary()) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + Optional primaryBeanName = getSingleBeanName(primaryBdMap.keySet(), classRef, + () -> "more than one 'primary' bean found among candidates: " + primaryBdMap.keySet()); + + // In Java 11 should use Optional.or() + if (primaryBeanName.isPresent()) { + return primaryBeanName.get(); + } + + // determine matchesBeanName + + Optional matchesBeanName = beanNames.stream() + .filter(name -> propertyName.equals(name) + || ObjectUtils.containsElement(listableBeanFactory.getAliases(name), propertyName)) + .findFirst(); + + if (matchesBeanName.isPresent()) { + return matchesBeanName.get(); + } + + } + + if (beanNames.size() == 1) { + return beanNames.get(0); + } + } + return null; + }); + if (beanName != null) { + builder.addPropertyReference(propertyName, beanName); + } else if (required) { + throw new NoSuchBeanDefinitionException(classRef); + } + } + + private Optional getSingleBeanName(Collection beanNames, Class classRef, + Supplier errorMessage) { + if (beanNames.size() > 1) { + throw new NoUniqueBeanDefinitionException(classRef, beanNames.size(), errorMessage.get()); + } + + return beanNames.stream().findFirst(); + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 927afcc822..a8f69c6167 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -57,7 +57,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend * * @param repositoryInterface must not be {@literal null}. */ - JdbcRepositoryFactoryBean(Class repositoryInterface) { + protected JdbcRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } @@ -96,7 +96,6 @@ protected void setMappingContext(RelationalMappingContext mappingContext) { /** * @param dataAccessStrategy can be {@literal null}. */ - @Autowired(required = false) public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) { this.dataAccessStrategy = dataAccessStrategy; } @@ -110,7 +109,6 @@ public void setRowMapperMap(RowMapperMap rowMapperMap) { this.rowMapperMap = rowMapperMap; } - @Autowired public void setJdbcOperations(NamedParameterJdbcOperations operations) { this.operations = operations; } diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index abcd128c4d..194294be0f 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -15,50 +15,69 @@ */ package org.springframework.data.jdbc.repository.config; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import lombok.Data; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import java.lang.reflect.Field; +import javax.sql.DataSource; + import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; +import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.util.ReflectionUtils; +import lombok.Data; + /** * Tests the {@link EnableJdbcRepositories} annotation. * * @author Jens Schauder * @author Greg Turnquist + * @author Fei Dong */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = TestConfiguration.class) public class EnableJdbcRepositoriesIntegrationTests { static final Field ROW_MAPPER_MAP = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "rowMapperMap"); + static final Field OPERATIONS = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "operations"); + static final Field DATA_ACCESS_STRATEGY = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "dataAccessStrategy"); public static final RowMapper DUMMY_ENTITY_ROW_MAPPER = mock(RowMapper.class); public static final RowMapper STRING_ROW_MAPPER = mock(RowMapper.class); @Autowired JdbcRepositoryFactoryBean factoryBean; @Autowired DummyRepository repository; + @Autowired @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations defaultOperations; + @Autowired @Qualifier("defaultDataAccessStrategy") DataAccessStrategy defaultDataAccessStrategy; + @Autowired @Qualifier("qualifierJdbcOperations") NamedParameterJdbcOperations qualifierJdbcOperations; + @Autowired @Qualifier("qualifierDataAccessStrategy") DataAccessStrategy qualifierDataAccessStrategy; @BeforeClass public static void setup() { ROW_MAPPER_MAP.setAccessible(true); + OPERATIONS.setAccessible(true); + DATA_ACCESS_STRATEGY.setAccessible(true); } @Test // DATAJDBC-100 @@ -80,6 +99,15 @@ public void customRowMapperConfigurationGetsPickedUp() { assertThat(mapping.rowMapperFor(DummyEntity.class)).isEqualTo(DUMMY_ENTITY_ROW_MAPPER); } + @Test // DATAJDBC-293 + public void jdbcOperationsRef() { + NamedParameterJdbcOperations operations = (NamedParameterJdbcOperations) ReflectionUtils.getField(OPERATIONS, factoryBean); + assertThat(operations).isNotSameAs(defaultDataAccessStrategy).isSameAs(qualifierJdbcOperations); + + DataAccessStrategy dataAccessStrategy = (DataAccessStrategy) ReflectionUtils.getField(DATA_ACCESS_STRATEGY, factoryBean); + assertThat(dataAccessStrategy).isNotSameAs(defaultDataAccessStrategy).isSameAs(qualifierDataAccessStrategy); + } + interface DummyRepository extends CrudRepository { } @@ -90,7 +118,9 @@ static class DummyEntity { } @ComponentScan("org.springframework.data.jdbc.testing") - @EnableJdbcRepositories(considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = DummyRepository.class)) + @EnableJdbcRepositories(considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = DummyRepository.class), + jdbcOperationsRef = "qualifierJdbcOperations", dataAccessStrategyRef = "qualifierDataAccessStrategy") static class TestConfiguration { @Bean @@ -105,5 +135,15 @@ RowMapperMap rowMappers() { .register(String.class, STRING_ROW_MAPPER); } + @Bean("qualifierJdbcOperations") + NamedParameterJdbcOperations qualifierJdbcOperations(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + @Bean("qualifierDataAccessStrategy") + DataAccessStrategy defaultDataAccessStrategy(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, + RelationalMappingContext context, RelationalConverter converter) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, template); + } } } diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 1afdfbc239..33e81d09dc 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -21,10 +21,12 @@ import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; @@ -46,6 +48,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Mark Paluch + * @author Fei Dong */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) @@ -56,8 +59,8 @@ public class TestConfiguration { @Autowired(required = false) SqlSessionFactory sqlSessionFactory; @Bean - JdbcRepositoryFactory jdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - RelationalConverter converter) { + JdbcRepositoryFactory jdbcRepositoryFactory(@Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, + RelationalMappingContext context, RelationalConverter converter) { return new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, namedParameterJdbcTemplate()); } @@ -72,13 +75,13 @@ PlatformTransactionManager transactionManager() { } @Bean - DataAccessStrategy defaultDataAccessStrategy(RelationalMappingContext context, RelationalConverter converter) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, - namedParameterJdbcTemplate()); + DataAccessStrategy defaultDataAccessStrategy(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, + RelationalMappingContext context, RelationalConverter converter) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter,template); } @Bean - RelationalMappingContext jdbcMappingContext(NamedParameterJdbcOperations template, + RelationalMappingContext jdbcMappingContext(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, Optional namingStrategy, CustomConversions conversions) { RelationalMappingContext mappingContext = new RelationalMappingContext(