#406 - Allow configuring R2dbcEntityOperations bean reference in EnableR2dbcRepositories.

We now accept a bean reference to R2dbcEntityOperations so that a single application can scan for repositories that use different dialects/database systems.
This commit is contained in:
Mark Paluch
2020-07-23 10:31:54 +02:00
parent b605daa0e8
commit 0bd6f8a431
11 changed files with 293 additions and 32 deletions

View File

@@ -339,3 +339,42 @@ include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+
include::../{spring-data-commons-docs}/entity-callbacks.adoc[leveloffset=+1]
include::./r2dbc-entity-callbacks.adoc[leveloffset=+2]
[[r2dbc.multiple-databases]]
=== Working with multiple Databases
When working with multiple, potentially different databases, your application will require a different approach to configuration.
The provided `AbstractR2dbcConfiguration` support class assumes a single `ConnectionFactory` from which the `Dialect` gets derived.
That being said, you need to define a few beans yourself to configure Spring Data R2DBC to work with multiple databases.
R2DBC repositories require either a `DatabaseClient` and `ReactiveDataAccessStrategy` or `R2dbcEntityOperations` to implement repositories.
A simple configuration to scan for repositories without using `AbstractR2dbcConfiguration` looks like:
[source,java]
----
@Configuration
@EnableR2dbcRepositories(basePackages = "com.acme.mysql", entityOperationsRef = "mysqlR2dbcEntityOperations")
static class MySQLConfiguration {
@Bean
@Qualifier("mysql")
public ConnectionFactory mysqlConnectionFactory() {
return …;
}
@Bean
public R2dbcEntityOperations mysqlR2dbcEntityOperations(@Qualifier("mysql") ConnectionFactory connectionFactory) {
DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(MySqlDialect.INSTANCE);
DatabaseClient databaseClient = DatabaseClient.builder()
.connectionFactory(connectionFactory)
.dataAccessStrategy(strategy)
.build();
return new R2dbcEntityTemplate(databaseClient, strategy);
}
}
----
Note that `@EnableR2dbcRepositories` allows configuration either through `databaseClientRef` or `entityOperationsRef`.
Using various `DatabaseClient` beans is useful when connecting to multiple databases of the same type.
When using different database systems that differ in their dialect, use `@EnableR2dbcRepositories`(entityOperationsRef = …)` instead.

View File

@@ -42,6 +42,15 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
*/
DatabaseClient getDatabaseClient();
/**
* Expose the underlying {@link ReactiveDataAccessStrategy} encapsulating dialect specifics.
*
* @return the underlying {@link ReactiveDataAccessStrategy}.
* @see ReactiveDataAccessStrategy
* @since 1.1.3
*/
ReactiveDataAccessStrategy getDataAccessStrategy();
// -------------------------------------------------------------------------
// Methods dealing with org.springframework.data.r2dbc.query.Query
// -------------------------------------------------------------------------

View File

@@ -125,6 +125,15 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
return this.databaseClient;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getDataAccessStrategy()
*/
@Override
public ReactiveDataAccessStrategy getDataAccessStrategy() {
return this.dataAccessStrategy;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)

View File

@@ -121,9 +121,21 @@ public @interface EnableR2dbcRepositories {
* repositories detected.
*
* @return
* @see #entityOperationsRef()
*/
String databaseClientRef() default "r2dbcDatabaseClient";
/**
* Configures the name of the {@link org.springframework.data.r2dbc.core.R2dbcEntityOperations} bean to be used with
* the repositories detected. Used as alternative to {@link #databaseClientRef()} to configure an access strategy when
* using repositories with different database systems/dialects. If this attribute is set, then
* {@link #databaseClientRef()} is ignored.
*
* @return
* @since 1.1.3
*/
String entityOperationsRef() default "";
/**
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
* repositories infrastructure.

View File

@@ -29,6 +29,7 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.util.StringUtils;
/**
* Reactive {@link RepositoryConfigurationExtension} for R2DBC.
@@ -97,8 +98,13 @@ public class R2dbcRepositoryConfigurationExtension extends RepositoryConfigurati
AnnotationAttributes attributes = config.getAttributes();
builder.addPropertyReference("databaseClient", attributes.getString("databaseClientRef"));
builder.addPropertyReference("dataAccessStrategy", "reactiveDataAccessStrategy");
String entityOperationsRef = attributes.getString("entityOperationsRef");
if (StringUtils.hasText(entityOperationsRef)) {
builder.addPropertyReference("entityOperations", entityOperationsRef);
} else {
builder.addPropertyReference("databaseClient", attributes.getString("databaseClientRef"));
builder.addPropertyReference("dataAccessStrategy", "reactiveDataAccessStrategy");
}
}
/*

View File

@@ -22,6 +22,7 @@ import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
@@ -54,9 +55,9 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final DatabaseClient databaseClient;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
private final R2dbcConverter converter;
private final ReactiveDataAccessStrategy dataAccessStrategy;
/**
* Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}.
@@ -70,9 +71,25 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
Assert.notNull(dataAccessStrategy, "ReactiveDataAccessStrategy must not be null!");
this.databaseClient = databaseClient;
this.dataAccessStrategy = dataAccessStrategy;
this.converter = dataAccessStrategy.getConverter();
this.mappingContext = this.converter.getMappingContext();
}
/**
* Creates a new {@link R2dbcRepositoryFactory} given {@link R2dbcEntityOperations}.
*
* @param operations must not be {@literal null}.
* @since 1.1.3
*/
public R2dbcRepositoryFactory(R2dbcEntityOperations operations) {
Assert.notNull(operations, "R2dbcEntityOperations must not be null!");
this.databaseClient = operations.getDatabaseClient();
this.dataAccessStrategy = operations.getDataAccessStrategy();
this.converter = dataAccessStrategy.getConverter();
this.mappingContext = this.converter.getMappingContext();
this.dataAccessStrategy = dataAccessStrategy;
}
/*

View File

@@ -19,6 +19,7 @@ import java.io.Serializable;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
@@ -28,7 +29,8 @@ import org.springframework.util.Assert;
/**
* {@link org.springframework.beans.factory.FactoryBean} to create
* {@link org.springframework.data.r2dbc.repository.R2dbcRepository} instances.
* {@link org.springframework.data.r2dbc.repository.R2dbcRepository} instances. Can be either configured with
* {@link R2dbcEntityOperations} or {@link DatabaseClient} with {@link ReactiveDataAccessStrategy}.
*
* @author Mark Paluch
* @author Christoph Strobl
@@ -39,6 +41,7 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
private @Nullable DatabaseClient client;
private @Nullable ReactiveDataAccessStrategy dataAccessStrategy;
private @Nullable R2dbcEntityOperations operations;
private boolean mappingContextConfigured = false;
@@ -56,26 +59,31 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
*
* @param client the client to set
*/
public void setDatabaseClient(@Nullable DatabaseClient client) {
public void setDatabaseClient(DatabaseClient client) {
this.client = client;
}
public void setDataAccessStrategy(ReactiveDataAccessStrategy dataAccessStrategy) {
this.dataAccessStrategy = dataAccessStrategy;
}
/**
* @param operations
* @since 1.1.3
*/
public void setEntityOperations(R2dbcEntityOperations operations) {
this.operations = operations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext)
*/
@Override
protected void setMappingContext(@Nullable MappingContext<?, ?> mappingContext) {
protected void setMappingContext(MappingContext<?, ?> mappingContext) {
this.mappingContextConfigured = true;
super.setMappingContext(mappingContext);
if (mappingContext != null) {
this.mappingContextConfigured = true;
}
}
public void setDataAccessStrategy(@Nullable ReactiveDataAccessStrategy dataAccessStrategy) {
this.dataAccessStrategy = dataAccessStrategy;
}
/*
@@ -84,7 +92,9 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
*/
@Override
protected final RepositoryFactorySupport createRepositoryFactory() {
return getFactoryInstance(client, dataAccessStrategy);
return this.operations != null ? getFactoryInstance(this.operations)
: getFactoryInstance(this.client, this.dataAccessStrategy);
}
/**
@@ -99,6 +109,17 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
return new R2dbcRepositoryFactory(client, dataAccessStrategy);
}
/**
* Creates and initializes a {@link RepositoryFactorySupport} instance.
*
* @param operations must not be {@literal null}.
* @return new instance of {@link RepositoryFactorySupport}.
* @since 1.1.3
*/
protected RepositoryFactorySupport getFactoryInstance(R2dbcEntityOperations operations) {
return new R2dbcRepositoryFactory(operations);
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
@@ -106,8 +127,14 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
@Override
public void afterPropertiesSet() {
Assert.state(client != null, "DatabaseClient must not be null!");
Assert.state(dataAccessStrategy != null, "ReactiveDataAccessStrategy must not be null!");
if (operations == null) {
Assert.state(client != null, "DatabaseClient must not be null when R2dbcEntityOperations is not configured!");
Assert.state(dataAccessStrategy != null,
"ReactiveDataAccessStrategy must not be null when R2dbcEntityOperations is not configured!");
} else {
dataAccessStrategy = operations.getDataAccessStrategy();
}
if (!mappingContextConfigured) {
setMappingContext(dataAccessStrategy.getConverter().getMappingContext());

View File

@@ -18,4 +18,4 @@ package org.springframework.data.r2dbc.repository.config;
/**
* @author Mark Paluch
*/
class Person {}
public class Person {}

View File

@@ -15,34 +15,38 @@
*/
package org.springframework.data.r2dbc.repository.config;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.dialect.MySqlDialect;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.data.r2dbc.dialect.SqlServerDialect;
import org.springframework.data.r2dbc.repository.config.mysql.MySqlPersonRepository;
import org.springframework.data.r2dbc.repository.config.sqlserver.SqlServerPersonRepository;
/**
* Integration tests for {@link R2dbcRepositoriesRegistrar}.
*
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@ContextConfiguration
public class R2dbcRepositoriesRegistrarTests {
@Configuration
@EnableR2dbcRepositories(basePackages = "org.springframework.data.r2dbc.repository.config")
static class Config {
static class EnableWithDatabaseClient {
@Bean
public DatabaseClient r2dbcDatabaseClient() {
@@ -51,13 +55,103 @@ public class R2dbcRepositoriesRegistrarTests {
@Bean
public ReactiveDataAccessStrategy reactiveDataAccessStrategy() {
return new DefaultReactiveDataAccessStrategy(new PostgresDialect());
return new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE);
}
}
@Autowired PersonRepository personRepository;
@Autowired ApplicationContext context;
@Configuration
@EnableR2dbcRepositories(basePackages = "org.springframework.data.r2dbc.repository.config",
entityOperationsRef = "myEntityOperations")
static class EnableWithEntityOperations {
@Bean
public R2dbcEntityOperations myEntityOperations() {
return new R2dbcEntityTemplate(mock(DatabaseClient.class),
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
}
}
@Configuration
@EnableR2dbcRepositories(basePackages = "org.springframework.data.r2dbc.repository.config.mysql",
entityOperationsRef = "mysqlR2dbcEntityOperations")
static class MySQLConfiguration {
@Bean
@Qualifier("mysql")
public ConnectionFactory mysqlConnectionFactory() {
return mock(ConnectionFactory.class);
}
@Bean
public R2dbcEntityOperations mysqlR2dbcEntityOperations(@Qualifier("mysql") ConnectionFactory connectionFactory) {
DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(MySqlDialect.INSTANCE);
DatabaseClient databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(strategy).build();
return new R2dbcEntityTemplate(databaseClient, strategy);
}
}
@Configuration
@EnableR2dbcRepositories(basePackages = "org.springframework.data.r2dbc.repository.config.sqlserver",
entityOperationsRef = "sqlserverR2dbcEntityOperations")
static class SQLServerConfiguration {
@Bean
public ConnectionFactory sqlserverConnectionFactory() {
return mock(ConnectionFactory.class);
}
@Bean
public DatabaseClient sqlserverDatabaseClient(
@Qualifier("sqlserverConnectionFactory") ConnectionFactory connectionFactory,
@Qualifier("sqlserverDataAccessStrategy") ReactiveDataAccessStrategy mysqlDataAccessStrategy) {
return DatabaseClient.builder().connectionFactory(connectionFactory).dataAccessStrategy(mysqlDataAccessStrategy)
.build();
}
@Bean
public R2dbcEntityOperations sqlserverR2dbcEntityOperations(
@Qualifier("sqlserverDatabaseClient") DatabaseClient mysqlDatabaseClient,
@Qualifier("sqlserverDataAccessStrategy") ReactiveDataAccessStrategy mysqlDataAccessStrategy) {
return new R2dbcEntityTemplate(mysqlDatabaseClient, mysqlDataAccessStrategy);
}
@Bean
public ReactiveDataAccessStrategy sqlserverDataAccessStrategy() {
return new DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE);
}
}
@Test // gh-13
public void testConfiguration() {}
public void testConfigurationUsingDatabaseClient() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
EnableWithDatabaseClient.class)) {
assertThat(context.getBean(PersonRepository.class)).isNotNull();
}
}
@Test // gh-406
public void testConfigurationUsingEntityOperations() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
EnableWithEntityOperations.class)) {
assertThat(context.getBean(PersonRepository.class)).isNotNull();
}
}
@Test // gh-406
public void testMultipleDatabases() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySQLConfiguration.class,
SQLServerConfiguration.class)) {
assertThat(context.getBean(MySqlPersonRepository.class)).isNotNull();
assertThat(context.getBean(SqlServerPersonRepository.class)).isNotNull();
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2020 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
*
* https://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.r2dbc.repository.config.mysql;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.config.Person;
/**
* @author Mark Paluch
*/
public interface MySqlPersonRepository extends R2dbcRepository<Person, String> {}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2020 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
*
* https://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.r2dbc.repository.config.sqlserver;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.config.Person;
/**
* @author Mark Paluch
*/
public interface SqlServerPersonRepository extends R2dbcRepository<Person, String> {}