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 e822e876..6556dc72 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 @@ -32,18 +32,19 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBea * repositories by default. * * @author Jens Schauder + * @author Greg Turnquist * @since 2.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Import(JdbcRepositoriesRegistrar.class) +@Import({JdbcRepositoriesRegistrar.class, JdbcConfiguration.class}) public @interface EnableJdbcRepositories { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}. + * {@code @EnableJdbcRepositories("org.my.pkg")} instead of {@code @EnableJdbcRepositories(basePackages="org.my.pkg")}. */ String[] value() default {}; diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java new file mode 100644 index 00000000..865a68db --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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.Optional; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; +import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.NamingStrategy; + +/** + * Beans that must be registered for Spring Data JDBC to work. + * + * @author Greg Turnquist + */ +@Configuration +public class JdbcConfiguration { + + @Bean + JdbcMappingContext jdbcMappingContext(Optional namingStrategy, + Optional conversionCustomizer) { + + return new JdbcMappingContext( + namingStrategy.orElse(new DefaultNamingStrategy()), + conversionCustomizer.orElse(conversionService -> {})); + } +} 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 cb717b33..3e420de8 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 @@ -16,35 +16,16 @@ package org.springframework.data.jdbc.repository.support; import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.sql.DataSource; - -import org.apache.ibatis.session.SqlSessionFactory; -import org.springframework.context.ApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; -import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; -import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; -import org.springframework.data.jdbc.mapping.model.NamingStrategy; -import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; -import org.springframework.data.util.Optionals; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.util.ClassUtils; +import org.springframework.util.Assert; /** * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of @@ -55,141 +36,50 @@ import org.springframework.util.ClassUtils; * @since 2.0 */ public class JdbcRepositoryFactoryBean, S, ID extends Serializable> // - extends TransactionalRepositoryFactoryBeanSupport { + extends TransactionalRepositoryFactoryBeanSupport implements ApplicationEventPublisherAware { - private static final String NO_NAMED_PARAMETER_JDBC_OPERATION_ERROR_MESSAGE = // - "No unique NamedParameterJdbcOperation could be found, " // - + "nor JdbcOperations or DataSource to construct one from."; - - private static final String NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME = "namedParameterJdbcTemplate"; - private static final String JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate"; - private static final String DATA_SOURCE_BEAN_NAME = "dataSource"; - private static final String NAMING_STRATEGY_BEAN_NAME = "namingStrategy"; - private static final String SQL_SESSION_FACTORY_BEAN_NAME = "sqlSessionFactory"; - private static final String CONVERSION_CUSTOMIZER_BEAN_NAME = "conversionCustomizer"; - - private final ApplicationEventPublisher applicationEventPublisher; - private final ApplicationContext applicationContext; - - JdbcRepositoryFactoryBean(Class repositoryInterface, ApplicationEventPublisher applicationEventPublisher, - ApplicationContext applicationContext) { + private ApplicationEventPublisher publisher; + private JdbcMappingContext mappingContext; + private DataAccessStrategy dataAccessStrategy; + JdbcRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); - this.applicationEventPublisher = applicationEventPublisher; - this.applicationContext = applicationContext; } @Override - protected RepositoryFactorySupport doCreateRepositoryFactory() { - - final JdbcMappingContext context = new JdbcMappingContext(findOrCreateNamingStrategy(), findOrCreateConversionCustomizer()); - - return new JdbcRepositoryFactory(applicationEventPublisher, context, createDataAccessStrategy(context)); + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + + super.setApplicationEventPublisher(publisher); + this.publisher = publisher; } /** - *

- * Create the {@link DataAccessStrategy}, by combining all applicable strategies into one. - *

- *

- * The challenge is that the {@link DefaultDataAccessStrategy} when used for reading needs a - * {@link DataAccessStrategy} for loading referenced entities (see. - * {@link DefaultDataAccessStrategy#getEntityRowMapper(Class)}. But it should use all configured - * {@link DataAccessStrategy}s for this. This creates a cyclic dependency. In order to build this the - * {@link DefaultDataAccessStrategy} gets passed in a {@link DelegatingDataAccessStrategy} which at the end gets set - * to the full {@link CascadingDataAccessStrategy}. - *

+ * Creates the actual {@link RepositoryFactorySupport} instance. + * + * @return */ - private CascadingDataAccessStrategy createDataAccessStrategy(JdbcMappingContext context) { - - DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); - - List accessStrategies = Stream.of( // - createMyBatisDataAccessStrategy(), // - createDefaultAccessStrategy(context, delegatingDataAccessStrategy) // - ) // - .filter(Optional::isPresent) // - .map(Optional::get) // - .collect(Collectors.toList()); - - CascadingDataAccessStrategy strategy = new CascadingDataAccessStrategy(accessStrategies); - delegatingDataAccessStrategy.setDelegate(strategy); - - return strategy; + @Override + protected RepositoryFactorySupport doCreateRepositoryFactory() { + return new JdbcRepositoryFactory(publisher, mappingContext, dataAccessStrategy); } - private Optional createMyBatisDataAccessStrategy() { + @Autowired + protected void setMappingContext(JdbcMappingContext mappingContext) { - String myBatisSqlSessionFactoryClassName = "org.apache.ibatis.session.SqlSessionFactory"; - ClassLoader classLoader = this.getClass().getClassLoader(); - - if (!ClassUtils.isPresent(myBatisSqlSessionFactoryClassName, classLoader)) { - return Optional.empty(); - } - - try { - - return getBean(classLoader.loadClass(myBatisSqlSessionFactoryClassName), SQL_SESSION_FACTORY_BEAN_NAME) - // note that the cast to SqlSessionFactory happens in a lambda, which is basically a separate class - // thus it won't get loaded if this code path doesn't get executed. - .map(ssf -> new MyBatisDataAccessStrategy((SqlSessionFactory) ssf)); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Detected MyBatis on classpath but failed to load the class " + myBatisSqlSessionFactoryClassName); - } + super.setMappingContext(mappingContext); + this.mappingContext = mappingContext; } - private Optional createDefaultAccessStrategy(JdbcMappingContext context, - DelegatingDataAccessStrategy delegatingDataAccessStrategy) { - - return Optional.of(new DefaultDataAccessStrategy(new SqlGeneratorSource(context), findOrCreateJdbcOperations(), - context, delegatingDataAccessStrategy)); + @Autowired + public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) { + this.dataAccessStrategy = dataAccessStrategy; } - private NamedParameterJdbcOperations findOrCreateJdbcOperations() { + @Override + public void afterPropertiesSet() { - return Optionals.firstNonEmpty( // - this::getNamedParameterJdbcOperations, // - () -> getJdbcOperations().map(NamedParameterJdbcTemplate::new), // - () -> getDataSource().map(NamedParameterJdbcTemplate::new)) // - .orElseThrow(() -> new IllegalStateException(NO_NAMED_PARAMETER_JDBC_OPERATION_ERROR_MESSAGE)); - } - - private NamingStrategy findOrCreateNamingStrategy() { - return getNamingStrategy().orElse(new DefaultNamingStrategy()); - } - - private ConversionCustomizer findOrCreateConversionCustomizer() { - return getConversionCustomizer().orElse(conversionService->{}); - } - - private Optional getNamedParameterJdbcOperations() { - return getBean(NamedParameterJdbcOperations.class, NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME); - } - - private Optional getJdbcOperations() { - return getBean(JdbcOperations.class, JDBC_OPERATIONS_BEAN_NAME); - } - - private Optional getDataSource() { - return getBean(DataSource.class, DATA_SOURCE_BEAN_NAME); - } - - private Optional getNamingStrategy() { - return getBean(NamingStrategy.class, NAMING_STRATEGY_BEAN_NAME); - } - - private Optional getConversionCustomizer() { - return getBean(ConversionCustomizer.class, CONVERSION_CUSTOMIZER_BEAN_NAME); - } - - private Optional getBean(Class type, String name) { - - Map beansOfType = applicationContext.getBeansOfType(type); - - if (beansOfType.size() == 1) { - return beansOfType.values().stream().findFirst(); - } - - return Optional.ofNullable(beansOfType.get(name)); + Assert.notNull(this.dataAccessStrategy, "DataAccessStrategy must not be null!"); + Assert.notNull(this.mappingContext, "MappingContext must not be null!"); + super.afterPropertiesSet(); } } diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java index babb18f2..683f06ea 100644 --- a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java @@ -29,9 +29,8 @@ import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.data.jdbc.repository.JdbcRepositoryIdGenerationIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; -import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.test.context.ContextConfiguration; @@ -43,6 +42,7 @@ import org.springframework.transaction.annotation.Transactional; * Tests the integration with Mybatis. * * @author Jens Schauder + * @author Greg Turnquist */ @ContextConfiguration @Transactional @@ -53,8 +53,6 @@ public class MyBatisHsqlIntegrationTests { @EnableJdbcRepositories(considerNestedRepositories = true) static class Config { - @Autowired JdbcRepositoryFactory factory; - @Bean Class testClass() { return MyBatisHsqlIntegrationTests.class; @@ -75,6 +73,11 @@ public class MyBatisHsqlIntegrationTests { return sqlSessionFactoryBean; } + + @Bean + MyBatisDataAccessStrategy dataAccessStrategy(SqlSessionFactory factory) { + return new MyBatisDataAccessStrategy(factory); + } } @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index d4892638..8b69f31d 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -26,16 +26,21 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; 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.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.repository.CrudRepository; +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.rules.SpringClassRule; @@ -61,6 +66,11 @@ public class JdbcRepositoryIdGenerationIntegrationTests { return JdbcRepositoryIdGenerationIntegrationTests.class; } + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } } @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @@ -122,7 +132,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { @Configuration @ComponentScan("org.springframework.data.jdbc.testing") @EnableJdbcRepositories(considerNestedRepositories = true) - public static class TestConfiguration { + static class TestConfiguration { @Bean Class testClass() { diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java index c84173d7..47aad67d 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java @@ -19,6 +19,7 @@ import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; import junit.framework.AssertionFailedError; + import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -31,18 +32,23 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.conversion.DbAction; import org.springframework.data.jdbc.mapping.event.BeforeDelete; import org.springframework.data.jdbc.mapping.event.BeforeSave; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.rules.SpringClassRule; import org.springframework.test.context.junit4.rules.SpringMethodRule; @@ -52,6 +58,7 @@ import org.springframework.test.context.junit4.rules.SpringMethodRule; * executed against the database. * * @author Jens Schauder + * @author Greg Turnquist */ @ContextConfiguration public class JdbcRepositoryManipulateDbActionsIntegrationTests { @@ -227,6 +234,11 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { }; } + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } } } 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 3fe18ad4..af534c2d 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 @@ -22,12 +22,16 @@ import lombok.Data; 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.data.annotation.Id; -import org.springframework.data.jdbc.repository.JdbcRepositoryIntegrationTests; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -35,6 +39,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; * Tests the {@link EnableJdbcRepositories} annotation. * * @author Jens Schauder + * @author Greg Turnquist */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = TestConfiguration.class) @@ -69,6 +74,12 @@ public class EnableJdbcRepositoriesIntegrationTests { Class testClass() { return EnableJdbcRepositoriesIntegrationTests.class; } - } + + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } + } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index de2c8556..4d479579 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -2,338 +2,101 @@ package org.springframework.data.jdbc.repository.support; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.test.util.ReflectionTestUtils.*; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Predicate; - -import javax.sql.DataSource; - -import org.apache.ibatis.session.SqlSessionFactory; -import org.assertj.core.api.Condition; +import org.junit.Before; import org.junit.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; -import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.instrument.classloading.ShadowingClassLoader; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.lang.Nullable; -import org.springframework.util.ReflectionUtils; /** * Tests the dependency injection for {@link JdbcRepositoryFactoryBean}. * * @author Jens Schauder + * @author Greg Turnquist */ +@RunWith(MockitoJUnitRunner.class) public class JdbcRepositoryFactoryBeanUnitTests { - static final String EXPECTED_JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate"; - static final String EXPECTED_NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME = "namedParameterJdbcTemplate"; + JdbcRepositoryFactoryBean factoryBean; - static final String ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY = "accessStrategy"; - static final String OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY = "operations"; - private static final String MAPPING_CONTEXT_FIELD_NAME_IN_FACTORY = "context"; + StubRepositoryFactorySupport factory; + @Mock ListableBeanFactory beanFactory; + @Mock Repository repository; + @Mock DataAccessStrategy dataAccessStrategy; + @Mock JdbcMappingContext mappingContext; - ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); - ApplicationContext context = mock(ApplicationContext.class); + @Before + public void setUp() { - Map dataSources = new HashMap<>(); - Map jdbcOperations = new HashMap<>(); - Map namedJdbcOperations = new HashMap<>(); - Map sqlSessionFactories = new HashMap<>(); + factory = Mockito.spy(new StubRepositoryFactorySupport(repository)); - Map conversionCustomizers = new HashMap<>(); - - { - - when(context.getBeansOfType(DataSource.class)).thenReturn(dataSources); - when(context.getBeansOfType(JdbcOperations.class)).thenReturn(jdbcOperations); - when(context.getBeansOfType(NamedParameterJdbcOperations.class)).thenReturn(namedJdbcOperations); - when(context.getBeansOfType(SqlSessionFactory.class)).thenReturn(sqlSessionFactories); - when(context.getBeansOfType(ConversionCustomizer.class)).thenReturn(conversionCustomizers); + // Setup standard configuration + factoryBean = new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class); } - @Test // DATAJDBC-100 - public void exceptionWithUsefulMessage() { + @Test + public void setsUpBasicInstanceCorrectly() { - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(factoryBean::doCreateRepositoryFactory); + factoryBean.setDataAccessStrategy(dataAccessStrategy); + factoryBean.setMappingContext(mappingContext); + factoryBean.afterPropertiesSet(); + assertThat(factoryBean.getObject()).isNotNull(); } - @Test // DATAJDBC-100 - public void singleDataSourceGetsUsedForCreatingRepositoryFactory() { + @Test(expected = IllegalArgumentException.class) + public void requiresListableBeanFactory() { - DataSource expectedDataSource = mock(DataSource.class); - dataSources.put("arbitraryName", expectedDataSource); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedDataSource)); + factoryBean.setBeanFactory(mock(BeanFactory.class)); } - @Test // DATAJDBC-100 - public void multipleDataSourcesGetDisambiguatedByName() { + /** + * required to trick Mockito on invoking protected getRepository(Class repositoryInterface, Optional + * customImplementation + */ + private static class StubRepositoryFactorySupport extends RepositoryFactorySupport { - DataSource expectedDataSource = mock(DataSource.class); - dataSources.put("dataSource", expectedDataSource); - dataSources.put("arbitraryName", mock(DataSource.class)); + private final Repository repository; - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedDataSource)); - } - - @Test // DATAJDBC-100 - public void singleJdbcOperationsUsedForCreatingRepositoryFactory() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put("arbitraryName", expectedOperations); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void multipleJdbcOperationsGetDisambiguatedByName() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put(EXPECTED_JDBC_OPERATIONS_BEAN_NAME, expectedOperations); - jdbcOperations.put("arbitraryName", mock(JdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void singleNamedJdbcOperationsUsedForCreatingRepositoryFactory() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void multipleNamedJdbcOperationsGetDisambiguatedByName() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put(EXPECTED_NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME, expectedOperations); - namedJdbcOperations.put("arbitraryName", mock(NamedParameterJdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void namedParameterJdbcOperationsTakePrecedenceOverDataSource() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - dataSources.put("arbitraryName", mock(DataSource.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void jdbcOperationsTakePrecedenceOverDataSource() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put("arbitraryName", expectedOperations); - dataSources.put("arbitraryName", mock(DataSource.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void namedParameterJdbcOperationsTakePrecedenceOverJdbcOperations() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - jdbcOperations.put("arbitraryName", mock(JdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-123 - public void withoutSqlSessionFactoryThereIsNoMyBatisIntegration() { - - dataSources.put("anyname", mock(DataSource.class)); - sqlSessionFactories.clear(); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNull(); - } - - @Test // DATAJDBC-123 - public void withSqlSessionFactoryThereIsMyBatisIntegration() { - - dataSources.put("anyname", mock(DataSource.class)); - sqlSessionFactories.put("anyname", mock(SqlSessionFactory.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNotNull(); - } - - @Test // DATAJDBC-136 - public void canBeLoadedWithoutMyBatis() throws Exception { - - String sqlSessionFactoryClassName = SqlSessionFactory.class.getName(); - - ShadowingClassLoader classLoader = new ShadowingClassLoader(this.getClass().getClassLoader()) { - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - - if (name.equals(sqlSessionFactoryClassName)) { - throw new ClassNotFoundException("%s is configured not to get loaded by this classloader"); - } - return super.loadClass(name); - } - }; - - Class loadedClass = classLoader.loadClass(JdbcRepositoryFactoryBean.class.getName()); - - assertThat(loadedClass).isNotNull(); - ReflectionUtils.getAllDeclaredMethods(loadedClass); - } - - @Test // DATAJDBC-147 - public void registersConversionsCorrectly() { - - dataSources.put("anyname", mock(DataSource.class)); - conversionCustomizers.put("anyname", cs -> { - - cs.addConverter(new Converter() { - - @Nullable - @Override - public Long convert(Duration duration) { - return duration.toHours(); - } - }); - }); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - JdbcMappingContext mappingContext = (JdbcMappingContext) getField(factory, MAPPING_CONTEXT_FIELD_NAME_IN_FACTORY); - ConversionService conversions = mappingContext.getConversions(); - - assertThat(conversions.convert(Duration.ofDays(3), Long.class)).isEqualTo(72L); - } - - private Condition using(NamedParameterJdbcOperations expectedOperations) { - - Predicate predicate = r -> extractNamedParameterJdbcOperations(r) == expectedOperations; - return new Condition<>(predicate, "uses " + expectedOperations); - } - - private NamedParameterJdbcOperations extractNamedParameterJdbcOperations(RepositoryFactorySupport r) { - - DefaultDataAccessStrategy defaultDataAccessStrategy = findDataAccessStrategy(r, DefaultDataAccessStrategy.class); - return (NamedParameterJdbcOperations) getField(defaultDataAccessStrategy, - OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY); - } - - private Condition using(JdbcOperations expectedOperations) { - - Predicate predicate = r -> extractNamedParameterJdbcOperations(r) - .getJdbcOperations() == expectedOperations; - - return new Condition<>(predicate, "uses " + expectedOperations); - } - - private Condition using(DataSource expectedDataSource) { - - Predicate predicate = r -> { - - NamedParameterJdbcOperations namedOperations = extractNamedParameterJdbcOperations(r); - JdbcTemplate jdbcOperations = (JdbcTemplate) namedOperations.getJdbcOperations(); - return jdbcOperations.getDataSource() == expectedDataSource; - }; - - return new Condition<>(predicate, "using " + expectedDataSource); - } - - private static T findDataAccessStrategy(RepositoryFactorySupport r, Class type) { - - DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY); - return findDataAccessStrategy(accessStrategy, type); - } - - private static T findDataAccessStrategy(DataAccessStrategy accessStrategy, - Class type) { - - if (type.isInstance(accessStrategy)) - return (T) accessStrategy; - - if (accessStrategy instanceof DelegatingDataAccessStrategy) { - return findDataAccessStrategy((DataAccessStrategy) getField(accessStrategy, "delegate"), type); + private StubRepositoryFactorySupport(Repository repository) { + this.repository = repository; } - if (accessStrategy instanceof CascadingDataAccessStrategy) { - List strategies = (List) getField(accessStrategy, "strategies"); - return strategies.stream() // - .map((DataAccessStrategy das) -> findDataAccessStrategy(das, type)) // - .filter(Objects::nonNull) // - .findFirst() // - .orElse(null); + @Override + public T getRepository(Class repositoryInterface, RepositoryComposition.RepositoryFragments fragments) { + return (T) repository; } - return null; - } + @Override + public EntityInformation getEntityInformation(Class domainClass) { + return null; + } + @Override + protected Object getTargetRepository(RepositoryInformation metadata) { + return null; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return null; + } + } + private static class DummyEntity { @Id private Long id; }