DATAJDBC-151 - Reform Spring Data JDBC to stop autoconfiguration

In the past, Spring Data JDBC performed autoconfiguration such as gleaning whether or not MyBatis is on the classpath, and also whether or not certain other beans exist. This commit removes such flexible settings and instead wires up a JdbcMappingContext seeking an optional NamingStrategy and optional ConversionCustomizer. The other required beans will alert the end user if they don't exist.

All relevant test cases are updated to inject the proper components.

All autoconfiguration is being moved outside Spring Data JDBC, to eventually join Spring Boot after being shook out as an independent module.
This commit is contained in:
Greg Turnquist
2017-11-16 12:48:03 -06:00
committed by Jens Schauder
parent 0b2708ff9a
commit b044a13af1
8 changed files with 179 additions and 446 deletions

View File

@@ -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 {};

View File

@@ -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> namingStrategy,
Optional<ConversionCustomizer> conversionCustomizer) {
return new JdbcMappingContext(
namingStrategy.orElse(new DefaultNamingStrategy()),
conversionCustomizer.orElse(conversionService -> {}));
}
}

View File

@@ -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<T extends Repository<S, ID>, S, ID extends Serializable> //
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> 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<? extends T> repositoryInterface, ApplicationEventPublisher applicationEventPublisher,
ApplicationContext applicationContext) {
private ApplicationEventPublisher publisher;
private JdbcMappingContext mappingContext;
private DataAccessStrategy dataAccessStrategy;
JdbcRepositoryFactoryBean(Class<? extends T> 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;
}
/**
* <p>
* Create the {@link DataAccessStrategy}, by combining all applicable strategies into one.
* </p>
* <p>
* 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}.
* </p>
* Creates the actual {@link RepositoryFactorySupport} instance.
*
* @return
*/
private CascadingDataAccessStrategy createDataAccessStrategy(JdbcMappingContext context) {
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
List<DataAccessStrategy> 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<DataAccessStrategy> 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<DataAccessStrategy> 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<NamedParameterJdbcOperations> getNamedParameterJdbcOperations() {
return getBean(NamedParameterJdbcOperations.class, NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME);
}
private Optional<JdbcOperations> getJdbcOperations() {
return getBean(JdbcOperations.class, JDBC_OPERATIONS_BEAN_NAME);
}
private Optional<DataSource> getDataSource() {
return getBean(DataSource.class, DATA_SOURCE_BEAN_NAME);
}
private Optional<NamingStrategy> getNamingStrategy() {
return getBean(NamingStrategy.class, NAMING_STRATEGY_BEAN_NAME);
}
private Optional<ConversionCustomizer> getConversionCustomizer() {
return getBean(ConversionCustomizer.class, CONVERSION_CUSTOMIZER_BEAN_NAME);
}
private <R> Optional<R> getBean(Class<R> type, String name) {
Map<String, R> 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();
}
}

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<DummyEntityRepository, DummyEntity, Long> 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<String, DataSource> dataSources = new HashMap<>();
Map<String, JdbcOperations> jdbcOperations = new HashMap<>();
Map<String, NamedParameterJdbcOperations> namedJdbcOperations = new HashMap<>();
Map<String, SqlSessionFactory> sqlSessionFactories = new HashMap<>();
factory = Mockito.spy(new StubRepositoryFactorySupport(repository));
Map<String, ConversionCustomizer> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<T> repositoryInterface, Optional<Object>
* 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<Duration, Long>() {
@Nullable
@Override
public Long convert(Duration duration) {
return duration.toHours();
}
});
});
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> 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<? super RepositoryFactorySupport> using(NamedParameterJdbcOperations expectedOperations) {
Predicate<RepositoryFactorySupport> 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<? super RepositoryFactorySupport> using(JdbcOperations expectedOperations) {
Predicate<RepositoryFactorySupport> predicate = r -> extractNamedParameterJdbcOperations(r)
.getJdbcOperations() == expectedOperations;
return new Condition<>(predicate, "uses " + expectedOperations);
}
private Condition<? super RepositoryFactorySupport> using(DataSource expectedDataSource) {
Predicate<RepositoryFactorySupport> predicate = r -> {
NamedParameterJdbcOperations namedOperations = extractNamedParameterJdbcOperations(r);
JdbcTemplate jdbcOperations = (JdbcTemplate) namedOperations.getJdbcOperations();
return jdbcOperations.getDataSource() == expectedDataSource;
};
return new Condition<>(predicate, "using " + expectedDataSource);
}
private static <T extends DataAccessStrategy> T findDataAccessStrategy(RepositoryFactorySupport r, Class<T> type) {
DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY);
return findDataAccessStrategy(accessStrategy, type);
}
private static <T extends DataAccessStrategy> T findDataAccessStrategy(DataAccessStrategy accessStrategy,
Class<T> 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<DataAccessStrategy> strategies = (List<DataAccessStrategy>) getField(accessStrategy, "strategies");
return strategies.stream() //
.map((DataAccessStrategy das) -> findDataAccessStrategy(das, type)) //
.filter(Objects::nonNull) //
.findFirst() //
.orElse(null);
@Override
public <T> T getRepository(Class<T> repositoryInterface, RepositoryComposition.RepositoryFragments fragments) {
return (T) repository;
}
return null;
}
@Override
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> 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;
}