Provide JpaRepositoryFragmentsContributor in JPA Repository Factory and Repository Factory Bean.
Closes #3874
This commit is contained in:
@@ -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 <T>
|
||||
*/
|
||||
static class Meh<T extends RepositoryConfigurationSource> implements RepositoryConfiguration<T> {
|
||||
|
||||
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<String> getBasePackages() {
|
||||
return configuration.getBasePackages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Streamable<String> getImplementationBasePackages() {
|
||||
return configuration.getImplementationBasePackages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepositoryInterface() {
|
||||
return configuration.getRepositoryInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> getQueryLookupStrategyKey() {
|
||||
return Optional.ofNullable(configuration.getQueryLookupStrategyKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getNamedQueriesLocation() {
|
||||
return configuration.getNamedQueriesLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> 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<TypeFilter> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import org.springframework.util.Assert;
|
||||
public abstract class JpaEntityInformationSupport<T, ID> extends AbstractEntityInformation<T, ID>
|
||||
implements JpaEntityInformation<T, ID> {
|
||||
|
||||
private JpaEntityMetadata<T> metadata;
|
||||
private final JpaEntityMetadata<T> metadata;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JpaEntityInformationSupport} with the given domain class.
|
||||
|
||||
@@ -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 <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
|
||||
|
||||
return (JpaEntityInformation<T, ID>) 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.
|
||||
* <p>
|
||||
* 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) {
|
||||
|
||||
@@ -55,9 +55,10 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, 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<BeanFactory, QueryEnhancerSelector> 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<T extends Repository<S, ID>, 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<JpaQueryMethodFactory> 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<T extends Repository<S, ID>, 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<JpaQueryMethodFactory> 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<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, S, ID>
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
|
||||
public void setEscapeCharacter(char escapeCharacter) {
|
||||
|
||||
this.escapeCharacter = EscapeCharacter.of(escapeCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* Implementations must define a no-args constructor.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<User, Integer>, QuerydslPredicateExecutor<User> {
|
||||
|
||||
List<User> findUserNoArgumentsBy();
|
||||
|
||||
}
|
||||
@@ -80,6 +80,11 @@ public class TestJpaAotRepositoryContext<T> implements AotRepositoryContext {
|
||||
return "dummyRepository";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModuleName() {
|
||||
return "JPA";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getBasePackages() {
|
||||
return Set.of("org.springframework.data.dummy.repository.aot");
|
||||
|
||||
@@ -88,6 +88,11 @@ class JpaRepositoryRegistrationAotProcessorUnitTests {
|
||||
return "jpaRepository";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModuleName() {
|
||||
return "JPA";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getBasePackages() {
|
||||
return Collections.singleton(this.getClass().getPackageName());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<RepositoryFragment<?>> 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<User, Long>, QuerydslPredicateExecutor<User> {}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user