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:
committed by
Jens Schauder
parent
0b2708ff9a
commit
b044a13af1
@@ -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 {};
|
||||
|
||||
|
||||
@@ -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 -> {}));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user