Skip to content

Commit 70eca3a

Browse files
committed
Add support for R2DBC RelationalManagedTypes.
See #1279
1 parent 58bb158 commit 70eca3a

8 files changed

+161
-12
lines changed

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@
1818
import io.r2dbc.spi.ConnectionFactory;
1919

2020
import java.util.ArrayList;
21+
import java.util.Collection;
2122
import java.util.Collections;
23+
import java.util.HashSet;
2224
import java.util.List;
2325
import java.util.Optional;
26+
import java.util.Set;
2427

2528
import org.springframework.beans.BeansException;
29+
import org.springframework.beans.factory.config.BeanDefinition;
2630
import org.springframework.context.ApplicationContext;
2731
import org.springframework.context.ApplicationContextAware;
2832
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2934
import org.springframework.context.annotation.Configuration;
3035
import org.springframework.core.convert.converter.Converter;
36+
import org.springframework.core.type.filter.AnnotationTypeFilter;
3137
import org.springframework.data.convert.CustomConversions;
3238
import org.springframework.data.convert.CustomConversions.StoreConversions;
3339
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
@@ -39,11 +45,15 @@
3945
import org.springframework.data.r2dbc.dialect.DialectResolver;
4046
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
4147
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
48+
import org.springframework.data.relational.RelationalManagedTypes;
4249
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
4350
import org.springframework.data.relational.core.mapping.NamingStrategy;
51+
import org.springframework.data.relational.core.mapping.Table;
4452
import org.springframework.lang.Nullable;
4553
import org.springframework.r2dbc.core.DatabaseClient;
4654
import org.springframework.util.Assert;
55+
import org.springframework.util.ClassUtils;
56+
import org.springframework.util.StringUtils;
4757

4858
/**
4959
* Base class for Spring Data R2DBC configuration containing bean declarations that must be registered for Spring Data
@@ -78,14 +88,43 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
7888
*/
7989
public abstract ConnectionFactory connectionFactory();
8090

91+
/**
92+
* Returns the base packages to scan for R2DBC mapped entities at startup. Returns the package name of the
93+
* configuration class' (the concrete class, not this one here) by default. So if you have a
94+
* {@code com.acme.AppConfig} extending {@link AbstractR2dbcConfiguration} the base package will be considered
95+
* {@code com.acme} unless the method is overridden to implement alternate behavior.
96+
*
97+
* @return the base packages to scan for mapped {@link Table} classes or an empty collection to not enable scanning
98+
* for entities.
99+
* @since 3.0
100+
*/
101+
protected Collection<String> getMappingBasePackages() {
102+
103+
Package mappingBasePackage = getClass().getPackage();
104+
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
105+
}
106+
107+
/**
108+
* Returns the a {@link RelationalManagedTypes} object holding the initial entity set.
109+
*
110+
* @return new instance of {@link RelationalManagedTypes}.
111+
* @throws ClassNotFoundException
112+
* @since 3.0
113+
*/
114+
@Bean
115+
public RelationalManagedTypes r2dbcManagedTypes() throws ClassNotFoundException {
116+
return RelationalManagedTypes.fromIterable(getInitialEntitySet());
117+
}
118+
81119
/**
82120
* Return a {@link R2dbcDialect} for the given {@link ConnectionFactory}. This method attempts to resolve a
83121
* {@link R2dbcDialect} from {@link io.r2dbc.spi.ConnectionFactoryMetadata}. Override this method to specify a dialect
84122
* instead of attempting to resolve one.
85123
*
86124
* @param connectionFactory the configured {@link ConnectionFactory}.
87125
* @return the resolved {@link R2dbcDialect}.
88-
* @throws org.springframework.data.r2dbc.dialect.DialectResolver.NoDialectException if the {@link R2dbcDialect} cannot be determined.
126+
* @throws org.springframework.data.r2dbc.dialect.DialectResolver.NoDialectException if the {@link R2dbcDialect}
127+
* cannot be determined.
89128
*/
90129
public R2dbcDialect getDialect(ConnectionFactory connectionFactory) {
91130
return DialectResolver.getDialect(connectionFactory);
@@ -131,17 +170,20 @@ public R2dbcEntityTemplate r2dbcEntityTemplate(DatabaseClient databaseClient,
131170
*
132171
* @param namingStrategy optional {@link NamingStrategy}. Use {@link NamingStrategy#INSTANCE} as fallback.
133172
* @param r2dbcCustomConversions customized R2DBC conversions.
173+
* @param r2dbcManagedTypes R2DBC managed types, typically discovered through {@link #r2dbcManagedTypes() an entity
174+
* scan}.
134175
* @return must not be {@literal null}.
135176
* @throws IllegalArgumentException if any of the required args is {@literal null}.
136177
*/
137178
@Bean
138179
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
139-
R2dbcCustomConversions r2dbcCustomConversions) {
180+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
140181

141182
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
142183

143184
R2dbcMappingContext context = new R2dbcMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE));
144185
context.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder());
186+
context.setManagedTypes(r2dbcManagedTypes);
145187

146188
return context;
147189
}
@@ -239,4 +281,56 @@ ConnectionFactory lookupConnectionFactory() {
239281

240282
return connectionFactory();
241283
}
284+
285+
/**
286+
* Scans the mapping base package for classes annotated with {@link Table}. By default, it scans for entities in all
287+
* packages returned by {@link #getMappingBasePackages()}.
288+
*
289+
* @see #getMappingBasePackages()
290+
* @return
291+
* @throws ClassNotFoundException
292+
* @since 3.0
293+
*/
294+
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
295+
296+
Set<Class<?>> initialEntitySet = new HashSet<>();
297+
298+
for (String basePackage : getMappingBasePackages()) {
299+
initialEntitySet.addAll(scanForEntities(basePackage));
300+
}
301+
302+
return initialEntitySet;
303+
}
304+
305+
/**
306+
* Scans the given base package for entities, i.e. R2DBC-specific types annotated with {@link Table}.
307+
*
308+
* @param basePackage must not be {@literal null}.
309+
* @return
310+
* @throws ClassNotFoundException
311+
* @since 3.0
312+
*/
313+
protected Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
314+
315+
if (!StringUtils.hasText(basePackage)) {
316+
return Collections.emptySet();
317+
}
318+
319+
Set<Class<?>> initialEntitySet = new HashSet<>();
320+
321+
if (StringUtils.hasText(basePackage)) {
322+
323+
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
324+
false);
325+
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Table.class));
326+
327+
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
328+
329+
initialEntitySet
330+
.add(ClassUtils.forName(candidate.getBeanClassName(), AbstractR2dbcConfiguration.class.getClassLoader()));
331+
}
332+
}
333+
334+
return initialEntitySet;
335+
}
242336
}

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2828
import org.springframework.context.annotation.Bean;
2929
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
31+
import org.springframework.data.relational.RelationalManagedTypes;
3032
import org.springframework.r2dbc.core.DatabaseClient;
33+
import org.springframework.test.util.ReflectionTestUtils;
3134

3235
/**
3336
* Tests for {@link AbstractR2dbcConfiguration}.
@@ -88,6 +91,22 @@ void shouldRegisterConnectionFactory() {
8891
context.stop();
8992
}
9093

94+
@Test // GH-1279
95+
void shouldScanForInitialEntities() {
96+
97+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
98+
CustomConnectionFactoryBeanNameConfiguration.class);
99+
100+
R2dbcMappingContext mappingContext = context.getBean(R2dbcMappingContext.class);
101+
102+
RelationalManagedTypes managedTypes = (RelationalManagedTypes) ReflectionTestUtils.getField(mappingContext,
103+
"managedTypes");
104+
105+
assertThat(managedTypes.toList()).contains(H2IntegrationTests.LegoSet.class, TopLevelEntity.class);
106+
107+
context.stop();
108+
}
109+
91110
@Configuration(proxyBeanMethods = false)
92111
static class NonBeanConnectionFactoryConfiguration extends AbstractR2dbcConfiguration {
93112

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.config;
17+
18+
import org.springframework.data.relational.core.mapping.Table;
19+
20+
/**
21+
* Empty test entity annotated with {@code @Table}.
22+
*
23+
* @author Mark Paluch
24+
*/
25+
@Table
26+
class TopLevelEntity {}

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryWithMixedCaseNamesIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
3232
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
3333
import org.springframework.data.r2dbc.testing.H2TestSupport;
34+
import org.springframework.data.relational.RelationalManagedTypes;
3435
import org.springframework.data.relational.core.mapping.NamingStrategy;
3536
import org.springframework.test.context.ContextConfiguration;
3637
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -59,9 +60,10 @@ public ConnectionFactory connectionFactory() {
5960

6061
@Override
6162
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
62-
R2dbcCustomConversions r2dbcCustomConversions) {
63+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
6364

64-
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions);
65+
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions,
66+
r2dbcManagedTypes);
6567
r2dbcMappingContext.setForceQuote(true);
6668

6769
return r2dbcMappingContext;

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryWithMixedCaseNamesIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
3434
import org.springframework.data.r2dbc.testing.ExternalDatabase;
3535
import org.springframework.data.r2dbc.testing.MariaDbTestSupport;
36+
import org.springframework.data.relational.RelationalManagedTypes;
3637
import org.springframework.data.relational.core.mapping.NamingStrategy;
3738
import org.springframework.test.context.ContextConfiguration;
3839
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -63,9 +64,10 @@ public ConnectionFactory connectionFactory() {
6364

6465
@Override
6566
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
66-
R2dbcCustomConversions r2dbcCustomConversions) {
67+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
6768

68-
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions);
69+
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions,
70+
r2dbcManagedTypes);
6971
r2dbcMappingContext.setForceQuote(true);
7072

7173
return r2dbcMappingContext;

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/OracleR2dbcRepositoryWithMixedCaseNamesIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.data.r2dbc.testing.EnabledOnClass;
3636
import org.springframework.data.r2dbc.testing.ExternalDatabase;
3737
import org.springframework.data.r2dbc.testing.OracleTestSupport;
38+
import org.springframework.data.relational.RelationalManagedTypes;
3839
import org.springframework.data.relational.core.mapping.NamingStrategy;
3940
import org.springframework.test.context.ContextConfiguration;
4041
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -66,9 +67,10 @@ public ConnectionFactory connectionFactory() {
6667

6768
@Override
6869
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
69-
R2dbcCustomConversions r2dbcCustomConversions) {
70+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
7071

71-
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions);
72+
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions,
73+
r2dbcManagedTypes);
7274
r2dbcMappingContext.setForceQuote(true);
7375

7476
return r2dbcMappingContext;

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryWithMixedCaseNamesIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
3535
import org.springframework.data.r2dbc.testing.ExternalDatabase;
3636
import org.springframework.data.r2dbc.testing.PostgresTestSupport;
37+
import org.springframework.data.relational.RelationalManagedTypes;
3738
import org.springframework.data.relational.core.mapping.NamingStrategy;
3839
import org.springframework.test.context.ContextConfiguration;
3940
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -64,9 +65,10 @@ public ConnectionFactory connectionFactory() {
6465

6566
@Override
6667
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
67-
R2dbcCustomConversions r2dbcCustomConversions) {
68+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
6869

69-
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions);
70+
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions,
71+
r2dbcManagedTypes);
7072
r2dbcMappingContext.setForceQuote(true);
7173

7274
return r2dbcMappingContext;

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryWithMixedCaseNamesIntegrationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
3535
import org.springframework.data.r2dbc.testing.ExternalDatabase;
3636
import org.springframework.data.r2dbc.testing.SqlServerTestSupport;
37+
import org.springframework.data.relational.RelationalManagedTypes;
3738
import org.springframework.data.relational.core.mapping.NamingStrategy;
3839
import org.springframework.test.context.ContextConfiguration;
3940
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -64,9 +65,10 @@ public ConnectionFactory connectionFactory() {
6465

6566
@Override
6667
public R2dbcMappingContext r2dbcMappingContext(Optional<NamingStrategy> namingStrategy,
67-
R2dbcCustomConversions r2dbcCustomConversions) {
68+
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
6869

69-
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions);
70+
R2dbcMappingContext r2dbcMappingContext = super.r2dbcMappingContext(namingStrategy, r2dbcCustomConversions,
71+
r2dbcManagedTypes);
7072
r2dbcMappingContext.setForceQuote(true);
7173

7274
return r2dbcMappingContext;

0 commit comments

Comments
 (0)