Skip to content

Commit 58bb158

Browse files
committed
Add support for RelationalManagedTypes.
See #1269
1 parent 59a1761 commit 58bb158

File tree

7 files changed

+272
-5
lines changed

7 files changed

+272
-5
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java

+95-2
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,43 @@
1616
package org.springframework.data.jdbc.repository.config;
1717

1818
import java.util.ArrayList;
19+
import java.util.Collection;
1920
import java.util.Collections;
21+
import java.util.HashSet;
2022
import java.util.List;
2123
import java.util.Optional;
24+
import java.util.Set;
2225

2326
import org.apache.commons.logging.Log;
2427
import org.apache.commons.logging.LogFactory;
28+
2529
import org.springframework.beans.BeansException;
2630
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
31+
import org.springframework.beans.factory.config.BeanDefinition;
2732
import org.springframework.context.ApplicationContext;
2833
import org.springframework.context.ApplicationContextAware;
2934
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
3036
import org.springframework.context.annotation.Configuration;
3137
import org.springframework.context.annotation.Lazy;
3238
import org.springframework.core.convert.converter.Converter;
39+
import org.springframework.core.type.filter.AnnotationTypeFilter;
3340
import org.springframework.data.convert.CustomConversions;
3441
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
3542
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
3643
import org.springframework.data.jdbc.core.convert.*;
37-
import org.springframework.data.jdbc.core.convert.JdbcArrayColumns;
3844
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
3945
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
4046
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
4147
import org.springframework.data.mapping.model.SimpleTypeHolder;
48+
import org.springframework.data.relational.RelationalManagedTypes;
4249
import org.springframework.data.relational.core.conversion.RelationalConverter;
4350
import org.springframework.data.relational.core.dialect.Dialect;
4451
import org.springframework.data.relational.core.mapping.NamingStrategy;
52+
import org.springframework.data.relational.core.mapping.Table;
4553
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
54+
import org.springframework.util.ClassUtils;
55+
import org.springframework.util.StringUtils;
4656

4757
/**
4858
* Beans that must be registered for Spring Data JDBC to work.
@@ -63,19 +73,50 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware {
6373

6474
private ApplicationContext applicationContext;
6575

76+
/**
77+
* Returns the base packages to scan for JDBC mapped entities at startup. Returns the package name of the
78+
* configuration class' (the concrete class, not this one here) by default. So if you have a
79+
* {@code com.acme.AppConfig} extending {@link AbstractJdbcConfiguration} the base package will be considered
80+
* {@code com.acme} unless the method is overridden to implement alternate behavior.
81+
*
82+
* @return the base packages to scan for mapped {@link Table} classes or an empty collection to not enable scanning
83+
* for entities.
84+
* @since 3.0
85+
*/
86+
protected Collection<String> getMappingBasePackages() {
87+
88+
Package mappingBasePackage = getClass().getPackage();
89+
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
90+
}
91+
92+
/**
93+
* Returns the a {@link RelationalManagedTypes} object holding the initial entity set.
94+
*
95+
* @return new instance of {@link RelationalManagedTypes}.
96+
* @throws ClassNotFoundException
97+
* @since 3.0
98+
*/
99+
@Bean
100+
public RelationalManagedTypes jdbcManagedTypes() throws ClassNotFoundException {
101+
return RelationalManagedTypes.fromIterable(getInitialEntitySet());
102+
}
103+
66104
/**
67105
* Register a {@link JdbcMappingContext} and apply an optional {@link NamingStrategy}.
68106
*
69107
* @param namingStrategy optional {@link NamingStrategy}. Use {@link NamingStrategy#INSTANCE} as fallback.
70108
* @param customConversions see {@link #jdbcCustomConversions()}.
109+
* @param jdbcManagedTypes JDBC managed types, typically discovered through {@link #jdbcManagedTypes() an entity
110+
* scan}.
71111
* @return must not be {@literal null}.
72112
*/
73113
@Bean
74114
public JdbcMappingContext jdbcMappingContext(Optional<NamingStrategy> namingStrategy,
75-
JdbcCustomConversions customConversions) {
115+
JdbcCustomConversions customConversions, RelationalManagedTypes jdbcManagedTypes) {
76116

77117
JdbcMappingContext mappingContext = new JdbcMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE));
78118
mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
119+
mappingContext.setManagedTypes(jdbcManagedTypes);
79120

80121
return mappingContext;
81122
}
@@ -190,4 +231,56 @@ public Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
190231
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
191232
this.applicationContext = applicationContext;
192233
}
234+
235+
/**
236+
* Scans the mapping base package for classes annotated with {@link Table}. By default, it scans for entities in all
237+
* packages returned by {@link #getMappingBasePackages()}.
238+
*
239+
* @see #getMappingBasePackages()
240+
* @return
241+
* @throws ClassNotFoundException
242+
* @since 3.0
243+
*/
244+
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
245+
246+
Set<Class<?>> initialEntitySet = new HashSet<>();
247+
248+
for (String basePackage : getMappingBasePackages()) {
249+
initialEntitySet.addAll(scanForEntities(basePackage));
250+
}
251+
252+
return initialEntitySet;
253+
}
254+
255+
/**
256+
* Scans the given base package for entities, i.e. JDBC-specific types annotated with {@link Table}.
257+
*
258+
* @param basePackage must not be {@literal null}.
259+
* @return
260+
* @throws ClassNotFoundException
261+
* @since 3.0
262+
*/
263+
protected Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
264+
265+
if (!StringUtils.hasText(basePackage)) {
266+
return Collections.emptySet();
267+
}
268+
269+
Set<Class<?>> initialEntitySet = new HashSet<>();
270+
271+
if (StringUtils.hasText(basePackage)) {
272+
273+
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
274+
false);
275+
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Table.class));
276+
277+
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
278+
279+
initialEntitySet
280+
.add(ClassUtils.forName(candidate.getBeanClassName(), AbstractJdbcConfiguration.class.getClassLoader()));
281+
}
282+
}
283+
284+
return initialEntitySet;
285+
}
193286
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,20 +37,23 @@
3737
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3838
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
3939
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
40+
import org.springframework.data.relational.RelationalManagedTypes;
4041
import org.springframework.data.relational.core.dialect.Dialect;
4142
import org.springframework.data.relational.core.dialect.LimitClause;
4243
import org.springframework.data.relational.core.dialect.LockClause;
4344
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
4445
import org.springframework.jdbc.core.JdbcOperations;
4546
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4647
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
48+
import org.springframework.test.util.ReflectionTestUtils;
4749

4850
/**
4951
* Integration tests for {@link AbstractJdbcConfiguration}.
5052
*
5153
* @author Oliver Drotbohm
54+
* @author Mark Paluch
5255
*/
53-
public class AbstractJdbcConfigurationIntegrationTests {
56+
class AbstractJdbcConfigurationIntegrationTests {
5457

5558
@Test // DATAJDBC-395
5659
void configuresInfrastructureComponents() {
@@ -97,7 +100,22 @@ void userProvidedConversionsOverwriteDialectSpecificConversions() {
97100
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
98101
}
99102

100-
protected static void assertApplicationContext(Consumer<ConfigurableApplicationContext> verification,
103+
@Test // GH-1269
104+
void detectsInitialEntities() {
105+
106+
assertApplicationContext(context -> {
107+
108+
JdbcMappingContext mappingContext = context.getBean(JdbcMappingContext.class);
109+
RelationalManagedTypes managedTypes = (RelationalManagedTypes) ReflectionTestUtils.getField(mappingContext,
110+
"managedTypes");
111+
112+
assertThat(managedTypes.toList()).contains(JdbcRepositoryConfigExtensionUnitTests.Sample.class,
113+
TopLevelEntity.class);
114+
115+
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
116+
}
117+
118+
static void assertApplicationContext(Consumer<ConfigurableApplicationContext> verification,
101119
Class<?>... configurationClasses) {
102120

103121
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
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.jdbc.repository.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 {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.relational;
17+
18+
import java.util.Arrays;
19+
import java.util.function.Consumer;
20+
21+
import org.springframework.data.domain.ManagedTypes;
22+
23+
/**
24+
* Relational-specific extension to {@link ManagedTypes}.
25+
*
26+
* @author Mark Paluch
27+
* @since 3.0
28+
*/
29+
public final class RelationalManagedTypes implements ManagedTypes {
30+
31+
private final ManagedTypes delegate;
32+
33+
private RelationalManagedTypes(ManagedTypes types) {
34+
this.delegate = types;
35+
}
36+
37+
/**
38+
* Wraps an existing {@link ManagedTypes} object with {@link RelationalManagedTypes}.
39+
*
40+
* @param managedTypes
41+
* @return
42+
*/
43+
public static RelationalManagedTypes from(ManagedTypes managedTypes) {
44+
return new RelationalManagedTypes(managedTypes);
45+
}
46+
47+
/**
48+
* Factory method used to construct {@link RelationalManagedTypes} from the given array of {@link Class types}.
49+
*
50+
* @param types array of {@link Class types} used to initialize the {@link ManagedTypes}; must not be {@literal null}.
51+
* @return new instance of {@link RelationalManagedTypes} initialized from {@link Class types}.
52+
*/
53+
public static RelationalManagedTypes from(Class<?>... types) {
54+
return fromIterable(Arrays.asList(types));
55+
}
56+
57+
/**
58+
* Factory method used to construct {@link RelationalManagedTypes} from the given, required {@link Iterable} of
59+
* {@link Class types}.
60+
*
61+
* @param types {@link Iterable} of {@link Class types} used to initialize the {@link ManagedTypes}; must not be
62+
* {@literal null}.
63+
* @return new instance of {@link RelationalManagedTypes} initialized the given, required {@link Iterable} of
64+
* {@link Class types}.
65+
*/
66+
public static RelationalManagedTypes fromIterable(Iterable<? extends Class<?>> types) {
67+
return from(ManagedTypes.fromIterable(types));
68+
}
69+
70+
/**
71+
* Factory method to return an empty {@link RelationalManagedTypes} object.
72+
*
73+
* @return an empty {@link RelationalManagedTypes} object.
74+
*/
75+
public static RelationalManagedTypes empty() {
76+
return from(ManagedTypes.empty());
77+
}
78+
79+
@Override
80+
public void forEach(Consumer<Class<?>> action) {
81+
delegate.forEach(action);
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.relational.aot;
17+
18+
import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor;
19+
import org.springframework.data.relational.RelationalManagedTypes;
20+
import org.springframework.lang.Nullable;
21+
import org.springframework.util.ClassUtils;
22+
23+
/**
24+
* Relational-specific extension to {@link ManagedTypesBeanRegistrationAotProcessor}.
25+
*
26+
* @author Mark Paluch
27+
* @since 3.0
28+
*/
29+
class RelationalManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor {
30+
31+
protected boolean isMatch(@Nullable Class<?> beanType, @Nullable String beanName) {
32+
return this.matchesByType(beanType);
33+
}
34+
35+
protected boolean matchesByType(@Nullable Class<?> beanType) {
36+
return beanType != null && ClassUtils.isAssignable(RelationalManagedTypes.class, beanType);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Ahead of Time processing utilities for Spring Data Relational.
3+
*/
4+
@NonNullApi
5+
package org.springframework.data.relational.aot;
6+
7+
import org.springframework.lang.NonNullApi;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
2+
org.springframework.data.relational.aot.RelationalManagedTypesBeanRegistrationAotProcessor

0 commit comments

Comments
 (0)