DATAJPA-1273 - Disable usage of Tuple for projections when running on EclipseLink.

As EclipseLink doesn't support the execution of Tuple-based queries currently [0], we now fall back to a plan object array based execution for projecting queries. This implies the need for developers to make sure the columns returned are declared in the order the accessor methods are defined in projection interfaces. We now register a dedicated QueryExecutionListener that reports all methods that need that extra care on application startup into INFO level.

[0] https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141
This commit is contained in:
Oliver Gierke
2018-02-27 10:20:27 +01:00
parent 4f0429a704
commit 920cc2cf51
5 changed files with 76 additions and 2 deletions

View File

@@ -33,6 +33,7 @@ import javax.persistence.TypedQuery;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.HibernateUtils;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
@@ -64,6 +65,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
private final JpaQueryMethod method;
private final EntityManager em;
private final JpaMetamodel metamodel;
private final PersistenceProvider provider;
/**
* Creates a new {@link AbstractJpaQuery} from the given {@link JpaQueryMethod}.
@@ -79,6 +81,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
this.method = method;
this.em = em;
this.metamodel = new JpaMetamodel(em.getMetamodel());
this.provider = PersistenceProvider.fromEntityManager(em);
}
/*
@@ -235,6 +238,10 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
*/
protected Class<?> getTypeToRead() {
if (PersistenceProvider.ECLIPSELINK.equals(provider)) {
return null;
}
ResultProcessor resultFactory = getQueryMethod().getResultProcessor();
ReturnedType returnedType = resultFactory.getReturnedType();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2017 the original author or authors.
* Copyright 2008-2018 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.
@@ -17,25 +17,32 @@ package org.springframework.data.jpa.repository.support;
import static org.springframework.data.querydsl.QueryDslUtils.*;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import javax.persistence.EntityManager;
import javax.persistence.Tuple;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.Assert;
/**
@@ -65,6 +72,10 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
if (extractor.equals(PersistenceProvider.ECLIPSELINK)) {
addQueryCreationListener(new EclipseLinkProjectionQueryCreationListener(entityManager));
}
}
/*
@@ -172,4 +183,57 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
}
/**
* Query creation listener that informs EclipseLink users that they have to be extra careful when defining repository
* query methods using projections as we have to rely on the declaration order of the accessors in projection
* interfaces matching the order in columns. Alias-based mapping doesn't work with EclipseLink as it doesn't support
* {@link Tuple} based queries yet.
*
* @author Oliver Gierke
* @since 2.0.5
* @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141
*/
@Slf4j
private static class EclipseLinkProjectionQueryCreationListener implements QueryCreationListener<AbstractJpaQuery> {
private static final String ECLIPSELINK_PROJECTIONS = "Usage of Spring Data projections detected on persistence provider EclipseLink. Make sure the following query methods declare result columns in exactly the order the accessors are declared in the projecting interface or the order of parameters for DTOs:";
private final JpaMetamodel metamodel;
private boolean warningLogged = false;
/**
* Creates a new {@link EclipseLinkProjectionQueryCreationListener} for the given {@link EntityManager}.
*
* @param em must not be {@literal null}.
*/
public EclipseLinkProjectionQueryCreationListener(EntityManager em) {
Assert.notNull(em, "EntityManager must not be null!");
this.metamodel = new JpaMetamodel(em.getMetamodel());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery)
*/
@Override
public void onCreation(AbstractJpaQuery query) {
JpaQueryMethod queryMethod = query.getQueryMethod();
ReturnedType type = queryMethod.getResultProcessor().getReturnedType();
if (type.isProjecting() && !metamodel.isJpaManaged(type.getReturnedType())) {
if (!warningLogged) {
log.info(ECLIPSELINK_PROJECTIONS);
this.warningLogged = true;
}
log.info(" - {}", queryMethod);
}
}
}
}

View File

@@ -54,6 +54,7 @@ public class AbstractStringBasedJpaQueryIntegrationTests {
public void createsNormalQueryForJpaManagedReturnTypes() throws Exception {
EntityManager mock = mock(EntityManager.class);
when(mock.getDelegate()).thenReturn(mock);
when(mock.getEntityManagerFactory()).thenReturn(em.getEntityManagerFactory());
when(mock.getMetamodel()).thenReturn(em.getMetamodel());

View File

@@ -66,6 +66,7 @@ public class NamedQueryUnitTests {
when(em.getMetamodel()).thenReturn(metamodel);
when(em.getEntityManagerFactory()).thenReturn(emf);
when(em.getDelegate()).thenReturn(em);
when(emf.createEntityManager()).thenReturn(em);
}

View File

@@ -90,6 +90,7 @@ public class SimpleJpaQueryUnitTests {
when(em.createQuery(anyString())).thenReturn(query);
when(em.createQuery(anyString(), eq(Long.class))).thenReturn(typedQuery);
when(em.getEntityManagerFactory()).thenReturn(emf);
when(em.getDelegate()).thenReturn(em);
when(emf.createEntityManager()).thenReturn(em);
when(metadata.getDomainType()).thenReturn((Class) User.class);
when(metadata.getReturnedDomainClass(Mockito.any(Method.class))).thenReturn((Class) User.class);