Introduce @Meta data support for repository methods.

Closes #775.
This commit is contained in:
Greg L. Turnquist
2022-07-11 14:31:16 -05:00
parent 36764e5f1a
commit aa6a809c31
15 changed files with 1164 additions and 21 deletions

View File

@@ -18,22 +18,22 @@ package org.springframework.data.jpa.provider;
import static org.springframework.data.jpa.provider.JpaClassUtils.*;
import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Set;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.metamodel.IdentifiableType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.SingularAttribute;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Set;
import org.eclipse.persistence.config.QueryHints;
import org.eclipse.persistence.jpa.JpaQuery;
import org.eclipse.persistence.queries.ScrollableCursor;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor;
import org.springframework.data.util.CloseableIterator;
@@ -49,8 +49,9 @@ import org.springframework.util.ConcurrentReferenceHashMap;
* @author Thomas Darimont
* @author Mark Paluch
* @author Jens Schauder
* @author Greg Turnquist
*/
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
/**
* Hibernate persistence provider.
@@ -102,9 +103,15 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
}
@Override
public JpaParametersParameterAccessor getParameterAccessor(JpaParameters parameters, Object[] values, EntityManager em) {
public JpaParametersParameterAccessor getParameterAccessor(JpaParameters parameters, Object[] values,
EntityManager em) {
return new HibernateJpaParametersParameterAccessor(parameters, values, em);
}
@Override
public String getCommentHintKey() {
return "org.hibernate.comment";
}
},
/**
@@ -133,6 +140,16 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
return new EclipseLinkScrollableResultsIterator<>(jpaQuery);
}
@Override
public String getCommentHintKey() {
return QueryHints.HINT;
}
@Override
public String getCommentHintValue(String comment) {
return "/* " + comment + " */";
}
},
/**
@@ -161,11 +178,18 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
public Object getIdentifierFrom(Object entity) {
return null;
}
@Nullable
@Override
public String getCommentHintKey() {
return null;
}
};
static ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
private final Iterable<String> entityManagerClassNames;
private final Iterable<String> metamodelClassNames;
/**
* Creates a new {@link PersistenceProvider}.
*
@@ -249,7 +273,8 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor {
return cacheAndReturn(metamodelType, GENERIC_JPA);
}
public JpaParametersParameterAccessor getParameterAccessor(JpaParameters parameters, Object[] values, EntityManager em) {
public JpaParametersParameterAccessor getParameterAccessor(JpaParameters parameters, Object[] values,
EntityManager em) {
return new JpaParametersParameterAccessor(parameters, values);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2013-2022 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.provider;
import jakarta.persistence.Query;
import org.springframework.lang.Nullable;
/**
* Interface to hide different implementations of query hints that insert comments into a {@link Query}.
*
* @author Greg Turnquist
* @since 3.0
*/
public interface QueryComment {
@Nullable
String getCommentHintKey();
@Nullable
default String getCommentHintValue(String comment) {
return comment;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2014-2022 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to assign metadata to repository operations.
*
* @author Greg Turnquist
* @since 3.0
* @see org.springframework.data.jpa.repository.query.Meta
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface Meta {
/**
* Add a comment to the query.
*
* @return empty {@link String} by default.
*/
String comment() default "";
}

View File

@@ -187,6 +187,15 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
}
}
// Apply any meta-attributes that exist
if (method.hasQueryMetaAttributes()) {
if (provider.getCommentHintKey() != null) {
query.setHint( //
provider.getCommentHintKey(), provider.getCommentHintValue(method.getQueryMetaAttributes().getComment()));
}
}
return query;
}

View File

@@ -24,6 +24,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -32,6 +33,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
@@ -46,6 +48,7 @@ import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
/**
@@ -94,6 +97,7 @@ public class JpaQueryMethod extends QueryMethod {
private final Lazy<Boolean> isCollectionQuery;
private final Lazy<Boolean> isProcedureQuery;
private final Lazy<JpaEntityMetadata<?>> entityMetadata;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
/**
* Creates a {@link JpaQueryMethod}.
@@ -135,6 +139,7 @@ public class JpaQueryMethod extends QueryMethod {
this.isCollectionQuery = Lazy.of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(this.returnType));
this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null);
this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
this.annotationCache = new ConcurrentReferenceHashMap<>();
Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
String.format("Modifying method must not contain %s", Parameters.TYPES));
@@ -193,6 +198,13 @@ public class JpaQueryMethod extends QueryMethod {
return modifying.getNullable() != null;
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
}
/**
* Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints}
*
@@ -259,6 +271,47 @@ public class JpaQueryMethod extends QueryMethod {
return returnType;
}
/**
* @return return true if {@link Meta} annotation is available.
* @since 3.0
*/
public boolean hasQueryMetaAttributes() {
return getMetaAnnotation() != null;
}
/**
* Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available.
*
* @return
* @since 3.0
*/
@Nullable
Meta getMetaAnnotation() {
return doFindAnnotation(Meta.class).orElse(null);
}
/**
* Returns the {@link org.springframework.data.jpa.repository.query.Meta} attributes to be applied.
*
* @return never {@literal null}.
* @since 1.6
*/
public org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes() {
Meta meta = getMetaAnnotation();
if (meta == null) {
return new org.springframework.data.jpa.repository.query.Meta();
}
org.springframework.data.jpa.repository.query.Meta metaAttributes = new org.springframework.data.jpa.repository.query.Meta();
if (StringUtils.hasText(meta.comment())) {
metaAttributes.setComment(meta.comment());
}
return metaAttributes;
}
/**
* Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found
* nor the attribute was specified.

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2014-2022 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 java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Value object to hold metadata about repository methods.
*
* @author Greg Turnquist
* @since 3.0
* @see org.springframework.data.jpa.repository.Meta
*/
public class Meta {
private enum MetaKey {
COMMENT("comment");
private String key;
MetaKey(String key) {
this.key = key;
}
}
private Map<String, Object> values = Collections.emptyMap();
public Meta() {}
/**
* Copy a {@link Meta} object.
*
* @since 3.0
* @param source
*/
Meta(Meta source) {
this.values = new LinkedHashMap<>(source.values);
}
/**
* Add a comment to the query that is propagated to the profile log.
*
* @param comment
*/
public void setComment(String comment) {
setValue(MetaKey.COMMENT.key, comment);
}
/**
* @return {@literal null} if not set.
*/
@Nullable
public String getComment() {
return getValue(MetaKey.COMMENT.key);
}
/**
* @return
*/
public boolean hasValues() {
return !this.values.isEmpty();
}
/**
* Get {@link Iterable} of set meta values.
*
* @return
*/
public Iterable<Map.Entry<String, Object>> values() {
return Collections.unmodifiableSet(this.values.entrySet());
}
/**
* Sets or removes the value in case of {@literal null} or empty {@link String}.
*
* @param key must not be {@literal null} or empty.
* @param value
*/
void setValue(String key, @Nullable Object value) {
Assert.hasText(key, "Meta key must not be 'null' or blank");
if (values == Collections.EMPTY_MAP) {
values = new LinkedHashMap<>(2);
}
if (value == null || (value instanceof String && !StringUtils.hasText((String) value))) {
this.values.remove(key);
}
this.values.put(key, value);
}
@Nullable
@SuppressWarnings("unchecked")
private <T> T getValue(String key) {
return (T) this.values.get(key);
}
}

View File

@@ -15,11 +15,11 @@
*/
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.LockModeType;
import java.lang.reflect.Method;
import java.util.Optional;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.lang.Nullable;
@@ -32,6 +32,7 @@ import org.springframework.lang.Nullable;
* @author Christoph Strobl
* @author Mark Paluch
* @author Jens Schauder
* @author Greg Turnquist
*/
public interface CrudMethodMetadata {
@@ -59,6 +60,14 @@ public interface CrudMethodMetadata {
*/
QueryHints getQueryHintsForCount();
/**
* Returns query comment to be applied to query.
*
* @return
* @since 3.0
*/
String getComment();
/**
* Returns the {@link EntityGraph} to be used.
*

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Optional;
@@ -23,9 +26,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Predicate;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.TargetSource;
@@ -36,6 +36,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
@@ -180,6 +181,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
private final @Nullable LockModeType lockModeType;
private final org.springframework.data.jpa.repository.support.QueryHints queryHints;
private final org.springframework.data.jpa.repository.support.QueryHints queryHintsForCount;
private final String comment;
private final Optional<EntityGraph> entityGraph;
private final Method method;
@@ -195,6 +197,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
this.lockModeType = findLockModeType(method);
this.queryHints = findQueryHints(method, it -> true);
this.queryHintsForCount = findQueryHints(method, QueryHints::forCounting);
this.comment = findComment(method);
this.entityGraph = findEntityGraph(method);
this.method = method;
}
@@ -233,6 +236,13 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
return queryHints;
}
@Nullable
private static String findComment(Method method) {
Meta annotation = AnnotatedElementUtils.findMergedAnnotation(method, Meta.class);
return annotation == null ? null : (String) AnnotationUtils.getValue(annotation, "comment");
}
@Nullable
@Override
public LockModeType getLockModeType() {
@@ -249,6 +259,11 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
return queryHintsForCount;
}
@Override
public String getComment() {
return comment;
}
@Override
public Optional<EntityGraph> getEntityGraph() {
return entityGraph;

View File

@@ -238,6 +238,8 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
query.setParameter("ids", idsCollection);
}
applyQueryHints(query);
query.executeUpdate();
}
}
@@ -279,7 +281,12 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
@Override
@Transactional
public void deleteAllInBatch() {
em.createQuery(getDeleteAllQueryString()).executeUpdate();
Query query = em.createQuery(getDeleteAllQueryString());
applyQueryHints(query);
query.executeUpdate();
}
@Override
@@ -296,8 +303,13 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = new HashMap<>();
getQueryHints().withFetchGraphs(em).forEach(hints::put);
if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
hints.put(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
}
return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
@@ -355,6 +367,15 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
TypedQuery<Long> query = em.createQuery(existsQuery, Long.class);
Map<String, Object> hints = new HashMap<>();
getQueryHints().withFetchGraphs(em).forEach(hints::put);
if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
hints.put(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
}
hints.forEach(query::setHint);
if (!entityInformation.hasCompositeId()) {
query.setParameter(idAttributeNames.iterator().next(), id);
return query.getSingleResult() == 1L;
@@ -556,9 +577,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
Assert.notNull(spec, "Specification must not be null");
Assert.notNull(queryFunction, "Query function must not be null");
Function<Sort, TypedQuery<T>> finder = sort -> {
return getQuery(spec, getDomainClass(), sort);
};
Function<Sort, TypedQuery<T>> finder = sort -> getQuery(spec, getDomainClass(), sort);
FetchableFluentQuery<R> fluentQuery = new FetchableFluentQueryBySpecification<T, R>(spec, getDomainClass(),
Sort.unsorted(), null, finder, this::count, this::exists, this.em);
@@ -568,7 +587,12 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
@Override
public long count() {
return em.createQuery(getCountQueryString(), Long.class).getSingleResult();
TypedQuery<Long> query = em.createQuery(getCountQueryString(), Long.class);
applyQueryHintsForCount(query);
return query.getSingleResult();
}
@Override
@@ -804,7 +828,12 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
}
private void applyQueryHints(Query query) {
getQueryHints().withFetchGraphs(em).forEach(query::setHint);
if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
query.setHint(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
}
}
private <S> TypedQuery<S> applyRepositoryMethodMetadataForCount(TypedQuery<S> query) {
@@ -819,7 +848,12 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
}
private void applyQueryHintsForCount(Query query) {
getQueryHintsForCount().forEach(query::setHint);
if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
query.setHint(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
}
}
/**

View File

@@ -19,15 +19,15 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2013-2022 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 org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManagerFactory;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Properties;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.sample.RoleRepositoryWithMeta;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
/**
* Verify that {@link Meta}-annotated methods properly embed comments into EclipseLink queries.
*
* @author Greg Turnquist
* @since 3.0
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@Transactional
public class MetaAnnotatedQueryMethodEclipseLinkIntegrationTests {
@Autowired RoleRepositoryWithMeta repository;
private static final ResourceLoader RESOURCE_LOADER = new DefaultResourceLoader();
private static final String LOG_FILE = "test-eclipselink-meta.log";
@BeforeEach
void cleanoutLogfile() throws IOException {
new FileOutputStream(LOG_FILE).close();
}
@AfterAll
static void deleteLogfile() throws IOException {
FileSystemUtils.deleteRecursively(Path.of(LOG_FILE));
}
@Test // GH-775
void findAllShouldLogAComment() {
repository.findAll();
assertAtLeastOneComment();
}
@Test // GH-775
void findByIdShouldLogAComment() {
repository.findById(0);
assertAtLeastOneComment();
}
@Test // GH-775
void existsByIdShouldLogAComment() {
repository.existsById(0);
assertAtLeastOneComment();
}
@Test // GH-775
void customFinderShouldLogAComment() throws Exception {
repository.findByName("name");
assertAtLeastOneComment();
}
@Test // GH-775
void findOneWithExampleShouldLogAComment() {
repository.findOne(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void findAllWithExampleShouldLogAComment() {
repository.findAll(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void findAllWithExampleAndSortShouldLogAComment() {
repository.findAll(Example.of(new Role()), Sort.by("name"));
assertAtLeastOneComment();
}
@Test // GH-775
void findByFluentDslWithExampleShouldLogAComment() {
repository.findBy(Example.of(new Role()), FluentQuery.FetchableFluentQuery::all);
assertAtLeastOneComment();
}
@Test // GH-775
void existsByExampleShouldLogAComment() {
repository.exists(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void countShouldLogAComment() {
repository.count();
assertAtLeastOneComment();
}
@Test // GH-775
void customCountShouldLogAComment() {
repository.countByName("name");
assertAtLeastOneComment();
}
@Test // GH-775
void deleteAllInBatchShouldLogAComment() {
repository.deleteAllInBatch();
assertAtLeastOneComment();
}
void assertAtLeastOneComment() {
try (Reader reader = new InputStreamReader(RESOURCE_LOADER.getResource("file:" + LOG_FILE).getInputStream(),
StandardCharsets.UTF_8)) {
String logFileOutput = FileCopyUtils.copyToString(reader);
assertThat(logFileOutput).contains("/* foobar */");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Configuration
@EnableJpaRepositories(basePackages = "org.springframework.data.jpa.repository.sample")
static class Config {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().generateUniqueName(true).build();
}
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.put("eclipselink.weaving", "false");
properties.put("eclipselink.logging.level.sql", "FINE");
properties.put("eclipselink.logging.file", LOG_FILE);
return properties;
}
@Bean
public AbstractJpaVendorAdapter vendorAdaptor() {
EclipseLinkJpaVendorAdapter vendorAdapter = new EclipseLinkJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
vendorAdapter.setDatabase(Database.HSQL);
return vendorAdapter;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource());
factory.setPersistenceUnitName("spring-data-jpa");
factory.setJpaVendorAdapter(vendorAdaptor());
factory.setJpaProperties(jpaProperties());
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public JpaDialect jpaDialect() {
return new HibernateJpaDialect();
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2013-2022 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 org.assertj.core.api.Assertions.*;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import jakarta.persistence.EntityManagerFactory;
import java.util.List;
import java.util.Properties;
import java.util.function.Predicate;
import javax.sql.DataSource;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.sample.RoleRepositoryWithMeta;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
/**
* Verify that {@link Meta}-annotated methods properly embed comments into Hibernate queries.
*
* @author Greg Turnquist
* @since 3.0
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@Transactional
public class MetaAnnotatedQueryMethodHibernateIntegrationTests {
@Autowired RoleRepositoryWithMeta repository;
Logger testLogger = (Logger) LoggerFactory.getLogger("org.hibernate.SQL");
ListAppender<ILoggingEvent> testAppender;
@BeforeEach
void setUp() {
testAppender = new ListAppender<>();
testAppender.start();
testLogger.setLevel(Level.DEBUG);
testLogger.addAppender(testAppender);
}
@AfterEach
void clearUp() {
testLogger.detachAppender(testAppender);
}
@Test // GH-775
void findAllShouldLogAComment() {
repository.findAll();
assertAtLeastOneComment();
}
@Test // GH-775
void findByIdShouldNotLogAComment() {
repository.findById(0);
assertNoComments();
}
@Test // GH-775
void existsByIdShouldLogAComment() {
repository.existsById(0);
assertAtLeastOneComment();
}
@Test // GH-775
void customFinderShouldLogAComment() {
repository.findByName("name");
assertAtLeastOneComment();
}
@Test // GH-775
void findOneWithExampleShouldLogAComment() {
repository.findOne(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void findAllWithExampleShouldLogAComment() {
repository.findAll(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void findAllWithExampleAndSortShouldLogAComment() {
repository.findAll(Example.of(new Role()), Sort.by("name"));
assertAtLeastOneComment();
}
@Test // GH-775
void findByFluentDslWithExampleShouldLogAComment() {
repository.findBy(Example.of(new Role()), FluentQuery.FetchableFluentQuery::all);
assertAtLeastOneComment();
}
@Test // GH-775
void existsByExampleShouldLogAComment() {
repository.exists(Example.of(new Role()));
assertAtLeastOneComment();
}
@Test // GH-775
void countShouldLogAComment() {
repository.count();
assertAtLeastOneComment();
}
@Test // GH-775
void customCountShouldLogAComment() {
repository.countByName("name");
assertAtLeastOneComment();
}
@Test // GH-775
void deleteAllByIdInBatchShouldLogAComment() {
repository.deleteAllByIdInBatch(List.of(0, 1, 2));
assertAtLeastOneComment();
}
@Test // GH-775
void deleteAllInBatchShouldLogAComment() {
repository.deleteAllInBatch();
assertAtLeastOneComment();
}
private final static Predicate<String> hasComment = s -> s.startsWith("/* foobar */");
private void assertAtLeastOneComment() {
assertThat(testAppender.list).extracting(ILoggingEvent::getFormattedMessage)
.haveAtLeastOne(new Condition<String>(hasComment, "SQL contains a comment"));
}
private void assertNoComments() {
assertThat(testAppender.list).extracting(ILoggingEvent::getFormattedMessage).noneMatch(hasComment);
}
@Configuration
@EnableJpaRepositories(basePackages = "org.springframework.data.jpa.repository.sample")
static class Config {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().generateUniqueName(true).build();
}
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
@Bean
public AbstractJpaVendorAdapter vendorAdaptor() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
vendorAdapter.setDatabase(Database.HSQL);
return vendorAdapter;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource());
factory.setPersistenceUnitName("spring-data-jpa");
factory.setJpaVendorAdapter(vendorAdaptor());
factory.setJpaProperties(jpaProperties());
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public JpaDialect jpaDialect() {
return new HibernateJpaDialect();
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2013-2022 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 org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Verify that {@link Meta}-annotated methods property capture comments.
*
* @author Greg Turnquist
* @since 3.0
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class MetaAnnotatedQueryMethodUnitTests {
@Mock QueryExtractor extractor;
private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
@Test // GH-775
void metaAnnotationCommentsAreCapturedInJpaQueryMethod() throws Exception {
Method method = UserRepository.class.getMethod("metaMethod");
DefaultRepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(UserRepository.class);
JpaQueryMethod jpaQueryMethod = new JpaQueryMethod(method, repositoryMetadata, factory, extractor);
assertThat(jpaQueryMethod.getQueryMetaAttributes().getComment()).isEqualTo("Comments embedded in SQL");
}
interface UserRepository extends CrudRepository<User, String> {
@Meta(comment = "Comments embedded in SQL")
void metaMethod();
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2008-2022 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.sample;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.FluentQuery;
import com.querydsl.core.types.Predicate;
/**
* Typed repository for {@link Role} but with {@link Meta} annotations applied.
*
* @author Greg Turnquist
* @since 3.0
*/
public interface RoleRepositoryWithMeta extends JpaRepository<Role, Integer>, QuerydslPredicateExecutor<Role> {
// Finders
@Override
@Meta(comment = "foobar")
List<Role> findAll();
@Override
@Meta(comment = "foobar")
Optional<Role> findById(Integer id);
@Override
@Meta(comment = "foobar")
Optional<Role> findOne(Predicate predicate);
@Meta(comment = "foobar")
List<Role> findByName(String name);
@Override
@Meta(comment = "foobar")
<S extends Role> Optional<S> findOne(Example<S> example);
@Override
@Meta(comment = "foobar")
<S extends Role> List<S> findAll(Example<S> example);
@Override
@Meta(comment = "foobar")
<S extends Role> List<S> findAll(Example<S> example, Sort sort);
@Override
@Meta(comment = "foobar")
<S extends Role, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
// counters
@Override
@Meta(comment = "foobar")
long count();
@Meta(comment = "foobar")
long countByName(String name);
// exists
@Override
@Meta(comment = "foobar")
boolean existsById(Integer integer);
@Override
@Meta(comment = "foobar")
<S extends Role> boolean exists(Example<S> example);
// delete
@Override
@Meta(comment = "foobar")
void deleteAllInBatch();
@Override
@Meta(comment = "foobar")
void deleteAllByIdInBatch(Iterable<Integer> integers);
}

View File

@@ -726,6 +726,139 @@ public interface UserRepository extends Repository<User, Long> {
====
The preceding declaration would apply the configured `@QueryHint` for that actually query but omit applying it to the count query triggered to calculate the total number of pages.
[[jpa.query-hints.comments]]
==== Adding Comments to Queries
Sometimes, you need to debug a query based upon database performance.
The query your database administrator shows you may look VERY different than what you wrote using `@Query`, or it may look
nothing like what you presume Spring Data JPA has generated regarding a custom finder or if you used query by example.
To make this process easier, you can insert custom comments into almost any JPA operation, whether its a query or other operation
by applying the `@Meta` annotation.
.Apply `@Meta` annotation to repository operations
====
[source, java]
----
public interface RoleRepository extends JpaRepository<Role, Integer> {
@Meta(comment = "find roles by name")
List<Role> findByName(String name);
@Override
@Meta(comment = "find roles using QBE")
<S extends Role> List<S> findAll(Example<S> example);
@Meta(comment = "count roles for a given name")
long countByName(String name);
@Override
@Meta(comment = "exists based on QBE")
<S extends Role> boolean exists(Example<S> example);
}
----
====
This sample repository has a mixture of custom finders as well as overriding the inherited operations from `JpaRepository`.
Either way, the `@Meta` annotation lets you add a `comment` that will be inserted into queries before they are sent to the database.
It's also important to note that this feature isn't confined solely to queries. It extends to the `count` and `exists` operations.
And while not shown, it also extends to certain `delete` operations.
IMPORTANT: While we have attempted to apply this feature everywhere possible, some operations of the underlying `EntityManager` don't support comments. For example, `entityManager.createQuery()` is clearly documented as supporting comments, but `entityManager.find()` operations do not.
Neither JPQL logging nor SQL logging is a standard in JPA, so each provider requires custom configuration, as shown the sections below.
===== Activating Hibernate comments
To activate query comments in Hibernate, you must set `hibernate.use_sql_comments` to `true`.
If you are using Java-based configuration settings, this can be done like this:
.Java-based JPA configuration
====
[source, java]
----
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
----
====
If you have a `persistence.xml` file, you can apply it there:
.`persistence.xml`-based configuration
====
[source, xml]
----
<persistence-unit name="my-persistence-unit">
...registered classes...
<properties>
<property name="hibernate.use_sql_comments" value="true" />
</properties>
</persistence-unit>
----
====
Finally, if you are using Spring Boot, then you can set it up inside your `application.properties` file:
.Spring Boot property-based configuration
====
----
spring.jpa.properties.hibernate.use_sql_comments=true
----
====
===== Activating EclipseLink comments
To activate query comments in EclipseLink, you must set `eclipselink.logging.level.sql` to `FINE`.
If you are using Java-based configuration settings, this can be done like this:
.Java-based JPA configuration
====
[source, java]
----
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("eclipselink.logging.level.sql", "FINE");
return properties;
}
----
====
If you have a `persistence.xml` file, you can apply it there:
.`persistence.xml`-based configuration
====
[source, xml]
----
<persistence-unit name="my-persistence-unit">
...registered classes...
<properties>
<property name="eclipselink.logging.level.sql" value="FINE" />
</properties>
</persistence-unit>
----
====
Finally, if you are using Spring Boot, then you can set it up inside your `application.properties` file:
.Spring Boot property-based configuration
====
----
spring.jpa.properties.eclipselink.logging.level.sql=FINE
----
====
[[jpa.entity-graph]]
=== Configuring Fetch- and LoadGraphs