Skip to content

Commit 8c94f3b

Browse files
committed
Add support for named queries.
Closes #4961
1 parent 04d0d94 commit 8c94f3b

File tree

6 files changed

+111
-13
lines changed

6 files changed

+111
-13
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,36 @@
1818
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*;
1919
import static org.springframework.data.mongodb.repository.aot.QueryBlocks.*;
2020

21+
import java.io.IOException;
2122
import java.lang.reflect.Method;
23+
import java.util.Properties;
2224

2325
import org.apache.commons.logging.Log;
2426
import org.apache.commons.logging.LogFactory;
2527
import org.jspecify.annotations.Nullable;
2628

2729
import org.springframework.core.annotation.AnnotatedElementUtils;
30+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
2831
import org.springframework.data.mongodb.core.MongoOperations;
2932
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
3033
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
3134
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3235
import org.springframework.data.mongodb.repository.Query;
3336
import org.springframework.data.mongodb.repository.Update;
3437
import org.springframework.data.mongodb.repository.VectorSearch;
38+
import org.springframework.data.mongodb.repository.config.MongoRepositoryConfigurationExtension;
3539
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
3640
import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder;
3741
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
3842
import org.springframework.data.repository.aot.generate.MethodContributor;
3943
import org.springframework.data.repository.aot.generate.QueryMetadata;
4044
import org.springframework.data.repository.aot.generate.RepositoryContributor;
4145
import org.springframework.data.repository.config.AotRepositoryContext;
46+
import org.springframework.data.repository.config.PropertiesBasedNamedQueriesFactoryBean;
47+
import org.springframework.data.repository.config.RepositoryConfigurationSource;
48+
import org.springframework.data.repository.core.NamedQueries;
4249
import org.springframework.data.repository.core.RepositoryInformation;
50+
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
4351
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
4452
import org.springframework.data.repository.query.QueryMethod;
4553
import org.springframework.data.repository.query.parser.PartTree;
@@ -61,11 +69,18 @@ public class MongoRepositoryContributor extends RepositoryContributor {
6169

6270
private final AotQueryCreator queryCreator;
6371
private final MongoMappingContext mappingContext;
72+
private final NamedQueries namedQueries;
6473

6574
public MongoRepositoryContributor(AotRepositoryContext repositoryContext) {
6675

6776
super(repositoryContext);
6877

78+
ClassLoader classLoader = repositoryContext.getBeanFactory() != null ? repositoryContext.getClassLoader() : null;
79+
if (classLoader == null) {
80+
classLoader = getClass().getClassLoader();
81+
}
82+
namedQueries = getNamedQueries(repositoryContext.getConfigurationSource(), classLoader);
83+
6984
// avoid Java Time (JSR-310) Type introspection
7085
MongoCustomConversions mongoCustomConversions = MongoCustomConversions
7186
.create(MongoCustomConversions.MongoConverterConfigurationAdapter::useNativeDriverJavaTimeCodecs);
@@ -78,6 +93,32 @@ public MongoRepositoryContributor(AotRepositoryContext repositoryContext) {
7893
this.queryCreator = new AotQueryCreator(this.mappingContext);
7994
}
8095

96+
private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) {
97+
98+
String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null;
99+
100+
if (location == null) {
101+
location = new MongoRepositoryConfigurationExtension().getDefaultNamedQueryLocation();
102+
}
103+
104+
if (StringUtils.hasText(location)) {
105+
106+
try {
107+
108+
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);
109+
110+
PropertiesBasedNamedQueriesFactoryBean factoryBean = new PropertiesBasedNamedQueriesFactoryBean();
111+
factoryBean.setLocations(resolver.getResources(location));
112+
factoryBean.afterPropertiesSet();
113+
return factoryBean.getObject();
114+
} catch (IOException e) {
115+
throw new RuntimeException(e);
116+
}
117+
}
118+
119+
return new PropertiesBasedNamedQueries(new Properties());
120+
}
121+
81122
@Override
82123
protected void customizeClass(AotRepositoryClassBuilder classBuilder) {
83124
classBuilder.customize(builder -> builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class)));
@@ -189,6 +230,10 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor
189230
if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) {
190231
query = new QueryInteraction(new AotStringQuery(queryMethod.getAnnotatedQuery()), queryAnnotation.count(),
191232
queryAnnotation.delete(), queryAnnotation.exists());
233+
} else if (namedQueries.hasQuery(queryMethod.getNamedQueryName())) {
234+
query = new QueryInteraction(new AotStringQuery(namedQueries.getQuery(queryMethod.getNamedQueryName())),
235+
queryAnnotation != null && queryAnnotation.count(), queryAnnotation != null && queryAnnotation.delete(),
236+
queryAnnotation != null && queryAnnotation.exists());
192237
} else {
193238

194239
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());

spring-data-mongodb/src/test/java/example/aot/UserRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ public interface UserRepository extends CrudRepository<User, String> {
140140

141141
// TODO: TextSearch
142142

143+
/* Named Queries */
144+
145+
List<User> findByNamedQuery(String lastname);
146+
143147
/* Annotated Queries */
144148

145149
@Query("{ 'username' : ?0 }")

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import org.springframework.beans.factory.config.BeanDefinition;
2424
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
2525
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
26+
import org.springframework.beans.factory.config.RuntimeBeanReference;
2627
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2728
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2829
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2930
import org.springframework.core.test.tools.TestCompiler;
31+
import org.springframework.data.mongodb.core.MongoOperations;
3032
import org.springframework.data.projection.ProjectionFactory;
3133
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3234
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -39,18 +41,25 @@
3941
* <p>
4042
* This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
4143
* fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
42-
* invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
44+
* invocations to the backing AOT fragment by default (or when setting {@code fragmentFacade=true}). Note that
45+
* {@code repositoryInterface} is not a repository proxy.
4346
*
4447
* @author Christoph Strobl
4548
*/
4649
public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor {
4750

4851
private final Class<?> repositoryInterface;
52+
private final boolean registerFragmentFacade;
4953
private final TestMongoAotRepositoryContext repositoryContext;
5054

5155
public AotFragmentTestConfigurationSupport(Class<?> repositoryInterface) {
56+
this(repositoryInterface, true);
57+
}
58+
59+
public AotFragmentTestConfigurationSupport(Class<?> repositoryInterface, boolean registerFragmentFacade) {
5260

5361
this.repositoryInterface = repositoryInterface;
62+
this.registerFragmentFacade = registerFragmentFacade;
5463
this.repositoryContext = new TestMongoAotRepositoryContext(repositoryInterface, null);
5564
}
5665

@@ -59,28 +68,31 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
5968

6069
TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface);
6170

71+
repositoryContext.setBeanFactory(beanFactory);
6272
new MongoRepositoryContributor(repositoryContext).contribute(generationContext);
6373

6474
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
6575
.genericBeanDefinition(
6676
repositoryInterface.getPackageName() + "." + repositoryInterface.getSimpleName() + "Impl__Aot") //
67-
.addConstructorArgReference("mongoOperations") //
77+
.addConstructorArgValue(new RuntimeBeanReference(MongoOperations.class)) //
6878
.addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition();
6979

7080
TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
7181
beanFactory.setBeanClassLoader(compiled.getClassLoader());
7282
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository);
7383
});
7484

75-
BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
85+
if (registerFragmentFacade) {
86+
BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
7687

77-
Object fragment = beanFactory.getBean("fragment");
78-
Object proxy = getFragmentFacadeProxy(fragment);
88+
Object fragment = beanFactory.getBean("fragment");
89+
Object proxy = getFragmentFacadeProxy(fragment);
7990

80-
return repositoryInterface.cast(proxy);
81-
}).getBeanDefinition();
91+
return repositoryInterface.cast(proxy);
92+
}).getBeanDefinition();
8293

83-
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
94+
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
95+
}
8496

8597
beanFactory.registerSingleton("generationContext", generationContext);
8698
}
@@ -135,4 +147,5 @@ public MethodNotImplementedException(String message) {
135147
super(message);
136148
}
137149
}
150+
138151
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ void shouldDocumentBase() throws IOException {
7777
.containsEntry("type", "IMPERATIVE");
7878
}
7979

80+
@Test // GH-4961
81+
void shouldDocumentNamedQuery() throws IOException {
82+
83+
Resource resource = getResource();
84+
85+
assertThat(resource).isNotNull();
86+
assertThat(resource.exists()).isTrue();
87+
88+
String json = resource.getContentAsString(StandardCharsets.UTF_8);
89+
90+
assertThatJson(json).inPath("$.methods[?(@.name == 'findByNamedQuery')].query").isArray().element(0).isObject()
91+
.containsEntry("filter", "{'firstname' : ?0}");
92+
}
93+
8094
@Test // GH-4964
8195
void shouldDocumentDerivedQuery() throws IOException {
8296

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,42 @@
2929
import org.springframework.core.io.ClassPathResource;
3030
import org.springframework.core.test.tools.ClassFile;
3131
import org.springframework.data.mongodb.core.mapping.Document;
32+
import org.springframework.data.mongodb.repository.support.MongoRepositoryFragmentsContributor;
33+
import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
3234
import org.springframework.data.repository.config.AotRepositoryContext;
35+
import org.springframework.data.repository.config.AotRepositoryInformation;
3336
import org.springframework.data.repository.config.RepositoryConfigurationSource;
3437
import org.springframework.data.repository.core.RepositoryInformation;
38+
import org.springframework.data.repository.core.RepositoryMetadata;
39+
import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata;
3540
import org.springframework.data.repository.core.support.RepositoryComposition;
3641

3742
/**
3843
* @author Christoph Strobl
3944
*/
4045
public class TestMongoAotRepositoryContext implements AotRepositoryContext {
4146

42-
private final StubRepositoryInformation repositoryInformation;
47+
private final AotRepositoryInformation repositoryInformation;
4348
private final Environment environment = new StandardEnvironment();
49+
private final Class<?> repositoryInterface;
50+
private @Nullable ConfigurableListableBeanFactory beanFactory;
4451

4552
public TestMongoAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
46-
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
53+
54+
this.repositoryInterface = repositoryInterface;
55+
56+
RepositoryMetadata metadata = AnnotationRepositoryMetadata.getMetadata(repositoryInterface);
57+
58+
RepositoryComposition.RepositoryFragments fragments = MongoRepositoryFragmentsContributor.DEFAULT
59+
.describe(metadata);
60+
61+
this.repositoryInformation = new AotRepositoryInformation(metadata, SimpleMongoRepository.class,
62+
fragments.stream().toList());
4763
}
4864

4965
@Override
5066
public ConfigurableListableBeanFactory getBeanFactory() {
51-
return null;
67+
return beanFactory;
5268
}
5369

5470
@Override
@@ -78,7 +94,7 @@ public RepositoryConfigurationSource getConfigurationSource() {
7894

7995
@Override
8096
public Set<String> getBasePackages() {
81-
return Set.of("org.springframework.data.dummy.repository.aot");
97+
return Set.of(repositoryInterface.getPackageName());
8298
}
8399

84100
@Override
@@ -121,4 +137,9 @@ static ClassFile classFileForType(Class<?> type) {
121137
public Environment getEnvironment() {
122138
return environment;
123139
}
140+
141+
public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) {
142+
this.beanFactory = beanFactory;
143+
}
144+
124145
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Person.findByNamedQuery={'firstname' : ?0}
1+
Person.findByNamedQuery={'firstname' : ?0}
2+
User.findByNamedQuery={'firstname' : ?0}

0 commit comments

Comments
 (0)