DATAJPA-466 - Add support for lazy loading configuration via JPA 2.1 fetch-/loadgraph.
We now support load-graph / fetch-graph QueryHints on repository query methods, which are applied when a JPA 2.1 capable JPA implementation is used. We explicitly reject the usage of those hints in case the user is running a JPA 2.0 provider.
FetchGraphs / LoadGraphs can now be defined on the Entity via the @NamedEntityGraphs annotation.
@Entity
@QueryEntity
@NamedEntityGraphs(@NamedEntityGraph(name = "GroupInfo.members", attributeNodes = @NamedAttributeNode("members")))
public class GroupInfo {
@ManyToMany List<GroupMember> members = new ArrayList<GroupMember>(); //default fetch mode is "lazy".
}
The entity graph "GroupInfo.members" overwrites the fetch-mode of the members collection to be "eager".
The entity graph to be used can now configured on a repository query method.
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph("GroupInfo.members")
GroupInfo getByGroupName(String name);
}
The new method JpaQueryMethod#getEntityGraph analyses an @EntityGraph annotation and constructs a new JpaEntityGraph value object that contains the information form the annotation. The new method AbstractJpaQuery#applyEntityGraphConfiguration tries to apply the given EntityGraph configuration if the used JPA persistence provider supports the JPA 2.1 spec.
Changed the class path order such that EclipseLink is now placed before the eclipse dependency. EclipseLink references the JPA 2.1 API and allows us to provide type-safe support for the new JPA 2.1 features.
Original pull request: #74.
This commit is contained in:
committed by
Oliver Gierke
parent
4ffe6d0d07
commit
dd64fe21ea
@@ -30,6 +30,9 @@ import javax.persistence.Id;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.ManyToMany;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedAttributeNode;
|
||||
import javax.persistence.NamedEntityGraph;
|
||||
import javax.persistence.NamedEntityGraphs;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
@@ -42,6 +45,10 @@ import javax.persistence.TemporalType;
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
@Entity
|
||||
@NamedEntityGraphs({
|
||||
@NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }),
|
||||
@NamedEntityGraph(name = "User.detail", attributeNodes = { @NamedAttributeNode("roles"),
|
||||
@NamedAttributeNode("manager"), @NamedAttributeNode("colleagues") }) })
|
||||
@NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1")
|
||||
public class User {
|
||||
|
||||
|
||||
@@ -28,10 +28,13 @@ import javax.persistence.Query;
|
||||
import javax.persistence.QueryHint;
|
||||
import javax.persistence.TypedQuery;
|
||||
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.QueryHints;
|
||||
import org.springframework.data.jpa.repository.support.PersistenceProvider;
|
||||
@@ -39,6 +42,8 @@ import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Integration test for {@link AbstractJpaQuery}.
|
||||
@@ -49,8 +54,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
@ContextConfiguration("classpath:infrastructure.xml")
|
||||
public class AbstractJpaQueryTests {
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
@PersistenceContext EntityManager em;
|
||||
|
||||
Query query;
|
||||
TypedQuery<Long> countQuery;
|
||||
@@ -122,6 +126,55 @@ public class AbstractJpaQueryTests {
|
||||
verify(result).setLockMode(LockModeType.PESSIMISTIC_WRITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@Test
|
||||
@Transactional
|
||||
public void shouldAddEntityGraphHintForFetch() throws Exception {
|
||||
|
||||
Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager());
|
||||
|
||||
Method findAllMethod = SampleRepository.class.getMethod("findAll");
|
||||
QueryExtractor provider = PersistenceProvider.fromEntityManager(em);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(findAllMethod,
|
||||
new DefaultRepositoryMetadata(SampleRepository.class), provider);
|
||||
|
||||
javax.persistence.EntityGraph<?> entityGraph = em.getEntityGraph("User.overview");
|
||||
|
||||
AbstractJpaQuery jpaQuery = new DummyJpaQuery(queryMethod, em);
|
||||
Query result = jpaQuery.createQuery(new Object[0]);
|
||||
|
||||
verify(result).setHint("javax.persistence.fetchgraph", entityGraph);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@Test
|
||||
@Transactional
|
||||
public void shouldAddEntityGraphHintForLoad() throws Exception {
|
||||
|
||||
Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager());
|
||||
|
||||
Method getByIdMethod = SampleRepository.class.getMethod("getById", Integer.class);
|
||||
QueryExtractor provider = PersistenceProvider.fromEntityManager(em);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(getByIdMethod,
|
||||
new DefaultRepositoryMetadata(SampleRepository.class), provider);
|
||||
|
||||
javax.persistence.EntityGraph<?> entityGraph = em.getEntityGraph("User.detail");
|
||||
|
||||
AbstractJpaQuery jpaQuery = new DummyJpaQuery(queryMethod, em);
|
||||
Query result = jpaQuery.createQuery(new Object[] { 1 });
|
||||
|
||||
verify(result).setHint("javax.persistence.loadgraph", entityGraph);
|
||||
}
|
||||
|
||||
private boolean currentEntityManagerIsAJpa21EntityManager() {
|
||||
return ReflectionUtils.findMethod(((org.springframework.orm.jpa.EntityManagerProxy) em).getTargetEntityManager()
|
||||
.getClass(), "getEntityGraph", String.class) != null;
|
||||
}
|
||||
|
||||
interface SampleRepository extends Repository<User, Integer> {
|
||||
|
||||
@QueryHints({ @QueryHint(name = "foo", value = "bar") })
|
||||
@@ -133,6 +186,18 @@ public class AbstractJpaQueryTests {
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@org.springframework.data.jpa.repository.Query("select u from User u where u.id = ?1")
|
||||
List<User> findOneLocked(Integer primaryKey);
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@EntityGraph(value = "User.detail", type = EntityGraphType.LOAD)
|
||||
User getById(Integer id);
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@EntityGraph("User.overview")
|
||||
List<User> findAll();
|
||||
}
|
||||
|
||||
class DummyJpaQuery extends AbstractJpaQuery {
|
||||
|
||||
@@ -36,6 +36,8 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
@@ -57,14 +59,12 @@ public class JpaQueryMethodUnitTests {
|
||||
static final Class<?> DOMAIN_CLASS = User.class;
|
||||
static final String METHOD_NAME = "findByFirstname";
|
||||
|
||||
@Mock
|
||||
QueryExtractor extractor;
|
||||
@Mock
|
||||
RepositoryMetadata metadata;
|
||||
@Mock QueryExtractor extractor;
|
||||
@Mock RepositoryMetadata metadata;
|
||||
|
||||
Method repositoryMethod, invalidReturnType, pageableAndSort, pageableTwice, sortableTwice, modifyingMethod,
|
||||
nativeQuery, namedQuery, findWithLockMethod, invalidNamedParameter, findsProjections, findsProjection,
|
||||
withMetaAnnotation;
|
||||
withMetaAnnotation, queryMethodWithCustomEntityFetchGraph;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
@@ -91,6 +91,9 @@ public class JpaQueryMethodUnitTests {
|
||||
findsProjection = ValidRepository.class.getMethod("findsProjection");
|
||||
|
||||
withMetaAnnotation = ValidRepository.class.getMethod("withMetaAnnotation");
|
||||
|
||||
queryMethodWithCustomEntityFetchGraph = ValidRepository.class.getMethod("queryMethodWithCustomEntityFetchGraph",
|
||||
Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -313,6 +316,19 @@ public class JpaQueryMethodUnitTests {
|
||||
assertThat(method.getHints().get(0).value(), is("bar"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@Test
|
||||
public void shouldStoreJpa21FetchGraphInformationAsHint() {
|
||||
|
||||
JpaQueryMethod method = new JpaQueryMethod(queryMethodWithCustomEntityFetchGraph, metadata, extractor);
|
||||
|
||||
assertThat(method.getEntityGraph(), is(notNullValue()));
|
||||
assertThat(method.getEntityGraph().getName(), is("User.propertyLoadPath"));
|
||||
assertThat(method.getEntityGraph().getType(), is(EntityGraphType.LOAD));
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to define invalid repository methods for testing.
|
||||
*
|
||||
@@ -367,6 +383,12 @@ public class JpaQueryMethodUnitTests {
|
||||
|
||||
@CustomAnnotation
|
||||
void withMetaAnnotation();
|
||||
|
||||
/**
|
||||
* @see DATAJPA-466
|
||||
*/
|
||||
@EntityGraph(value = "User.propertyLoadPath", type = EntityGraphType.LOAD)
|
||||
User queryMethodWithCustomEntityFetchGraph(Integer id);
|
||||
}
|
||||
|
||||
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.query;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
@@ -30,7 +32,11 @@ import javax.persistence.PersistenceContext;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.spi.PersistenceProvider;
|
||||
import javax.persistence.spi.PersistenceProviderResolver;
|
||||
import javax.persistence.spi.PersistenceProviderResolverHolder;
|
||||
|
||||
import org.hibernate.ejb.HibernatePersistence;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.data.jpa.domain.sample.Order;
|
||||
@@ -118,12 +124,22 @@ public class QueryUtilsIntegrationTests {
|
||||
@Test
|
||||
public void traversesPluralAttributeCorrectly() {
|
||||
|
||||
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("merchant");
|
||||
CriteriaBuilder builder = entityManagerFactory.createEntityManager().getCriteriaBuilder();
|
||||
CriteriaQuery<Merchant> query = builder.createQuery(Merchant.class);
|
||||
Root<Merchant> root = query.from(Merchant.class);
|
||||
PersistenceProviderResolver originalPersistenceProviderResolver = PersistenceProviderResolverHolder
|
||||
.getPersistenceProviderResolver();
|
||||
|
||||
QueryUtils.toExpressionRecursively(root, PropertyPath.from("employeesCredentialsUid", Merchant.class));
|
||||
try {
|
||||
|
||||
PersistenceProviderResolverHolder.setPersistenceProviderResolver(new HibernateOnlyPersistenceProviderResolver());
|
||||
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("merchant");
|
||||
CriteriaBuilder builder = entityManagerFactory.createEntityManager().getCriteriaBuilder();
|
||||
CriteriaQuery<Merchant> query = builder.createQuery(Merchant.class);
|
||||
Root<Merchant> root = query.from(Merchant.class);
|
||||
|
||||
QueryUtils.toExpressionRecursively(root, PropertyPath.from("employeesCredentialsUid", Merchant.class));
|
||||
|
||||
} finally {
|
||||
PersistenceProviderResolverHolder.setPersistenceProviderResolver(originalPersistenceProviderResolver);
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertNoJoinRequestedForOptionalAssociation(Root<Order> root) {
|
||||
@@ -150,4 +166,21 @@ public class QueryUtilsIntegrationTests {
|
||||
@Id String id;
|
||||
String uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PersistenceProviderResolver} that returns only {@link HibernatePersistence} and ignores other
|
||||
* {@link PersistenceProvider}s.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
static class HibernateOnlyPersistenceProviderResolver implements PersistenceProviderResolver {
|
||||
|
||||
@Override
|
||||
public List<PersistenceProvider> getPersistenceProviders() {
|
||||
return Arrays.<PersistenceProvider> asList(new HibernatePersistence());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCachedProviders() {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user