Create spring-boot-data-r2dbc module

This commit is contained in:
Stéphane Nicoll
2025-03-28 17:17:27 +01:00
committed by Phillip Webb
parent cacaeacbe9
commit de2827e23f
20 changed files with 67 additions and 39 deletions

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.domain.EntityScanner;
import org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.dialect.DialectResolver;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
import org.springframework.data.relational.RelationalManagedTypes;
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
import org.springframework.data.relational.core.mapping.NamingStrategy;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.r2dbc.core.DatabaseClient;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link DatabaseClient}.
*
* @author Mark Paluch
* @author Oliver Drotbohm
* @since 4.0.0
*/
@AutoConfiguration(after = R2dbcAutoConfiguration.class)
@ConditionalOnClass({ DatabaseClient.class, R2dbcEntityTemplate.class })
@ConditionalOnSingleCandidate(DatabaseClient.class)
public class R2dbcDataAutoConfiguration {
private final DatabaseClient databaseClient;
private final R2dbcDialect dialect;
public R2dbcDataAutoConfiguration(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
this.dialect = DialectResolver.getDialect(this.databaseClient.getConnectionFactory());
}
@Bean
@ConditionalOnMissingBean
public R2dbcEntityTemplate r2dbcEntityTemplate(R2dbcConverter r2dbcConverter) {
return new R2dbcEntityTemplate(this.databaseClient, this.dialect, r2dbcConverter);
}
@Bean
@ConditionalOnMissingBean
static RelationalManagedTypes r2dbcManagedTypes(ApplicationContext applicationContext)
throws ClassNotFoundException {
return RelationalManagedTypes.fromIterable(new EntityScanner(applicationContext).scan(Table.class));
}
@Bean
@ConditionalOnMissingBean
public R2dbcMappingContext r2dbcMappingContext(ObjectProvider<NamingStrategy> namingStrategy,
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext(
namingStrategy.getIfAvailable(() -> DefaultNamingStrategy.INSTANCE));
relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder());
relationalMappingContext.setManagedTypes(r2dbcManagedTypes);
return relationalMappingContext;
}
@Bean
@ConditionalOnMissingBean
public MappingR2dbcConverter r2dbcConverter(R2dbcMappingContext mappingContext,
R2dbcCustomConversions r2dbcCustomConversions) {
return new MappingR2dbcConverter(mappingContext, r2dbcCustomConversions);
}
@Bean
@ConditionalOnMissingBean
public R2dbcCustomConversions r2dbcCustomConversions() {
List<Object> converters = new ArrayList<>(this.dialect.getConverters());
converters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
return new R2dbcCustomConversions(
CustomConversions.StoreConversions.of(this.dialect.getSimpleTypeHolder(), converters),
Collections.emptyList());
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.autoconfigure;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Import;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean;
import org.springframework.r2dbc.core.DatabaseClient;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data R2DBC Repositories.
*
* @author Mark Paluch
* @since 4.0.0
* @see EnableR2dbcRepositories
*/
@AutoConfiguration(after = R2dbcDataAutoConfiguration.class)
@ConditionalOnClass({ ConnectionFactory.class, R2dbcRepository.class })
@ConditionalOnBean(DatabaseClient.class)
@ConditionalOnBooleanProperty(name = "spring.data.r2dbc.repositories.enabled", matchIfMissing = true)
@ConditionalOnMissingBean(R2dbcRepositoryFactoryBean.class)
@Import(R2dbcRepositoriesAutoConfigureRegistrar.class)
public class R2dbcRepositoriesAutoConfiguration {
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.autoconfigure;
import java.lang.annotation.Annotation;
import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.data.r2dbc.repository.config.R2dbcRepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
/**
* {@link ImportBeanDefinitionRegistrar} used to auto-configure Spring Data R2DBC
* Repositories.
*
* @author Mark Paluch
*/
class R2dbcRepositoriesAutoConfigureRegistrar extends AbstractRepositoryConfigurationSourceSupport {
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableR2dbcRepositories.class;
}
@Override
protected Class<?> getConfiguration() {
return EnableR2dbcRepositoriesConfiguration.class;
}
@Override
protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() {
return new R2dbcRepositoryConfigurationExtension();
}
@EnableR2dbcRepositories
private static final class EnableR2dbcRepositoriesConfiguration {
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-Configuration for Spring Data R2DBC.
*/
package org.springframework.boot.data.r2dbc.autoconfigure;

View File

@@ -0,0 +1,11 @@
{
"groups": [],
"properties": [
{
"name": "spring.data.r2dbc.repositories.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable R2DBC repositories.",
"defaultValue": true
}
]
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.data.r2dbc.autoconfigure.R2dbcDataAutoConfiguration
org.springframework.boot.data.r2dbc.autoconfigure.R2dbcRepositoriesAutoConfiguration

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.data.r2dbc.domain.city.City;
import org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link R2dbcDataAutoConfiguration}.
*
* @author Mark Paluch
*/
class R2dbcDataAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class));
@Test
void r2dbcEntityTemplateIsConfigured() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(R2dbcEntityTemplate.class));
}
@Test
void entityScanShouldSetManagedTypes() {
this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> {
R2dbcMappingContext mappingContext = context.getBean(R2dbcMappingContext.class);
ManagedTypes managedTypes = (ManagedTypes) ReflectionTestUtils.getField(mappingContext, "managedTypes");
assertThat(managedTypes.toList()).containsOnly(City.class);
});
}
@TestAutoConfigurationPackage(City.class)
static class TestConfiguration {
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.autoconfigure;
import java.time.Duration;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.data.r2dbc.domain.city.City;
import org.springframework.boot.data.r2dbc.domain.city.CityRepository;
import org.springframework.boot.data.r2dbc.domain.empty.EmptyDataPackage;
import org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.data.r2dbc.repository.config.R2dbcRepositoryConfigurationExtension;
import org.springframework.data.repository.Repository;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
import org.springframework.r2dbc.core.DatabaseClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link R2dbcRepositoriesAutoConfiguration}.
*
* @author Mark Paluch
*/
@WithResource(name = "schema.sql", content = """
CREATE TABLE CITY (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(30),
state VARCHAR(30),
country VARCHAR(30),
map VARCHAR(30)
);
""")
@WithResource(name = "data.sql",
content = "INSERT INTO CITY (ID, NAME, STATE, COUNTRY, MAP) values (2000, 'Washington', 'DC', 'US', 'Google');")
class R2dbcRepositoriesAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(R2dbcRepositoriesAutoConfiguration.class));
@Test
void backsOffWithNoConnectionFactory() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(R2dbcRepositoryConfigurationExtension.class));
}
@Test
void backsOffWithNoDatabaseClientOperations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader("org.springframework.r2dbc"))
.withUserConfiguration(TestConfiguration.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(DatabaseClient.class);
assertThat(context).doesNotHaveBean(R2dbcRepositoryConfigurationExtension.class);
});
}
@Test
void basicAutoConfiguration() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class))
.withUserConfiguration(DatabaseInitializationConfiguration.class, TestConfiguration.class)
.withPropertyValues("spring.r2dbc.generate-unique-name:true")
.run((context) -> {
assertThat(context).hasSingleBean(CityRepository.class);
context.getBean(CityRepository.class)
.findById(2000L)
.as(StepVerifier::create)
.expectNextCount(1)
.expectComplete()
.verify(Duration.ofSeconds(30));
});
}
@Test
void autoConfigurationWithNoRepositories() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withUserConfiguration(EmptyConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(Repository.class));
}
@Test
void honorsUsersEnableR2dbcRepositoriesConfiguration() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class))
.withUserConfiguration(DatabaseInitializationConfiguration.class, EnableRepositoriesConfiguration.class)
.withPropertyValues("spring.r2dbc.generate-unique-name:true")
.run((context) -> {
assertThat(context).hasSingleBean(CityRepository.class);
context.getBean(CityRepository.class)
.findById(2000L)
.as(StepVerifier::create)
.expectNextCount(1)
.expectComplete()
.verify(Duration.ofSeconds(30));
});
}
@Configuration(proxyBeanMethods = false)
static class DatabaseInitializationConfiguration {
@Autowired
void initializeDatabase(ConnectionFactory connectionFactory) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource[] scripts = new Resource[] { resourceLoader.getResource("classpath:schema.sql"),
resourceLoader.getResource("classpath:data.sql") };
new ResourceDatabasePopulator(scripts).populate(connectionFactory).block();
}
}
@Configuration(proxyBeanMethods = false)
@TestAutoConfigurationPackage(City.class)
static class TestConfiguration {
}
@Configuration(proxyBeanMethods = false)
@TestAutoConfigurationPackage(EmptyDataPackage.class)
static class EmptyConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableR2dbcRepositories(basePackageClasses = City.class)
static class EnableRepositoriesConfiguration {
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.domain.city;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table("CITY")
public class City {
@Id
private Long id;
private String name;
private String state;
private String country;
private String map;
protected City() {
}
public City(String name, String country) {
this.name = name;
this.country = country;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
public String getMap() {
return this.map;
}
@Override
public String toString() {
return getName() + "," + getState() + "," + getCountry();
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.domain.city;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface CityRepository extends ReactiveCrudRepository<City, Long> {
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2025 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.boot.data.r2dbc.domain.empty;
public class EmptyDataPackage {
}