committed by
Mark Paluch
parent
b58c423bdf
commit
831d04dd2c
5
pom.xml
5
pom.xml
@@ -145,6 +145,11 @@
|
||||
<version>${spring}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<version>3.6.1.Final</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<version>3.6.1.Final</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
|
||||
@@ -88,12 +88,16 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
@@ -239,6 +243,12 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<version>3.6.1.Final</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -370,6 +380,11 @@
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
<version>${jakarta-persistence-api}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<version>3.6.1.Final</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2024-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.generated;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.metamodel.EmbeddableType;
|
||||
import jakarta.persistence.metamodel.EntityType;
|
||||
import jakarta.persistence.metamodel.ManagedType;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
import jakarta.persistence.spi.ClassTransformer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl;
|
||||
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.instrument.classloading.SimpleThrowawayClassLoader;
|
||||
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class AotMetaModel implements Metamodel {
|
||||
|
||||
private final String persistenceUnit;
|
||||
private final Set<Class<?>> managedTypes;
|
||||
private final Lazy<EntityManagerFactory> entityManagerFactory = Lazy.of(this::init);
|
||||
private final Lazy<Metamodel> metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel());
|
||||
private final Lazy<EntityManager> entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager());
|
||||
|
||||
public AotMetaModel(Set<Class<?>> managedTypes) {
|
||||
this("dynamic-tests", managedTypes);
|
||||
}
|
||||
|
||||
private AotMetaModel(String persistenceUnit, Set<Class<?>> managedTypes) {
|
||||
this.persistenceUnit = persistenceUnit;
|
||||
this.managedTypes = managedTypes;
|
||||
}
|
||||
|
||||
public static AotMetaModel hibernateModel(Class<?>... types) {
|
||||
return new AotMetaModel(Set.of(types));
|
||||
}
|
||||
|
||||
public static AotMetaModel hibernateModel(String persistenceUnit, Class<?>... types) {
|
||||
return new AotMetaModel(persistenceUnit, Set.of(types));
|
||||
}
|
||||
|
||||
public <X> EntityType<X> entity(Class<X> cls) {
|
||||
return metamodel.get().entity(cls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityType<?> entity(String s) {
|
||||
return metamodel.get().entity(s);
|
||||
}
|
||||
|
||||
public <X> ManagedType<X> managedType(Class<X> cls) {
|
||||
return metamodel.get().managedType(cls);
|
||||
}
|
||||
|
||||
public <X> EmbeddableType<X> embeddable(Class<X> cls) {
|
||||
return metamodel.get().embeddable(cls);
|
||||
}
|
||||
|
||||
public Set<ManagedType<?>> getManagedTypes() {
|
||||
return metamodel.get().getManagedTypes();
|
||||
}
|
||||
|
||||
public Set<EntityType<?>> getEntities() {
|
||||
return metamodel.get().getEntities();
|
||||
}
|
||||
|
||||
public Set<EmbeddableType<?>> getEmbeddables() {
|
||||
return metamodel.get().getEmbeddables();
|
||||
}
|
||||
|
||||
public EntityManager entityManager() {
|
||||
return entityManager.get();
|
||||
}
|
||||
|
||||
EntityManagerFactory init() {
|
||||
|
||||
MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() {
|
||||
@Override
|
||||
public ClassLoader getNewTempClassLoader() {
|
||||
return new SimpleThrowawayClassLoader(this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransformer(ClassTransformer classTransformer) {
|
||||
// just ingnore it
|
||||
}
|
||||
};
|
||||
|
||||
persistenceUnitInfo.setPersistenceUnitName(persistenceUnit);
|
||||
this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName);
|
||||
|
||||
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
|
||||
|
||||
return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) {
|
||||
@Override
|
||||
public List<String> getManagedClassNames() {
|
||||
return persistenceUnitInfo.getManagedClassNames();
|
||||
}
|
||||
}, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.generated;
|
||||
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.jpa.repository.query.EscapeCharacter;
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters;
|
||||
import org.springframework.data.jpa.repository.query.JpaQueryCreator;
|
||||
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider;
|
||||
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
|
||||
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
|
||||
import org.springframework.data.repository.query.ParametersSource;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
public class AotQueryCreator {
|
||||
|
||||
Metamodel metamodel;
|
||||
|
||||
public AotQueryCreator(Metamodel metamodel) {
|
||||
this.metamodel = metamodel;
|
||||
}
|
||||
|
||||
AotStringQuery createQuery(PartTree partTree, ReturnedType returnedType,
|
||||
AotRepositoryMethodGenerationContext context) {
|
||||
|
||||
ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod());
|
||||
JpaParameters parameters = new JpaParameters(parametersSource);
|
||||
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
|
||||
JpqlQueryTemplates.UPPER);
|
||||
|
||||
JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider,
|
||||
JpqlQueryTemplates.UPPER, metamodel);
|
||||
AotStringQuery query = AotStringQuery.bindable(queryCreator.createQuery(), metadataProvider.getBindings());
|
||||
|
||||
if (partTree.isLimiting()) {
|
||||
query.setLimit(partTree.getResultLimit());
|
||||
}
|
||||
query.setCountQuery(context.annotationValue(Query.class, "countQuery"));
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.generated;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBindingParser;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBindingParser.Metadata;
|
||||
import org.springframework.data.jpa.repository.query.QueryUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
class AotStringQuery {
|
||||
|
||||
private final String raw;
|
||||
private final String sanitized;
|
||||
private @Nullable String countQuery;
|
||||
private final List<ParameterBinding> parameterBindings;
|
||||
private final Metadata parameterMetadata;
|
||||
private Limit limit;
|
||||
private boolean nativeQuery;
|
||||
|
||||
public AotStringQuery(String raw, String sanitized, List<ParameterBinding> parameterBindings,
|
||||
Metadata parameterMetadata) {
|
||||
this.raw = raw;
|
||||
this.sanitized = sanitized;
|
||||
this.parameterBindings = parameterBindings;
|
||||
this.parameterMetadata = parameterMetadata;
|
||||
}
|
||||
|
||||
static AotStringQuery of(String raw) {
|
||||
|
||||
List<ParameterBinding> bindings = new ArrayList<>();
|
||||
Metadata metadata = new Metadata();
|
||||
String targetQuery = ParameterBindingParser.INSTANCE
|
||||
.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(raw, bindings, metadata);
|
||||
|
||||
return new AotStringQuery(raw, targetQuery, bindings, metadata);
|
||||
}
|
||||
|
||||
static AotStringQuery nativeQuery(String raw) {
|
||||
AotStringQuery q = of(raw);
|
||||
q.nativeQuery = true;
|
||||
return q;
|
||||
}
|
||||
|
||||
static AotStringQuery bindable(String query, List<ParameterBinding> bindings) {
|
||||
return new AotStringQuery(query, query, bindings, new Metadata());
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
public String getCountQuery(@Nullable String projection) {
|
||||
|
||||
if (StringUtils.hasText(countQuery)) {
|
||||
return countQuery;
|
||||
}
|
||||
return QueryUtils.createCountQueryFor(sanitized, StringUtils.hasText(projection) ? projection : null, nativeQuery);
|
||||
}
|
||||
|
||||
public List<ParameterBinding> parameterBindings() {
|
||||
return this.parameterBindings;
|
||||
}
|
||||
|
||||
boolean isLimited() {
|
||||
return limit != null && limit.isLimited();
|
||||
}
|
||||
|
||||
Limit getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(Limit limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public boolean isNativeQuery() {
|
||||
return nativeQuery;
|
||||
}
|
||||
|
||||
public void setCountQuery(@Nullable String countQuery) {
|
||||
this.countQuery = StringUtils.hasText(countQuery) ? countQuery : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.generated;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.domain.SliceImpl;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.repository.query.DeclaredQuery;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding;
|
||||
import org.springframework.data.jpa.repository.query.QueryEnhancer;
|
||||
import org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation;
|
||||
import org.springframework.data.jpa.repository.query.QueryEnhancerFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.support.PageableExecutionUtils;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
import org.springframework.javapoet.CodeBlock.Builder;
|
||||
import org.springframework.javapoet.TypeName;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
public class JpaCodeBlocks {
|
||||
|
||||
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
|
||||
|
||||
static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) {
|
||||
return new QueryBlockBuilder(context);
|
||||
}
|
||||
|
||||
static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) {
|
||||
return new QueryExecutionBlockBuilder(context);
|
||||
}
|
||||
|
||||
static class QueryExecutionBlockBuilder {
|
||||
|
||||
AotRepositoryMethodGenerationContext context;
|
||||
private String queryVariableName;
|
||||
|
||||
public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
QueryExecutionBlockBuilder referencing(String queryVariableName) {
|
||||
|
||||
this.queryVariableName = queryVariableName;
|
||||
return this;
|
||||
}
|
||||
|
||||
CodeBlock build() {
|
||||
|
||||
Builder builder = CodeBlock.builder();
|
||||
|
||||
boolean isProjecting = context.getActualReturnType() != null
|
||||
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
|
||||
context.getActualReturnType());
|
||||
Object actualReturnType = isProjecting ? context.getActualReturnType()
|
||||
: context.getRepositoryInformation().getDomainType();
|
||||
|
||||
builder.add("\n");
|
||||
|
||||
if (context.isDeleteMethod()) {
|
||||
|
||||
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName);
|
||||
builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class));
|
||||
if (context.returnsSingleValue()) {
|
||||
if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) {
|
||||
builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType());
|
||||
} else {
|
||||
builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()");
|
||||
}
|
||||
} else {
|
||||
builder.addStatement("return resultList");
|
||||
}
|
||||
} else if (context.isExistsMethod()) {
|
||||
builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName);
|
||||
} else {
|
||||
|
||||
if (context.returnsSingleValue()) {
|
||||
if (context.returnsOptionalValue()) {
|
||||
builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class,
|
||||
actualReturnType, queryVariableName);
|
||||
} else {
|
||||
builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName);
|
||||
}
|
||||
} else if (context.returnsPage()) {
|
||||
builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)",
|
||||
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName,
|
||||
context.getPageableParameterName());
|
||||
} else if (context.returnsSlice()) {
|
||||
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType,
|
||||
queryVariableName);
|
||||
builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()",
|
||||
context.getPageableParameterName(), context.getPageableParameterName());
|
||||
builder.addStatement(
|
||||
"return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)",
|
||||
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName());
|
||||
} else {
|
||||
builder.addStatement("return ($T) query.getResultList()", context.getReturnType());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class QueryBlockBuilder {
|
||||
|
||||
private final AotRepositoryMethodGenerationContext context;
|
||||
private String queryVariableName;
|
||||
private AotStringQuery query;
|
||||
|
||||
public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
QueryBlockBuilder usingQueryVariableName(String queryVariableName) {
|
||||
|
||||
this.queryVariableName = queryVariableName;
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryBlockBuilder filter(String queryString) {
|
||||
return filter(AotStringQuery.of(queryString));
|
||||
}
|
||||
|
||||
QueryBlockBuilder filter(AotStringQuery query) {
|
||||
this.query = query;
|
||||
return this;
|
||||
}
|
||||
|
||||
CodeBlock build() {
|
||||
|
||||
boolean isProjecting = context.getActualReturnType() != null
|
||||
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
|
||||
context.getActualReturnType());
|
||||
Object actualReturnType = isProjecting ? context.getActualReturnType()
|
||||
: context.getRepositoryInformation().getDomainType();
|
||||
|
||||
CodeBlock.Builder builder = CodeBlock.builder();
|
||||
builder.add("\n");
|
||||
String queryStringNameVariableName = "%sString".formatted(queryVariableName);
|
||||
builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, query.getQueryString());
|
||||
|
||||
String countQueryStringNameVariableName = null;
|
||||
String countQuyerVariableName = null;
|
||||
if (context.returnsPage()) {
|
||||
countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName));
|
||||
countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName));
|
||||
String projection = context.annotationValue(org.springframework.data.jpa.repository.Query.class,
|
||||
"countProjection");
|
||||
builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName,
|
||||
query.getCountQuery(projection));
|
||||
}
|
||||
|
||||
// sorting
|
||||
// TODO: refactor into sort builder
|
||||
{
|
||||
String sortParameterName = context.getSortParameterName();
|
||||
if (sortParameterName == null && context.getPageableParameterName() != null) {
|
||||
sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(sortParameterName)) {
|
||||
builder.beginControlFlow("if($L.isSorted())", sortParameterName);
|
||||
|
||||
if(query.isNativeQuery()) {
|
||||
builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
|
||||
queryStringNameVariableName);
|
||||
} else {
|
||||
builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
|
||||
queryStringNameVariableName);
|
||||
}
|
||||
|
||||
String enhancerVarName = "%sEnhancer".formatted(queryStringNameVariableName);
|
||||
builder.addStatement("$T $L = $T.forQuery(declaredQuery).create(declaredQuery)", QueryEnhancer.class, enhancerVarName, QueryEnhancerFactory.class);
|
||||
|
||||
builder.addStatement("$L = $L.rewrite(new $T() { public $T getSort() { return $L; } public $T getReturnedType() { return $T.of($T.class, $T.class, new $T());} })", queryStringNameVariableName, enhancerVarName, QueryRewriteInformation.class,
|
||||
Sort.class, sortParameterName, ReturnedType.class, ReturnedType.class,
|
||||
context.getRepositoryInformation().getDomainType(), actualReturnType, SpelAwareProxyProjectionFactory.class);
|
||||
|
||||
builder.endControlFlow();
|
||||
}
|
||||
}
|
||||
|
||||
addQueryBlock(builder, queryVariableName, queryStringNameVariableName, query.isNativeQuery());
|
||||
|
||||
if (context.isExistsMethod()) {
|
||||
builder.addStatement("$L.setMaxResults(1)", queryVariableName);
|
||||
} else {
|
||||
|
||||
{
|
||||
String limitParameterName = context.getLimitParameterName();
|
||||
|
||||
if (StringUtils.hasText(limitParameterName)) {
|
||||
builder.beginControlFlow("if($L.isLimited())", limitParameterName);
|
||||
builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limitParameterName);
|
||||
builder.endControlFlow();
|
||||
} else if (query.isLimited()) {
|
||||
builder.addStatement("$L.setMaxResults($L)", queryVariableName, query.getLimit().max());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
String pageableParamterName = context.getPageableParameterName();
|
||||
if (StringUtils.hasText(pageableParamterName)) {
|
||||
builder.beginControlFlow("if($L.isPaged())", pageableParamterName);
|
||||
builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName,
|
||||
pageableParamterName);
|
||||
if (context.returnsSlice() && !context.returnsPage()) {
|
||||
builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageableParamterName);
|
||||
} else {
|
||||
builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageableParamterName);
|
||||
}
|
||||
builder.endControlFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(countQueryStringNameVariableName)) {
|
||||
builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll");
|
||||
addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, query.isNativeQuery());
|
||||
builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName);
|
||||
|
||||
// end control flow does not work well with lambdas
|
||||
builder.unindent();
|
||||
builder.add("};\n");
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName,
|
||||
boolean nativeQuery) {
|
||||
|
||||
builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName,
|
||||
context.fieldNameOf(EntityManager.class), nativeQuery ? "createNativeQuery" : "createQuery",
|
||||
queryStringNameVariableName);
|
||||
|
||||
for (ParameterBinding binding : query.parameterBindings()) {
|
||||
|
||||
Object prepare = binding.prepare("s");
|
||||
if (prepare instanceof String prepared && !prepared.equals("s")) {
|
||||
String format = prepared.replaceAll("%", "%%").replace("s", "%s");
|
||||
if (binding.getIdentifier().hasPosition()) {
|
||||
builder.addStatement("$L.setParameter($L, $S.formatted($L))", queryVariableName,
|
||||
binding.getIdentifier().getPosition(), format,
|
||||
context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1));
|
||||
} else {
|
||||
builder.addStatement("$L.setParameter($S, $S.formatted($L))", queryVariableName,
|
||||
binding.getIdentifier().getName(), format, binding.getIdentifier().getName());
|
||||
}
|
||||
} else {
|
||||
if (binding.getIdentifier().hasPosition()) {
|
||||
builder.addStatement("$L.setParameter($L, $L)", queryVariableName, binding.getIdentifier().getPosition(),
|
||||
context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1));
|
||||
} else {
|
||||
builder.addStatement("$L.setParameter($S, $L)", queryVariableName, binding.getIdentifier().getName(),
|
||||
binding.getIdentifier().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2024 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.generated;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
|
||||
import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder;
|
||||
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
|
||||
import org.springframework.data.repository.aot.generate.RepositoryContributor;
|
||||
import org.springframework.data.repository.config.AotRepositoryContext;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.javapoet.TypeName;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class JpaRepsoitoryContributor extends RepositoryContributor {
|
||||
|
||||
AotQueryCreator queryCreator;
|
||||
AotMetaModel metaModel;
|
||||
|
||||
public JpaRepsoitoryContributor(AotRepositoryContext repositoryContext) {
|
||||
super(repositoryContext);
|
||||
|
||||
metaModel = new AotMetaModel(repositoryContext.getResolvedTypes());
|
||||
this.queryCreator = new AotQueryCreator(metaModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
|
||||
constructorBuilder.addParameter("entityManager", TypeName.get(EntityManager.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AotRepositoryMethodBuilder contributeRepositoryMethod(
|
||||
AotRepositoryMethodGenerationContext generationContext) {
|
||||
|
||||
{
|
||||
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class);
|
||||
if (queryAnnotation != null) {
|
||||
if (StringUtils.hasText(queryAnnotation.value())
|
||||
&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> {
|
||||
|
||||
Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class);
|
||||
if (query != null && StringUtils.hasText(query.value())) {
|
||||
|
||||
AotStringQuery aotStringQuery = query.nativeQuery() ? AotStringQuery.nativeQuery(query.value())
|
||||
: AotStringQuery.of(query.value());
|
||||
aotStringQuery.setCountQuery(query.countQuery());
|
||||
body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
|
||||
|
||||
body.addCode(
|
||||
|
||||
JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(aotStringQuery).build());
|
||||
} else {
|
||||
|
||||
PartTree partTree = new PartTree(context.getMethod().getName(),
|
||||
context.getRepositoryInformation().getDomainType());
|
||||
|
||||
CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory();
|
||||
|
||||
boolean isProjecting = context.getActualReturnType() != null
|
||||
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
|
||||
context.getActualReturnType());
|
||||
|
||||
Class<?> actualReturnType = context.getRepositoryInformation().getDomainType();
|
||||
try {
|
||||
actualReturnType = isProjecting
|
||||
? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader())
|
||||
: context.getRepositoryInformation().getDomainType();
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
ReturnedType returnedType = ReturnedType.of(actualReturnType,
|
||||
context.getRepositoryInformation().getDomainType(), projectionFactory);
|
||||
AotStringQuery stringQuery = queryCreator.createQuery(partTree, returnedType, context);
|
||||
|
||||
body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
|
||||
body.addCode(
|
||||
JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(stringQuery).build());
|
||||
}
|
||||
body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).referencing("query").build());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.config;
|
||||
|
||||
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*;
|
||||
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME;
|
||||
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_CONTEXT_BEAN_NAME;
|
||||
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_MAPPING_CONTEXT_BEAN_NAME;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
@@ -41,23 +43,34 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
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;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.aot.generated.JpaRepsoitoryContributor;
|
||||
import org.springframework.data.jpa.repository.support.DefaultJpaContext;
|
||||
import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor;
|
||||
import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension;
|
||||
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
|
||||
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;
|
||||
@@ -193,7 +206,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
|
||||
contextDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
|
||||
|
||||
return contextDefinition;
|
||||
|
||||
}, registry, JPA_CONTEXT_BEAN_NAME, source);
|
||||
|
||||
registerIfNotAlreadyRegistered(() -> new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), registry,
|
||||
@@ -211,7 +223,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
|
||||
builder.addConstructorArgValue(value);
|
||||
|
||||
return builder.getBeanDefinition();
|
||||
|
||||
}, registry, JpaEvaluationContextExtension.class.getName(), source);
|
||||
}
|
||||
|
||||
@@ -316,8 +327,131 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
|
||||
*/
|
||||
public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor {
|
||||
|
||||
protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
|
||||
protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
|
||||
|
||||
// don't register domain types nor annotations.
|
||||
|
||||
if (!AotContext.aotGeneratedRepositoriesEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new JpaRepsoitoryContributor(repositoryContext);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected RepositoryConfiguration<?> getRepositoryMetadata(RegisteredBean bean) {
|
||||
RepositoryConfiguration<?> configuration = super.getRepositoryMetadata(bean);
|
||||
if (!configuration.getRepositoryBaseClassName().isEmpty()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ import org.springframework.util.Assert;
|
||||
* @author Christoph Strobl
|
||||
* @author Jinmyeong Kim
|
||||
*/
|
||||
class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
|
||||
public class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
|
||||
|
||||
private final ReturnedType returnedType;
|
||||
private final ParameterMetadataProvider provider;
|
||||
@@ -86,15 +86,21 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
|
||||
JpqlQueryTemplates templates, EntityManager em) {
|
||||
|
||||
this(tree, type, provider, templates, em.getMetamodel());
|
||||
}
|
||||
|
||||
public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
|
||||
JpqlQueryTemplates templates, Metamodel metamodel) {
|
||||
|
||||
super(tree);
|
||||
this.tree = tree;
|
||||
this.returnedType = type;
|
||||
this.provider = provider;
|
||||
this.templates = templates;
|
||||
this.escape = provider.getEscape();
|
||||
this.entityType = em.getMetamodel().entity(type.getDomainType());
|
||||
this.entityType = metamodel.entity(type.getDomainType());
|
||||
this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType());
|
||||
this.metamodel = em.getMetamodel();
|
||||
this.metamodel = metamodel;
|
||||
}
|
||||
|
||||
Bindable<?> getFrom() {
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class ParameterBinding {
|
||||
public class ParameterBinding {
|
||||
|
||||
private final BindingIdentifier identifier;
|
||||
private final ParameterOrigin origin;
|
||||
@@ -462,7 +462,7 @@ class ParameterBinding {
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
|
||||
public sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
|
||||
|
||||
/**
|
||||
* Creates an identifier for the given {@code name}.
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* 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.query;
|
||||
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.expression.ValueExpression;
|
||||
import org.springframework.data.expression.ValueExpressionParser;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
|
||||
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
|
||||
import org.springframework.data.repository.query.parser.Part.Type;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
public enum ParameterBindingParser {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
|
||||
public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
|
||||
// .....................................................................^ not followed by a hash or a letter.
|
||||
// .................................................................^ zero or more digits.
|
||||
// .............................................................^ start with a question mark.
|
||||
private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
|
||||
private static final Pattern PARAMETER_BINDING_PATTERN;
|
||||
private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit]
|
||||
private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit]
|
||||
private static final Pattern NAMED_STYLE_PARAM = Pattern.compile("(?!\\\\):\\w+"); // no \ and :[text]
|
||||
|
||||
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type; "
|
||||
+ "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding";
|
||||
private static final int INDEXED_PARAMETER_GROUP = 4;
|
||||
private static final int NAMED_PARAMETER_GROUP = 6;
|
||||
private static final int COMPARISION_TYPE_GROUP = 1;
|
||||
|
||||
public static class Metadata {
|
||||
private boolean usesJdbcStyleParameters = false;
|
||||
|
||||
public boolean usesJdbcStyleParameters() {
|
||||
return usesJdbcStyleParameters;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are
|
||||
* bound to potentially unique query parameters for {@link LikeParameterBinding#prepare(Object) LIKE rewrite}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1.2
|
||||
*/
|
||||
static class ParameterBindings {
|
||||
|
||||
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
|
||||
|
||||
private final Consumer<ParameterBinding> registration;
|
||||
private int syntheticParameterIndex;
|
||||
|
||||
public ParameterBindings(List<ParameterBinding> bindings, Consumer<ParameterBinding> registration,
|
||||
int syntheticParameterIndex) {
|
||||
|
||||
for (ParameterBinding binding : bindings) {
|
||||
this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding)));
|
||||
}
|
||||
|
||||
this.registration = registration;
|
||||
this.syntheticParameterIndex = syntheticParameterIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the identifier is already bound.
|
||||
*
|
||||
* @param identifier
|
||||
* @return
|
||||
*/
|
||||
public boolean isBound(BindingIdentifier identifier) {
|
||||
return !getBindings(identifier).isEmpty();
|
||||
}
|
||||
|
||||
BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
|
||||
Function<BindingIdentifier, ParameterBinding> bindingFactory) {
|
||||
|
||||
Assert.isInstanceOf(MethodInvocationArgument.class, origin);
|
||||
|
||||
BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier();
|
||||
List<ParameterBinding> bindingsForOrigin = getBindings(methodArgument);
|
||||
|
||||
if (!isBound(identifier)) {
|
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier);
|
||||
registration.accept(binding);
|
||||
bindingsForOrigin.add(binding);
|
||||
return binding.getIdentifier();
|
||||
}
|
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier);
|
||||
|
||||
for (ParameterBinding existing : bindingsForOrigin) {
|
||||
|
||||
if (existing.isCompatibleWith(binding)) {
|
||||
return existing.getIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
BindingIdentifier syntheticIdentifier;
|
||||
if (identifier.hasName() && methodArgument.hasName()) {
|
||||
|
||||
int index = 0;
|
||||
String newName = methodArgument.getName();
|
||||
while (existsBoundParameter(newName)) {
|
||||
index++;
|
||||
newName = methodArgument.getName() + "_" + index;
|
||||
}
|
||||
syntheticIdentifier = BindingIdentifier.of(newName);
|
||||
} else {
|
||||
syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex);
|
||||
}
|
||||
|
||||
ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier);
|
||||
registration.accept(newBinding);
|
||||
bindingsForOrigin.add(newBinding);
|
||||
return newBinding.getIdentifier();
|
||||
}
|
||||
|
||||
private boolean existsBoundParameter(String key) {
|
||||
return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream)
|
||||
.anyMatch(it -> key.equals(it.getName()));
|
||||
}
|
||||
|
||||
private List<ParameterBinding> getBindings(BindingIdentifier identifier) {
|
||||
return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>());
|
||||
}
|
||||
|
||||
public void register(ParameterBinding parameterBinding) {
|
||||
registration.accept(parameterBinding);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
|
||||
List<String> keywords = new ArrayList<>();
|
||||
|
||||
for (ParameterBindingType type : ParameterBindingType.values()) {
|
||||
if (type.getKeyword() != null) {
|
||||
keywords.add(type.getKeyword());
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("(");
|
||||
builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
|
||||
builder.append(")?");
|
||||
builder.append("(?: )?"); // some whitespace
|
||||
builder.append("\\(?"); // optional braces around parameters
|
||||
builder.append("(");
|
||||
builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
|
||||
builder.append("|"); // or
|
||||
|
||||
// named parameter and the parameter name
|
||||
builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
|
||||
|
||||
builder.append(")");
|
||||
builder.append("\\)?"); // optional braces around parameters
|
||||
|
||||
PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
|
||||
* the cleaned up query.
|
||||
*/
|
||||
public String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List<ParameterBinding> bindings,
|
||||
Metadata queryMeta) {
|
||||
|
||||
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
|
||||
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
|
||||
|
||||
/*
|
||||
* Prefer indexed access over named parameters if only SpEL Expression parameters are present.
|
||||
*/
|
||||
if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
|
||||
parametersShouldBeAccessedByIndex = true;
|
||||
greatestParameterIndex = 0;
|
||||
}
|
||||
|
||||
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query,
|
||||
parametersShouldBeAccessedByIndex,
|
||||
greatestParameterIndex);
|
||||
|
||||
String resultingQuery = parsedQuery.getQueryString();
|
||||
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
|
||||
|
||||
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
|
||||
int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size();
|
||||
|
||||
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings),
|
||||
syntheticParameterIndex);
|
||||
int currentIndex = 0;
|
||||
|
||||
boolean usesJpaStyleParameters = false;
|
||||
|
||||
while (matcher.find()) {
|
||||
|
||||
if (parsedQuery.isQuoted(matcher.start())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP);
|
||||
String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
|
||||
Integer parameterIndex = getParameterIndex(parameterIndexString);
|
||||
|
||||
String match = matcher.group(0);
|
||||
if (JDBC_STYLE_PARAM.matcher(match).find()) {
|
||||
queryMeta.usesJdbcStyleParameters = true;
|
||||
}
|
||||
|
||||
if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) {
|
||||
usesJpaStyleParameters = true;
|
||||
}
|
||||
|
||||
if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
|
||||
throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported");
|
||||
}
|
||||
|
||||
String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
|
||||
Assert.isTrue(parameterIndexString != null || parameterName != null,
|
||||
() -> String.format("We need either a name or an index; Offending query string: %s", query));
|
||||
ValueExpression expression = parsedQuery
|
||||
.getParameter(parameterName == null ? parameterIndexString : parameterName);
|
||||
String replacement = null;
|
||||
|
||||
expressionParameterIndex++;
|
||||
if ("".equals(parameterIndexString)) {
|
||||
parameterIndex = expressionParameterIndex;
|
||||
}
|
||||
|
||||
BindingIdentifier queryParameter;
|
||||
if (parameterIndex != null) {
|
||||
queryParameter = BindingIdentifier.of(parameterIndex);
|
||||
} else {
|
||||
queryParameter = BindingIdentifier.of(parameterName);
|
||||
}
|
||||
ParameterOrigin origin = ObjectUtils.isEmpty(expression)
|
||||
? ParameterOrigin.ofParameter(parameterName, parameterIndex)
|
||||
: ParameterOrigin.ofExpression(expression);
|
||||
|
||||
BindingIdentifier targetBinding = queryParameter;
|
||||
Function<BindingIdentifier, ParameterBinding> bindingFactory = switch (ParameterBindingType.of(typeSource)) {
|
||||
case LIKE -> {
|
||||
|
||||
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
|
||||
yield (identifier) -> new LikeParameterBinding(identifier, origin, likeType);
|
||||
}
|
||||
case IN -> (identifier) -> new InParameterBinding(identifier, origin); // fall-through we don't need a special parameter queryParameter for the given parameter.
|
||||
default -> (identifier) -> new ParameterBinding(identifier, origin);
|
||||
};
|
||||
|
||||
if (origin.isExpression()) {
|
||||
parameterBindings.register(bindingFactory.apply(queryParameter));
|
||||
} else {
|
||||
targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory);
|
||||
}
|
||||
|
||||
replacement = targetBinding.hasName() ? ":" + targetBinding.getName()
|
||||
: ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?"
|
||||
: "?" + targetBinding.getPosition());
|
||||
String result;
|
||||
String substring = matcher.group(2);
|
||||
|
||||
int index = resultingQuery.indexOf(substring, currentIndex);
|
||||
if (index < 0) {
|
||||
result = resultingQuery;
|
||||
} else {
|
||||
currentIndex = index + replacement.length();
|
||||
result = resultingQuery.substring(0, index) + replacement
|
||||
+ resultingQuery.substring(index + substring.length());
|
||||
}
|
||||
|
||||
resultingQuery = result;
|
||||
}
|
||||
|
||||
return resultingQuery;
|
||||
}
|
||||
|
||||
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel,
|
||||
boolean parametersShouldBeAccessedByIndex,
|
||||
int greatestParameterIndex) {
|
||||
|
||||
/*
|
||||
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to
|
||||
* not mix-up with the actual parameter indices.
|
||||
*/
|
||||
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
|
||||
|
||||
BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
|
||||
? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
|
||||
: (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);
|
||||
|
||||
String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
|
||||
|
||||
BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
|
||||
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(),
|
||||
indexToParameterName, parameterNameToReplacement);
|
||||
|
||||
return rewriter.parse(queryWithSpel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Integer getParameterIndex(@Nullable String parameterIndexString) {
|
||||
|
||||
if (parameterIndexString == null || parameterIndexString.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Integer.valueOf(parameterIndexString);
|
||||
}
|
||||
|
||||
private static int tryFindGreatestParameterIndexIn(String query) {
|
||||
|
||||
Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
|
||||
|
||||
int greatestParameterIndex = -1;
|
||||
while (parameterIndexMatcher.find()) {
|
||||
|
||||
String parameterIndexString = parameterIndexMatcher.group(1);
|
||||
Integer parameterIndex = getParameterIndex(parameterIndexString);
|
||||
if (parameterIndex != null) {
|
||||
greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return greatestParameterIndex;
|
||||
}
|
||||
|
||||
private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
|
||||
|
||||
bindings.stream() //
|
||||
.filter(it -> it.bindsTo(binding)) //
|
||||
.forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
|
||||
|
||||
if (!bindings.contains(binding)) {
|
||||
bindings.add(binding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum for the different types of bindings.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private enum ParameterBindingType {
|
||||
|
||||
// Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
|
||||
// character, while = does not.
|
||||
LIKE("like "), IN("in "), AS_IS(null);
|
||||
|
||||
private final @Nullable String keyword;
|
||||
|
||||
ParameterBindingType(@Nullable String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a
|
||||
* keyword.
|
||||
*
|
||||
* @return the keyword
|
||||
*/
|
||||
@Nullable
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in
|
||||
* case no other {@link ParameterBindingType} could be found.
|
||||
*/
|
||||
static ParameterBindingType of(String typeSource) {
|
||||
|
||||
if (!StringUtils.hasText(typeSource)) {
|
||||
return AS_IS;
|
||||
}
|
||||
|
||||
for (ParameterBindingType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(typeSource.trim())) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static java.util.regex.Pattern.*;
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.data.expression.ValueExpression;
|
||||
import org.springframework.data.expression.ValueExpressionParser;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
|
||||
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -463,7 +464,7 @@ final class PreprocessedQuery implements DeclaredQuery {
|
||||
*/
|
||||
private static class ParameterBindings {
|
||||
|
||||
private final MultiValueMap<ParameterBinding.BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
|
||||
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
|
||||
|
||||
private final Consumer<ParameterBinding> registration;
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ public abstract class QueryUtils {
|
||||
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
|
||||
* @since 2.7.8
|
||||
*/
|
||||
static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) {
|
||||
public static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) {
|
||||
|
||||
Assert.hasText(originalQuery, "OriginalQuery must not be null or empty");
|
||||
|
||||
|
||||
@@ -105,11 +105,12 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
|
||||
* fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is
|
||||
* available.
|
||||
*
|
||||
* @param factory may be {@literal null}.
|
||||
* @param resolver may be {@literal null}.
|
||||
*/
|
||||
@Autowired
|
||||
public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) {
|
||||
public void setQueryMethodFactory(ObjectProvider<JpaQueryMethodFactory> resolver) { // TODO: nullable insteand of ObjectProvider
|
||||
|
||||
JpaQueryMethodFactory factory = resolver.getIfAvailable();
|
||||
if (factory != null) {
|
||||
this.queryMethodFactory = factory;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 com.example;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
public class UserDtoProjection {
|
||||
|
||||
private final String firstname;
|
||||
private final String emailAddress;
|
||||
|
||||
public UserDtoProjection(String firstname, String emailAddress) {
|
||||
this.firstname = firstname;
|
||||
this.emailAddress = emailAddress;
|
||||
}
|
||||
|
||||
public String getFirstname() {
|
||||
return firstname;
|
||||
}
|
||||
|
||||
public String getEmailAddress() {
|
||||
return emailAddress;
|
||||
}
|
||||
}
|
||||
140
spring-data-jpa/src/test/java/com/example/UserRepository.java
Normal file
140
spring-data-jpa/src/test/java/com/example/UserRepository.java
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2024 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 com.example;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface UserRepository extends CrudRepository<User, Integer> {
|
||||
|
||||
List<User> findUserNoArgumentsBy();
|
||||
|
||||
User findOneByEmailAddress(String emailAddress);
|
||||
|
||||
Optional<User> findOptionalOneByEmailAddress(String emailAddress);
|
||||
|
||||
Long countUsersByLastname(String lastname);
|
||||
|
||||
Boolean existsUserByLastname(String lastname);
|
||||
|
||||
List<User> findByLastnameStartingWith(String lastname);
|
||||
|
||||
List<User> findTop2ByLastnameStartingWith(String lastname);
|
||||
|
||||
List<User> findByLastnameStartingWithOrderByEmailAddress(String lastname);
|
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Limit limit);
|
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Sort sort);
|
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Sort sort, Limit limit);
|
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Pageable page);
|
||||
|
||||
Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page);
|
||||
|
||||
Slice<User> findSliceOfUserByLastnameStartingWith(String lastname, Pageable page);
|
||||
|
||||
/* Annotated Queries */
|
||||
|
||||
@Query("select u from User u where u.emailAddress = ?1")
|
||||
User findAnnotatedQueryByEmailAddress(String username);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
List<User> findAnnotatedQueryByLastname(String lastname);
|
||||
|
||||
@Query("select u from User u where u.lastname like :lastname%")
|
||||
List<User> findAnnotatedQueryByLastnameParamter(String lastname);
|
||||
|
||||
@Query("""
|
||||
select u
|
||||
from User u
|
||||
where u.lastname LIKE ?1%""")
|
||||
List<User> findAnnotatedMultilineQueryByLastname(String username);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
List<User> findAnnotatedQueryByLastname(String lastname, Sort sort);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
List<User> findAnnotatedQueryByLastname(String lastname, Pageable pageable);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
Page<User> findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable);
|
||||
|
||||
@Query("select u from User u where u.lastname like ?1%")
|
||||
Slice<User> findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable);
|
||||
|
||||
// modifying
|
||||
|
||||
User deleteByEmailAddress(String username);
|
||||
|
||||
Long deleteReturningDeleteCountByEmailAddress(String username);
|
||||
|
||||
@Modifying
|
||||
@Query("delete from User u where u.emailAddress = ?1")
|
||||
User deleteAnnotatedQueryByEmailAddress(String username);
|
||||
|
||||
// native queries
|
||||
|
||||
@Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User",
|
||||
nativeQuery = true)
|
||||
Page<String> findByNativeQueryWithPageable(Pageable pageable);
|
||||
|
||||
// projections
|
||||
|
||||
|
||||
|
||||
List<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname);
|
||||
|
||||
Page<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
|
||||
|
||||
// old ones
|
||||
|
||||
@Query("select u from User u where u.firstname = ?1")
|
||||
List<User> findAllUsingAnnotatedJpqlQuery(String firstname);
|
||||
|
||||
List<User> findByLastname(String lastname);
|
||||
|
||||
List<User> findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit);
|
||||
|
||||
List<User> findByLastname(String lastname, Sort sort);
|
||||
|
||||
List<User> findByLastname(String lastname, Pageable page);
|
||||
|
||||
List<User> findByLastnameOrderByFirstname(String lastname);
|
||||
|
||||
User findByEmailAddress(String emailAddress);
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
* Copyright 2024 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.generated;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.test.tools.TestCompiler;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import com.example.UserDtoProjection;
|
||||
import com.example.UserRepository;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class JpaRepositoryContributorUnitTests {
|
||||
|
||||
private static Verifyer generated;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
|
||||
TestJpaAotRepsitoryContext aotContext = new TestJpaAotRepsitoryContext(UserRepository.class, null);
|
||||
TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class);
|
||||
|
||||
new JpaRepsoitoryContributor(aotContext).contribute(generationContext);
|
||||
|
||||
AbstractBeanDefinition emBeanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition("org.springframework.orm.jpa.SharedEntityManagerCreator")
|
||||
.setFactoryMethod("createSharedEntityManager").addConstructorArgReference("entityManagerFactory")
|
||||
.setLazyInit(true).getBeanDefinition();
|
||||
|
||||
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
|
||||
.genericBeanDefinition("com.example.UserRepositoryImpl__Aot")
|
||||
.addConstructorArgReference("jpaSharedEM_entityManagerFactory").getBeanDefinition();
|
||||
|
||||
|
||||
/*
|
||||
alter the RepositoryFactory so we can write generated calsses into a supplier and then write some custom code for instantiation
|
||||
on JpaRepositoryFactoryBean
|
||||
|
||||
beanDefinition.getPropertyValues().addPropertyValue("aotImplementation", new Function<BeanFactory, Instance>() {
|
||||
|
||||
public Instance apply(BeanFactory beanFactor) {
|
||||
EntityManager em = beanFactory.getBean(EntityManger.class);
|
||||
return new com.example.UserRepositoryImpl__Aot(em);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// register a dedicated factory that can read stuff
|
||||
// don't write to spring.factories or uas another name for it
|
||||
// maybe write the code directly to a repo fragment
|
||||
// repo does not have to be a bean, but can be a method called by some component
|
||||
// pass list to entiy manager to have stuff in memory have to list written out directly when creating the bean
|
||||
|
||||
generated = generateContext(generationContext) //
|
||||
.registerBeansFrom(new ClassPathResource("infrastructure.xml"))
|
||||
.register("jpaSharedEM_entityManagerFactory", emBeanDefinition)
|
||||
.register("aotUserRepository", aotGeneratedRepository);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
|
||||
generated.doWithBean(EntityManager.class, em -> {
|
||||
|
||||
em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate();
|
||||
|
||||
User luke = new User("Luke", "Skywalker", "luke@jedi.org");
|
||||
em.persist(luke);
|
||||
|
||||
User leia = new User("Leia", "Organa", "leia@resistance.gov");
|
||||
em.persist(leia);
|
||||
|
||||
User han = new User("Han", "Solo", "han@smuggler.net");
|
||||
em.persist(han);
|
||||
|
||||
User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net");
|
||||
em.persist(chewbacca);
|
||||
|
||||
User yoda = new User("Yoda", "n/a", "yoda@jedi.org");
|
||||
em.persist(yoda);
|
||||
|
||||
User vader = new User("Anakin", "Skywalker", "vader@empire.com");
|
||||
em.persist(vader);
|
||||
|
||||
User kylo = new User("Ben", "Solo", "kylo@new-empire.com");
|
||||
em.persist(kylo);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindDerivedFinderSingleEntity() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
User user = methodInvoker.invoke("findByEmailAddress", "luke@jedi.org").onBean("aotUserRepository");
|
||||
assertThat(user.getLastname()).isEqualTo("Skywalker");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindDerivedFinderOptionalEntity() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Optional<User> user = methodInvoker.invoke("findOptionalOneByEmailAddress", "yoda@jedi.org")
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(user).isNotNull().containsInstanceOf(User.class)
|
||||
.hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedCount() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository");
|
||||
assertThat(value).isEqualTo(2L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedExists() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository");
|
||||
assertThat(exists).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderWithoutArguments() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository");
|
||||
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningList() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com",
|
||||
"kylo@new-empire.com", "han@smuggler.net");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLimitedDerivedFinder() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository");
|
||||
assertThat(users).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortedDerivedFinder() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findByLastnameStartingWithOrderByEmailAddress", "S")
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
|
||||
"luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderWithLimitArgument() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderWithSort() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
|
||||
"luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderWithSortAndLimit() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"), Limit.of(2))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningListWithPageable() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker
|
||||
.invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningPage() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Page<User> page = methodInvoker
|
||||
.invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(page.getTotalElements()).isEqualTo(4);
|
||||
assertThat(page.getSize()).isEqualTo(2);
|
||||
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
|
||||
"kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningSlice() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Slice<User> slice = methodInvoker
|
||||
.invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(slice.hasNext()).isTrue();
|
||||
assertThat(slice.getSize()).isEqualTo(2);
|
||||
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
|
||||
"kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderReturningSingleValueWithQuery() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
User user = methodInvoker.invoke("findAnnotatedQueryByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository");
|
||||
assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderReturningListWithQuery() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
|
||||
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastnameParamter", "S").onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
|
||||
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedMultilineFinderWithQuery() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
|
||||
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderWithQueryAndLimit() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderWithQueryAndSort() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("emailAddress"))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
|
||||
"luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderWithQueryLimitAndSort() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("emailAddress"))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderReturningListWithPageable() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<User> users = methodInvoker
|
||||
.invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderReturningPage() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Page<User> page = methodInvoker
|
||||
.invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(page.getTotalElements()).isEqualTo(4);
|
||||
assertThat(page.getSize()).isEqualTo(2);
|
||||
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
|
||||
"kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAnnotatedFinderReturningSlice() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Slice<User> slice = methodInvoker
|
||||
.invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(slice.hasNext()).isTrue();
|
||||
assertThat(slice.getSize()).isEqualTo(2);
|
||||
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
|
||||
"kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningListOfProjections() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
List<UserDtoProjection> users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S")
|
||||
.onBean("aotUserRepository");
|
||||
assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
|
||||
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDerivedFinderReturningPageOfProjections() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Page<UserDtoProjection> page = methodInvoker
|
||||
.invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
|
||||
.onBean("aotUserRepository");
|
||||
|
||||
assertThat(page.getTotalElements()).isEqualTo(4);
|
||||
assertThat(page.getSize()).isEqualTo(2);
|
||||
assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net",
|
||||
"kylo@new-empire.com");
|
||||
});
|
||||
}
|
||||
|
||||
// modifying
|
||||
|
||||
@Test
|
||||
void testDerivedDeleteSingle() {
|
||||
|
||||
generated.verifyInTx(methodInvoker -> {
|
||||
|
||||
User result = methodInvoker.invoke("deleteByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository");
|
||||
|
||||
assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org");
|
||||
}).doWithBean(EntityManager.class, em -> {
|
||||
Object yodaShouldBeGone = em
|
||||
.createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName()))
|
||||
.getSingleResultOrNull();
|
||||
assertThat(yodaShouldBeGone).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
// native queries
|
||||
|
||||
@Test
|
||||
void nativeQuery() {
|
||||
|
||||
generated.verify(methodInvoker -> {
|
||||
|
||||
Page<String> page = methodInvoker
|
||||
.invoke("findByNativeQueryWithPageable", PageRequest.of(0, 2))
|
||||
.onBean("aotUserRepository");
|
||||
|
||||
assertThat(page.getTotalElements()).isEqualTo(7);
|
||||
assertThat(page.getSize()).isEqualTo(2);
|
||||
assertThat(page.getContent()).containsExactly("Anakin", "Ben");
|
||||
});
|
||||
}
|
||||
|
||||
// old stuff below
|
||||
|
||||
// TODO:
|
||||
void todo() {
|
||||
|
||||
// Query q;
|
||||
// q.setMaxResults()
|
||||
// q.setFirstResult()
|
||||
|
||||
// 1 build some more stuff from below
|
||||
// 2 set up boot sample project in data samples
|
||||
|
||||
// query hints
|
||||
// first and max result for pagination
|
||||
// entity graphs
|
||||
// native queries
|
||||
// delete
|
||||
// @Modifying
|
||||
// flush / clear
|
||||
}
|
||||
|
||||
static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) {
|
||||
return new GeneratedContextBuilder(generationContext);
|
||||
}
|
||||
|
||||
static class GeneratedContextBuilder implements Verifyer {
|
||||
|
||||
TestGenerationContext generationContext;
|
||||
Map<String, BeanDefinition> beanDefinitions = new LinkedHashMap<>();
|
||||
Resource xmlBeanDefinitions;
|
||||
Lazy<DefaultListableBeanFactory> lazyFactory;
|
||||
|
||||
public GeneratedContextBuilder(TestGenerationContext generationContext) {
|
||||
|
||||
this.generationContext = generationContext;
|
||||
this.lazyFactory = Lazy.of(() -> {
|
||||
DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory();
|
||||
TestCompiler.forSystem().with(generationContext).compile(compiled -> {
|
||||
|
||||
freshBeanFactory.setBeanClassLoader(compiled.getClassLoader());
|
||||
if (xmlBeanDefinitions != null) {
|
||||
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(freshBeanFactory);
|
||||
beanDefinitionReader.loadBeanDefinitions(xmlBeanDefinitions);
|
||||
}
|
||||
|
||||
for (Entry<String, BeanDefinition> entry : beanDefinitions.entrySet()) {
|
||||
freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue());
|
||||
}
|
||||
});
|
||||
return freshBeanFactory;
|
||||
});
|
||||
}
|
||||
|
||||
GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) {
|
||||
this.beanDefinitions.put(name, beanDefinition);
|
||||
return this;
|
||||
}
|
||||
|
||||
GeneratedContextBuilder registerBeansFrom(Resource xmlBeanDefinitions) {
|
||||
this.xmlBeanDefinitions = xmlBeanDefinitions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Verifyer verify(Consumer<GeneratedContext> methodInvoker) {
|
||||
methodInvoker.accept(new GeneratedContext(lazyFactory));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface Verifyer {
|
||||
Verifyer verify(Consumer<GeneratedContext> methodInvoker);
|
||||
|
||||
default Verifyer verifyInTx(Consumer<GeneratedContext> methodInvoker) {
|
||||
|
||||
verify(ctx -> {
|
||||
|
||||
PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class);
|
||||
new TransactionTemplate(txMgr).execute(action -> {
|
||||
verify(methodInvoker);
|
||||
return "ok";
|
||||
});
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
default <T> void doWithBean(Class<T> type, Consumer<T> runit) {
|
||||
verify(ctx -> {
|
||||
|
||||
boolean isEntityManager = type == EntityManager.class;
|
||||
T bean = ctx.delegate.get().getBean(type);
|
||||
|
||||
if (!isEntityManager) {
|
||||
runit.accept(bean);
|
||||
} else {
|
||||
|
||||
PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class);
|
||||
new TransactionTemplate(txMgr).execute(action -> {
|
||||
runit.accept(bean);
|
||||
return "ok";
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static class GeneratedContext {
|
||||
|
||||
private Supplier<DefaultListableBeanFactory> delegate;
|
||||
|
||||
public GeneratedContext(Supplier<DefaultListableBeanFactory> defaultListableBeanFactory) {
|
||||
this.delegate = defaultListableBeanFactory;
|
||||
}
|
||||
|
||||
InvocationBuilder invoke(String method, Object... arguments) {
|
||||
|
||||
return new InvocationBuilder() {
|
||||
@Override
|
||||
public <T> T onBean(String beanName) {
|
||||
DefaultListableBeanFactory defaultListableBeanFactory = delegate.get();
|
||||
|
||||
Object bean = defaultListableBeanFactory.getBean(beanName);
|
||||
return ReflectionTestUtils.invokeMethod(bean, method, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface InvocationBuilder {
|
||||
<T> T onBean(String beanName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2024 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.generated;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
|
||||
import org.springframework.data.repository.core.CrudMethods;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
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 org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class StubRepositoryInformation implements RepositoryInformation {
|
||||
|
||||
private final RepositoryMetadata metadata;
|
||||
private final RepositoryComposition baseComposition;
|
||||
|
||||
public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
|
||||
|
||||
this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface);
|
||||
this.baseComposition = composition != null ? composition
|
||||
: RepositoryComposition.of(RepositoryFragment.structural(SimpleJpaRepository.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getIdTypeInformation() {
|
||||
return metadata.getIdTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getDomainTypeInformation() {
|
||||
return metadata.getDomainTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getRepositoryInterface() {
|
||||
return metadata.getRepositoryInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getReturnType(Method method) {
|
||||
return metadata.getReturnType(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnedDomainClass(Method method) {
|
||||
return metadata.getReturnedDomainClass(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrudMethods getCrudMethods() {
|
||||
return metadata.getCrudMethods();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPagingRepository() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<?>> getAlternativeDomainTypes() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReactiveRepository() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RepositoryFragment<?>> getFragments() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBaseClassMethod(Method method) {
|
||||
return baseComposition.findMethod(method).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustomMethod(Method method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isQueryMethod(Method method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Streamable<Method> getQueryMethods() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getRepositoryBaseClass() {
|
||||
return SimpleJpaRepository.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getTargetClassMethod(Method method) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2024 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.generated;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.test.tools.ClassFile;
|
||||
import org.springframework.data.jpa.domain.sample.Role;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.repository.config.AotRepositoryContext;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.support.RepositoryComposition;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class TestJpaAotRepsitoryContext implements AotRepositoryContext {
|
||||
|
||||
private final StubRepositoryInformation repositoryInformation;
|
||||
|
||||
public TestJpaAotRepsitoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
|
||||
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigurableListableBeanFactory getBeanFactory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeIntrospector introspectType(String typeName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanName() {
|
||||
return "dummyRepository";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getBasePackages() {
|
||||
return Set.of("org.springframework.data.dummy.repository.aot");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() {
|
||||
return Set.of(Entity.class, MappedSuperclass.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryInformation getRepositoryInformation() {
|
||||
return repositoryInformation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<?>> getResolvedTypes() {
|
||||
return Set.of(User.class, Role.class);
|
||||
}
|
||||
|
||||
public List<ClassFile> getRequiredContextFiles() {
|
||||
return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass()));
|
||||
}
|
||||
|
||||
static ClassFile classFileForType(Class<?> type) {
|
||||
|
||||
String name = type.getName();
|
||||
ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class");
|
||||
|
||||
try {
|
||||
return ClassFile.of(name, cpr.getContentAsByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.springframework.data.jpa.repository.query.ParameterBinding.InParamete
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBindingParser.Metadata;
|
||||
import org.springframework.data.repository.query.parser.Part.Type;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package org.springframework.data.jpa.repository.sample;
|
||||
|
||||
// DATAJPA-1334
|
||||
class NameOnlyDto {
|
||||
public class NameOnlyDto {
|
||||
|
||||
private String firstname;
|
||||
private String lastname;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
<!-- <logger name="org.testcontainers" level="debug" />-->
|
||||
|
||||
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
|
||||
|
||||
<root level="error">
|
||||
<appender-ref ref="console"/>
|
||||
</root>
|
||||
|
||||
Reference in New Issue
Block a user