From 65214fb0b185a5a5391aec5fd98229b5a297bc84 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 May 2025 17:11:38 +0200 Subject: [PATCH] Provide `JpaRepositoryFragmentsContributor` in JPA Repository Factory and Repository Factory Bean. Closes #3874 --- .../config/JpaRepositoryConfigExtension.java | 130 +----------------- .../support/JpaEntityInformationSupport.java | 2 +- .../support/JpaRepositoryFactory.java | 46 +++---- .../support/JpaRepositoryFactoryBean.java | 55 +++++--- .../JpaRepositoryFragmentsContributor.java | 84 +++++++++++ .../support/QuerydslContributor.java | 78 +++++++++++ .../aot/AotContributionIntegrationTests.java | 84 +++++++++++ ...JpaRepositoryMetadataIntegrationTests.java | 2 +- .../aot/QuerydslUserRepository.java | 28 ++++ .../aot/TestJpaAotRepositoryContext.java | 5 + ...toryRegistrationAotProcessorUnitTests.java | 5 + .../JpaRepositoryFactoryUnitTests.java | 16 ++- ...positoryFragmentsContributorUnitTests.java | 96 +++++++++++++ 13 files changed, 457 insertions(+), 174 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 99eec5010..eb89f0af8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -43,13 +43,10 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.aot.AotContext; @@ -63,14 +60,10 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.config.ImplementationDetectionConfiguration; -import org.springframework.data.repository.config.ImplementationLookupConfiguration; -import org.springframework.data.repository.config.RepositoryConfiguration; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; -import org.springframework.data.util.Streamable; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -105,6 +98,11 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi return "JPA"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleJpaRepository.class.getName(); + } + @Override public String getRepositoryFactoryBeanClassName() { return JpaRepositoryFactoryBean.class.getName(); @@ -342,123 +340,5 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi return emf != null ? new JpaRepositoryContributor(repositoryContext, emf) : new JpaRepositoryContributor(repositoryContext); } - - @Nullable - @Override - @SuppressWarnings("NullAway") - protected RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { - RepositoryConfiguration configuration = super.getRepositoryMetadata(bean); - - if (configuration != null && configuration.getRepositoryBaseClassName().isPresent()) { - return configuration; - } - return new Meh<>(configuration); - } - } - - /** - * I'm just a dirty hack so we can refine the {@link #getRepositoryBaseClassName()} method as we cannot instantiate - * the bean safely to extract it form the repository factory in data commons. So we either have a configurable - * {@link RepositoryConfiguration} return from - * {@link RepositoryRegistrationAotProcessor#getRepositoryMetadata(RegisteredBean)} or change the arrangement and - * maybe move the type out of the factoy. - * - * @param - */ - static class Meh implements RepositoryConfiguration { - - private RepositoryConfiguration configuration; - - public Meh(RepositoryConfiguration configuration) { - this.configuration = configuration; - } - - @Nullable - @Override - public Object getSource() { - return configuration.getSource(); - } - - @Override - public T getConfigurationSource() { - return (T) configuration.getConfigurationSource(); - } - - @Override - public boolean isLazyInit() { - return configuration.isLazyInit(); - } - - @Override - public boolean isPrimary() { - return configuration.isPrimary(); - } - - @Override - public Streamable getBasePackages() { - return configuration.getBasePackages(); - } - - @Override - public Streamable getImplementationBasePackages() { - return configuration.getImplementationBasePackages(); - } - - @Override - public String getRepositoryInterface() { - return configuration.getRepositoryInterface(); - } - - @Override - public Optional getQueryLookupStrategyKey() { - return Optional.ofNullable(configuration.getQueryLookupStrategyKey()); - } - - @Override - public Optional getNamedQueriesLocation() { - return configuration.getNamedQueriesLocation(); - } - - @Override - public Optional getRepositoryBaseClassName() { - String name = SimpleJpaRepository.class.getName(); - return Optional.of(name); - } - - @Override - public String getRepositoryFactoryBeanClassName() { - return configuration.getRepositoryFactoryBeanClassName(); - } - - @Override - public String getImplementationBeanName() { - return configuration.getImplementationBeanName(); - } - - @Override - public String getRepositoryBeanName() { - return configuration.getRepositoryBeanName(); - } - - @Override - public Streamable getExcludeFilters() { - return configuration.getExcludeFilters(); - } - - @Override - public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { - return configuration.toImplementationDetectionConfiguration(factory); - } - - @Override - public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { - return configuration.toLookupConfiguration(factory); - } - - @Nullable - @Override - public String getResourceDescription() { - return configuration.getResourceDescription(); - } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java index 6d8c0ba8d..62af51607 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java @@ -35,7 +35,7 @@ import org.springframework.util.Assert; public abstract class JpaEntityInformationSupport extends AbstractEntityInformation implements JpaEntityInformation { - private JpaEntityMetadata metadata; + private final JpaEntityMetadata metadata; /** * Creates a new {@link JpaEntityInformationSupport} with the given domain class. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 91314ed11..bbccb5b97 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -15,8 +15,6 @@ */ package org.springframework.data.jpa.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - import jakarta.persistence.EntityManager; import jakarta.persistence.Tuple; @@ -32,7 +30,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.JpaRepository; @@ -40,7 +37,6 @@ import org.springframework.data.jpa.repository.query.*; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -79,6 +75,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private JpaRepositoryFragmentsContributor fragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT; private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR; private JpaQueryMethodFactory queryMethodFactory; private QueryRewriterProvider queryRewriterProvider; @@ -159,6 +156,17 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.escapeCharacter = escapeCharacter; } + /** + * Configures the {@link JpaRepositoryFragmentsContributor} to be used. Defaults to + * {@link JpaRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 4.0 + */ + public void setFragmentsContributor(JpaRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + /** * Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link DefaultJpaQueryMethodFactory}. * @@ -259,51 +267,39 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { @Override @SuppressWarnings("unchecked") public JpaEntityInformation getEntityInformation(Class domainClass) { - return (JpaEntityInformation) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); } @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - return getRepositoryFragments(metadata, entityManager, entityPathResolver, this.crudMethodMetadata); } /** - * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically, * adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl. *

- * Can be overridden by subclasses to customize {@link RepositoryFragments}. + * Built-in fragment contribution can be customized by configuring {@link JpaRepositoryFragmentsContributor}. * * @param metadata repository metadata. * @param entityManager the entity manager. * @param resolver resolver to translate a plain domain class into a {@link EntityPath}. * @param crudMethodMetadata metadata about the invoked CRUD methods. - * @return + * @return {@link RepositoryFragments} to be added to the repository. * @since 2.5.1 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, EntityManager entityManager, EntityPathResolver resolver, CrudMethodMetadata crudMethodMetadata) { - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + RepositoryFragments fragments = this.fragmentsContributor.contribute(metadata, + getEntityInformation(metadata.getDomainType()), entityManager, resolver); - if (isQueryDslRepository) { - - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine Querydsl and reactive repository support in a single interface"); - } - - QuerydslJpaPredicateExecutor querydslJpaPredicateExecutor = new QuerydslJpaPredicateExecutor<>( - getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata); - invokeAwareMethods(querydslJpaPredicateExecutor); - - return RepositoryFragments - .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor)); + for (RepositoryFragment fragment : fragments) { + fragment.getImplementation().filter(JpaRepositoryConfigurationAware.class::isInstance) + .ifPresent(it -> invokeAwareMethods((JpaRepositoryConfigurationAware) it)); } - return RepositoryFragments.empty(); + return fragments; } private void invokeAwareMethods(JpaRepositoryConfigurationAware repository) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index a9d8622a4..30461fcab 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -55,9 +55,10 @@ public class JpaRepositoryFactoryBean, S, ID> private @Nullable BeanFactory beanFactory; private @Nullable EntityManager entityManager; private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE; + private JpaRepositoryFragmentsContributor repositoryFragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private @Nullable JpaQueryMethodFactory queryMethodFactory; - private @Nullable Function queryEnhancerSelectorSource; + private @Nullable Function<@Nullable BeanFactory, QueryEnhancerSelector> queryEnhancerSelectorSource; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. @@ -100,20 +101,24 @@ public class JpaRepositoryFactoryBean, S, ID> this.entityPathResolver = resolver.getIfAvailable(() -> SimpleEntityPathResolver.INSTANCE); } - /** - * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will - * fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is - * available. - * - * @param resolver may be {@literal null}. - */ - @Autowired - public void setQueryMethodFactory(ObjectProvider resolver) { // TODO: nullable insteand of ObjectProvider + @Override + public JpaRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } - JpaQueryMethodFactory factory = resolver.getIfAvailable(); - if (factory != null) { - this.queryMethodFactory = factory; - } + /** + * Configures the {@link JpaRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 4.0 + */ + public void setRepositoryFragmentsContributor(JpaRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + + public void setEscapeCharacter(char escapeCharacter) { + this.escapeCharacter = EscapeCharacter.of(escapeCharacter); } /** @@ -153,6 +158,23 @@ public class JpaRepositoryFactoryBean, S, ID> }; } + /** + * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will + * fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is + * available. + * + * @param resolver may be {@literal null}. + */ + @Autowired + public void setQueryMethodFactory(ObjectProvider resolver) { // TODO: nullable insteand of + // ObjectProvider + + JpaQueryMethodFactory factory = resolver.getIfAvailable(); + if (factory != null) { + this.queryMethodFactory = factory; + } + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { @@ -169,6 +191,7 @@ public class JpaRepositoryFactoryBean, S, ID> JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager); factory.setEntityPathResolver(entityPathResolver); factory.setEscapeCharacter(escapeCharacter); + factory.setFragmentsContributor(getRepositoryFragmentsContributor()); if (queryMethodFactory != null) { factory.setQueryMethodFactory(queryMethodFactory); @@ -189,8 +212,4 @@ public class JpaRepositoryFactoryBean, S, ID> super.afterPropertiesSet(); } - public void setEscapeCharacter(char escapeCharacter) { - - this.escapeCharacter = EscapeCharacter.of(escapeCharacter); - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java new file mode 100644 index 000000000..03d072b43 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java @@ -0,0 +1,84 @@ +/* + * Copyright 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.data.jpa.repository.support; + +import jakarta.persistence.EntityManager; + +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +import com.querydsl.core.types.EntityPath; + +/** + * JPA-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + *

+ * Contributed fragments may implement the {@link JpaRepositoryConfigurationAware} interface to access configuration + * settings. + * + * @author Mark Paluch + * @since 4.0 + */ +public interface JpaRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + JpaRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code JpaRepositoryFragmentsContributor} that first applies this contributor to its inputs, and + * then applies the {@code after} contributor concatenating effectively both results. If evaluation of either + * contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default JpaRepositoryFragmentsContributor andThen(JpaRepositoryFragmentsContributor after) { + + Assert.notNull(after, "JpaRepositoryFragmentsContributor must not be null"); + + return new JpaRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + return JpaRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, entityManager, resolver) + .append(after.contribute(metadata, entityInformation, entityManager, resolver)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return JpaRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific + * extensions. Typically, adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param entityManager the entity manager. + * @param resolver resolver to translate a plain domain class into a {@link EntityPath}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver); + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java new file mode 100644 index 000000000..5f5e819c7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java @@ -0,0 +1,78 @@ +/* + * Copyright 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.data.jpa.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import jakarta.persistence.EntityManager; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * JPA-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements + * {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 4.0 + * @see QuerydslJpaPredicateExecutor + */ +enum QuerydslContributor implements JpaRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + + if (isQuerydslRepository(metadata)) { + + if (metadata.isReactiveRepository()) { + throw new InvalidDataAccessApiUsageException( + "Cannot combine Querydsl and reactive repository support in a single interface"); + } + + QuerydslJpaPredicateExecutor executor = new QuerydslJpaPredicateExecutor<>(entityInformation, entityManager, + resolver, null); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslJpaPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java new file mode 100644 index 000000000..76390740a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 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.data.jpa.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.config.InfrastructureConfig; +import org.springframework.mock.env.MockPropertySource; + +/** + * Integration tests for AOT processing. + * + * @author Mark Paluch + */ +class AotContributionIntegrationTests { + + @EnableJpaRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) }) + static class AotConfiguration extends InfrastructureConfig { + + } + + @Test // GH-3830 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + QuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor") + .containsEntry("fragment", "org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.jpa.repository.support.SimpleJpaRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java index 3450bcf1a..0a65cd5c3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java @@ -61,7 +61,7 @@ class JpaRepositoryMetadataIntegrationTests { assertThatJson(json).isObject() // .containsEntry("name", UserRepository.class.getName()) // - .containsEntry("module", "") // TODO: JPA should be here + .containsEntry("module", "JPA") // .containsEntry("type", "IMPERATIVE"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java new file mode 100644 index 000000000..6c551c482 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 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.data.jpa.repository.aot; + +import java.util.List; + +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; + +interface QuerydslUserRepository extends CrudRepository, QuerydslPredicateExecutor { + + List findUserNoArgumentsBy(); + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index 216ed8ee1..6fc63defa 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -80,6 +80,11 @@ public class TestJpaAotRepositoryContext implements AotRepositoryContext { return "dummyRepository"; } + @Override + public String getModuleName() { + return "JPA"; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java index ba3f33f02..44c260dcb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java @@ -88,6 +88,11 @@ class JpaRepositoryRegistrationAotProcessorUnitTests { return "jpaRepository"; } + @Override + public String getModuleName() { + return "JPA"; + } + @Override public Set getBasePackages() { return Collections.singleton(this.getClass().getPackageName()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java index 4b5ad4cf3..bdc1a67a9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java @@ -15,13 +15,15 @@ */ package org.springframework.data.jpa.repository.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.metamodel.IdentifiableType; +import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Metamodel; import java.io.IOException; @@ -35,6 +37,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.aop.framework.Advised; import org.springframework.core.OverridingClassLoader; import org.springframework.data.jpa.domain.sample.User; @@ -62,6 +65,7 @@ class JpaRepositoryFactoryUnitTests { private JpaRepositoryFactory factory; @Mock EntityManager entityManager; + @Mock PersistenceUnitUtil persistenceUnitUtil; @Mock Metamodel metamodel; @Mock @SuppressWarnings("rawtypes") JpaEntityInformation entityInformation; @@ -74,6 +78,7 @@ class JpaRepositoryFactoryUnitTests { when(entityManager.getEntityManagerFactory()).thenReturn(emf); when(entityManager.getDelegate()).thenReturn(entityManager); when(emf.createEntityManager()).thenReturn(entityManager); + when(emf.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); // Setup standard factory configuration factory = new JpaRepositoryFactory(entityManager) { @@ -140,6 +145,9 @@ class JpaRepositoryFactoryUnitTests { @Test void createsProxyWithCustomBaseClass() { + when(metamodel.managedType(any())) + .thenReturn(mock(ManagedType.class, withSettings().extraInterfaces(IdentifiableType.class))); + JpaRepositoryFactory factory = new CustomGenericJpaRepositoryFactory(entityManager); factory.setQueryLookupStrategyKey(Key.CREATE_IF_NOT_FOUND); UserCustomExtendedRepository repository = factory.getRepository(UserCustomExtendedRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 000000000..7825534a3 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 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.data.jpa.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import jakarta.persistence.EntityManager; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.domain.sample.QCustomer; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +import com.querydsl.core.types.EntityPath; + +/** + * Unit tests for {@link JpaRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class JpaRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + JpaRepositoryFragmentsContributor contributor = JpaRepositoryFragmentsContributor.DEFAULT + .andThen(MyJpaRepositoryFragmentsContributor.INSTANCE); + + EntityPathResolver entityPathResolver = mock(EntityPathResolver.class); + when(entityPathResolver.createPath(any())).thenReturn((EntityPath) QCustomer.customer); + + EntityManager entityManager = mock(EntityManager.class); + when(entityManager.getDelegate()).thenReturn(entityManager); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new JpaEntityInformationSupportUnitTests.DummyJpaEntityInformation<>(QuerydslUserRepository.class), + entityManager, entityPathResolver); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslJpaPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyJpaRepositoryFragmentsContributor implements JpaRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, QuerydslPredicateExecutor {} + +}