diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index 4ac16782..5b372bef 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -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. diff --git a/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java b/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java index ab540b1c..e3ef2338 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java +++ b/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java @@ -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 // ------------------------------------------------------------------------- diff --git a/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java b/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java index e58d7c63..ce0663ce 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java +++ b/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java @@ -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) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java index fb497064..b624f181 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java @@ -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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java index f1c79418..dbd818d9 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java @@ -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"); + } } /* diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index f7c50d6c..fba62156 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -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 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; } /* diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index f2303e18..a16770ae 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -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, 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, 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, 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, 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, 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()); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java index 3982a341..575cdca0 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java @@ -18,4 +18,4 @@ package org.springframework.data.r2dbc.repository.config; /** * @author Mark Paluch */ -class Person {} +public class Person {} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index 9fd5feb8..e7c21af7 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -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(); + } + } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/mysql/MySqlPersonRepository.java b/src/test/java/org/springframework/data/r2dbc/repository/config/mysql/MySqlPersonRepository.java new file mode 100644 index 00000000..c890479a --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/mysql/MySqlPersonRepository.java @@ -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 {} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/sqlserver/SqlServerPersonRepository.java b/src/test/java/org/springframework/data/r2dbc/repository/config/sqlserver/SqlServerPersonRepository.java new file mode 100644 index 00000000..94f5bb88 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/sqlserver/SqlServerPersonRepository.java @@ -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 {}