DATAJPA-12 - Added Sort implementations for JPA meta-model API and Querydsl.

Introduced JpaSort for sorting by JPA meta-model attribute paths. Introduced JpaMetaModelPathBuilder that can be used to ease the construction of Jpa meta-model attribute paths by the provided static factory method. Added new testing scenario (MailMessage and MailSender) to avoid to mess up the existing sample classes. Enabled static JPA meta-model generation in pom.xml.

Enhanced Querydsl to generate appropriate left joins when sorting by nested (singular) association properties. Converted XML configuration for QueryDslRepositorySupportIntegrationTests into JavaConfig.

Original pull request: #54.
This commit is contained in:
Thomas Darimont
2013-12-04 17:32:20 +01:00
committed by Oliver Gierke
parent e51add8c76
commit 692b2382bf
18 changed files with 1057 additions and 52 deletions

65
pom.xml
View File

@@ -27,7 +27,7 @@
<hsqldb1>1.8.0.10</hsqldb1>
<jpa>2.0.0</jpa>
<openjpa>2.2.1</openjpa>
<springdata.commons>1.7.0.M1</springdata.commons>
<springdata.commons>1.7.0.BUILD-SNAPSHOT</springdata.commons>
</properties>
@@ -413,6 +413,69 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>wagon-maven-plugin</artifactId>
</plugin>
<plugin>
<!-- configuration for JPA Metamodel generation
as described here: http://stackoverflow.com/questions/18853585/maven-build-with-annotationprocessor-that-parses-files-in-src-main-java-and-gene
-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<!-- normal compile and generation of other classes to standard location (implicit, you shouldn't need that) -->
<id>default-compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessors>
<annotationProcessor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</annotationProcessor>
</annotationProcessors>
<generatedSourcesDirectory>${project.build.directory}/generated-sources/test</generatedSourcesDirectory>
<!-- generated class depends on test-scope libs, so don't compile now: proc:only DISABLES compilation of generated classes-->
<proc>only</proc>
</configuration>
</execution>
<!-- implicit test-compile:testCompile -->
</executions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.2.0.Final</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<!-- adds source-dir during generate-test-sources:add-test-source
so that the path to our generated class is now known to the
compiler during test-compile:testCompile -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/test</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2013 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.domain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;
/**
* Sort option for queries that wraps JPA MetaModel {@link Expression}s for sorting.
*
* @author Thomas Darimont
*/
public class JpaSort extends Sort {
private static final long serialVersionUID = 1L;
/**
* Creates a new {@link JpaSort} instance with the given {@link Path}s.
*
* @param jpaPaths must not be {@literal null} or empty.
*/
public JpaSort(Path<?>... jpaPaths) {
this(Arrays.asList(jpaPaths));
}
/**
* Creates a new {@link JpaSort} instance with the given {@link Path}s.
*
* @param direction
* @param jpaPaths must not be {@literal null} or empty.
*/
public JpaSort(Direction direction, Path<?>... jpaPaths) {
this(direction, Arrays.asList(jpaPaths));
}
/**
* Creates a new {@link JpaSort} instance with the given {@link Path}s.
*
* @param jpaPaths must not be {@literal null} or empty.
*/
public JpaSort(List<Path<?>> jpaPaths) {
this(DEFAULT_DIRECTION, jpaPaths);
}
/**
* Creates a new {@link JpaSort} instance with the given {@link Path}s.
*
* @param direction
* @param jpaPaths must not be {@literal null} or empty.
*/
public JpaSort(Direction direction, List<Path<?>> jpaPaths) {
super(direction, toPropertyPaths(jpaPaths));
}
/**
* @param jpaPaths must not be {@literal null} or empty.
* @return
*/
private static List<String> toPropertyPaths(List<Path<?>> jpaPaths) {
Assert.notEmpty(jpaPaths, "Jpa orders must not be null or empty!");
List<String> propertyPaths = new ArrayList<String>();
for (Path<?> path : jpaPaths) {
propertyPaths.add(toPropertyPath(path));
}
return propertyPaths;
}
/**
* @param path
* @return
*/
@SuppressWarnings("rawtypes")
private static String toPropertyPath(Path path) {
StringBuilder attributePath = new StringBuilder();
Path current = path;
while (!(current instanceof Root)) {
String attributePathSegment = ((SingularAttribute) current.getModel()).getName();
if (attributePath.length() > 0) {
attributePath.insert(0, ".");
}
attributePath.insert(0, attributePathSegment);
current = current.getParentPath();
}
return attributePath.toString();
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
@@ -99,7 +100,10 @@ public class QueryDslJpaRepository<T, ID extends Serializable> extends SimpleJpa
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, com.mysema.query.types.OrderSpecifier<?>[])
*/
public List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders) {
return createQuery(predicate).orderBy(orders).list(path);
JPQLQuery query = createQuery(predicate);
query = querydsl.applySorting(new QSort(orders), query);
return query.list(path);
}
/*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2013 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.
@@ -15,7 +15,9 @@
*/
package org.springframework.data.jpa.repository.support;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
@@ -25,6 +27,7 @@ import javax.persistence.metamodel.EntityType;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.querydsl.QSort;
import org.springframework.util.Assert;
import com.mysema.query.jpa.EclipseLinkTemplates;
@@ -35,6 +38,7 @@ import com.mysema.query.jpa.impl.JPAQuery;
import com.mysema.query.types.EntityPath;
import com.mysema.query.types.Expression;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Path;
import com.mysema.query.types.path.EntityPathBase;
import com.mysema.query.types.path.PathBuilder;
@@ -42,6 +46,7 @@ import com.mysema.query.types.path.PathBuilder;
* Helper instance to ease access to Querydsl JPA query API.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
public class Querydsl {
@@ -126,8 +131,87 @@ public class Querydsl {
return query;
}
if (sort instanceof QSort) {
return addOrderByFrom((QSort) sort, query);
}
return addOrderByFrom(sort, query);
}
/**
* Applies the given {@link OrderSpecifier}s to the given {@link JPQLQuery}. Potentially transforms the given
* {@code OrderSpecifier}s to be able to injection potentially necessary left-joins.
*
* @param qsort must not be {@literal null}.
* @param query must not be {@literal null}.
*/
private JPQLQuery addOrderByFrom(QSort qsort, JPQLQuery query) {
return query.orderBy(adjustOrderSpecifierIfNecessary(qsort.getOrderSpecifiers(), query));
}
/**
* Rewrites the given {@link OrderSpecifier} if necessary, e.g. generates proper aliases and left-joins to be created
* if we detect ordering by an nested attribute.
*
* @param originalOrderSpecifiers must not be {@literal null}.
* @param query must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private OrderSpecifier<?>[] adjustOrderSpecifierIfNecessary(List<OrderSpecifier<?>> originalOrderSpecifiers,
JPQLQuery query) {
Assert.notNull(originalOrderSpecifiers, "Original order specifiers must not be null!");
Assert.notNull(query, "Query must not be null!");
boolean orderModificationNecessary = false;
List<OrderSpecifier<?>> modifiedOrderSpecifiers = new ArrayList<OrderSpecifier<?>>();
for (OrderSpecifier<?> order : originalOrderSpecifiers) {
Path targetPath = ((Path) order.getTarget()).getMetadata().getParent();
boolean targetPathRootIsEntityRoot = targetPath.getRoot().equals(builder.getRoot());
boolean targetPathEqualsRootEnityPath = targetPath.toString().equals(builder.toString());
if (!targetPathRootIsEntityRoot) {
query.leftJoin((EntityPath) builder.get((String) targetPath.getMetadata().getElement()), targetPath);
} else if (targetPathRootIsEntityRoot && !targetPathEqualsRootEnityPath) {
PathBuilder joinPathBuilder = new PathBuilder(targetPath.getType(), targetPath.getMetadata().getElement()
.toString());
query.leftJoin((EntityPath) targetPath, joinPathBuilder);
OrderSpecifier<?> modifiedOrder = new OrderSpecifier(order.getOrder(), joinPathBuilder.get(((Path) order
.getTarget()).getMetadata().getElement().toString()), order.getNullHandling());
modifiedOrderSpecifiers.add(modifiedOrder);
orderModificationNecessary = true;
continue;
}
modifiedOrderSpecifiers.add(order);
}
return orderModificationNecessary ? modifiedOrderSpecifiers.toArray(new OrderSpecifier<?>[modifiedOrderSpecifiers
.size()]) : originalOrderSpecifiers.toArray(new OrderSpecifier<?>[originalOrderSpecifiers.size()]);
}
/**
* Converts the {@link Order} items of the given {@link Sort} into {@link OrderSpecifier} and attaches those to the
* given {@link JPQLQuery}.
*
* @param sort must not be {@literal null}.
* @param query must not be {@literal null}.
* @return
*/
private JPQLQuery addOrderByFrom(Sort sort, JPQLQuery query) {
Assert.notNull(sort, "Sort must not be null!");
Assert.notNull(query, "Query must not be null!");
for (Order order : sort) {
query.orderBy(toOrder(order, query));
query.orderBy(toOrderSpecifier(order, query));
}
return query;
@@ -140,7 +224,7 @@ public class Querydsl {
* @return
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private OrderSpecifier<?> toOrder(Order order, JPQLQuery query) {
private OrderSpecifier<?> toOrderSpecifier(Order order, JPQLQuery query) {
Expression<?> property = createExpressionAndPotentionallyAddLeftJoinForReferencedAssociation(order, query);
@@ -190,20 +274,29 @@ public class Querydsl {
}
/**
* @param attribute
* @param order
* @param query
* Adds a left-join to the given {@link JPQLQuery} with a proper alias for the property referenced on the given
* {@link Order} relative to the given parent {@link Attribute}.
*
* @param parentAttribute must not be {@literal null}.
* @param order must not be {@literal null}.
* @param query must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private Expression<?> createLeftJoinForAttributeInOrderBy(Attribute<?, ?> attribute, Order order, JPQLQuery query) {
private Expression<?> createLeftJoinForAttributeInOrderBy(Attribute<?, ?> parentAttribute, Order order,
JPQLQuery query) {
EntityPathBase<?> associationPathRoot = new EntityPathBase<Object>(attribute.getJavaType(), attribute.getName());
query.leftJoin((EntityPath) builder.get(attribute.getName()), associationPathRoot);
PathBuilder<Object> attributePathBuilder = new PathBuilder<Object>(attribute.getJavaType(),
Assert.notNull(parentAttribute, "Attribute must not be null!");
Assert.notNull(order, "Order must not be null!");
Assert.notNull(query, "Query must not be null!");
EntityPathBase<?> associationPathRoot = new EntityPathBase<Object>(parentAttribute.getJavaType(),
parentAttribute.getName());
query.leftJoin((EntityPath) builder.get(parentAttribute.getName()), associationPathRoot);
PathBuilder<Object> attributePathBuilder = new PathBuilder<Object>(parentAttribute.getJavaType(),
associationPathRoot.getMetadata());
String nestedAttributePath = order.getProperty().substring(attribute.getName().length() + 1); // exclude "."
String nestedAttributePath = order.getProperty().substring(parentAttribute.getName().length() + 1); // exclude "."
return order.isIgnoreCase() ? attributePathBuilder.getString(nestedAttributePath).lower() : attributePathBuilder
.get(nestedAttributePath);
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2011-2013 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.support;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.util.Assert;
/**
* Builder for building JPA meta-model access {@link Path}s that can be used as Path expressions e.g. in {@link JpaSort}
*
* @author Thomas Darimont
*/
public class JpaMetaModelPathBuilder<T, A> {
private final Class<?> rootType;
private final List<SingularAttribute<?, ?>> currentPathSegments;
/**
* Static factory method to create a {@link JpaMetaModelPathBuilder} from the given root {@link SingularAttribute}.
*
* @param attribute the root attribute to use must not be {@literal null}.
* @return
*/
public static <T, A> JpaMetaModelPathBuilder<T, A> path(SingularAttribute<T, A> attribute) {
return new JpaMetaModelPathBuilder<T, A>(attribute.getDeclaringType().getJavaType(), attribute);
}
/**
* Creates a new {@link JpaMetaModelPathBuilder}.
*
* @param rootType the root type of the expression must not be {@literal null}.
* @param attribute must not be {@literal null}.
*/
private JpaMetaModelPathBuilder(Class<?> rootType, SingularAttribute<T, A> attribute) {
this(rootType, attribute, new ArrayList<SingularAttribute<?, ?>>());
}
/**
* Creates a new {@link JpaMetaModelPathBuilder}.
*
* @param rootType the root type of the expression must not be {@literal null}.
* @param attribute must not be {@literal null}
* @param pathSegments must not be {@literal null}
*/
private JpaMetaModelPathBuilder(Class<?> rootType, SingularAttribute<T, A> attribute,
List<SingularAttribute<?, ?>> pathSegments) {
Assert.notNull(rootType, "Root type must not be null!");
Assert.notNull(attribute, "Attribute must not be null!");
Assert.notNull(pathSegments, "Path segments must not be null!");
this.rootType = rootType;
this.currentPathSegments = pathSegments;
this.currentPathSegments.add(attribute);
}
/**
* Returns a new {@link JpaMetaModelPathBuilder} instance with the given nested {@link SingularAttribute} attached.
*
* @param nestedAttribute must not be {@literal null}.
* @return
*/
public <NA> JpaMetaModelPathBuilder<A, NA> get(SingularAttribute<A, NA> nestedAttribute) {
return new JpaMetaModelPathBuilder<A, NA>(rootType, nestedAttribute, new ArrayList<SingularAttribute<?, ?>>(
currentPathSegments));
}
/**
* Constructs a {@link Path} from the collected {@link SingularAttribute}s.
*
* @param em must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public Path<A> build(EntityManager em) {
Assert.notNull(em, "EntityManager must nut be null");
Root<?> root = em.getCriteriaBuilder().createQuery().from(rootType);
@SuppressWarnings("rawtypes")
Path path = root;
for (SingularAttribute<?, ?> attribute : currentPathSegments) {
path = path.get(attribute);
}
return (Path<A>) path;
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2013 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.domain;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.jpa.support.JpaMetaModelPathBuilder.*;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.jpa.domain.sample.MailMessage_;
import org.springframework.data.jpa.domain.sample.MailSender_;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.domain.sample.User_;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Unit test for {@link JpaSort}.
*
* @author Thomas Darimont
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class JpaSortTests {
@Configuration
@ImportResource("classpath:infrastructure.xml")
static class Config {}
@PersistenceContext EntityManager em;
private static final MailMessage_ jmail = null;
private static final MailSender_ jsender = null;
/**
* @see DATAJPA-12
*/
@Test(expected = IllegalArgumentException.class)
public void shouldThrowIfNoOrderSpecifiersAreGiven() {
new JpaSort();
}
/**
* @see DATAJPA-12
*/
@Test(expected = IllegalArgumentException.class)
public void shouldThrowIfNullIsGiven() {
new JpaSort((List<Path<?>>) null);
}
/**
* @see DATAJPA-12
*/
@Test
public void sortBySinglePropertyWithDefaultSortDirection() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
JpaSort sort = new JpaSort(c.get("firstname"));
assertThat(sort, hasItems(new Sort.Order("firstname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void sortByMultiplePropertiesWithDefaultSortDirection() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
JpaSort sort = new JpaSort(c.get("firstname"), c.get("lastname"));
assertThat(sort, hasItems(new Sort.Order("firstname"), new Sort.Order("lastname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void sortByMultiplePropertiesWithDescSortDirection() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
JpaSort sort = new JpaSort(Direction.DESC, c.get("firstname"), c.get("lastname"));
assertThat(sort, hasItems(new Sort.Order(Direction.DESC, "firstname"), new Sort.Order(Direction.DESC, "lastname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void combiningSortByMultipleProperties() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
Sort sort = new JpaSort(c.get("firstname")).and(new JpaSort(c.get("lastname")));
assertThat(sort, hasItems(new Sort.Order("firstname"), new Sort.Order("lastname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void combiningSortByMultiplePropertiesWithDifferentSort() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
Sort sort = new JpaSort(c.get("firstname")).and(new JpaSort(Direction.DESC, c.get("lastname")));
assertThat(sort, hasItems(new Sort.Order("firstname"), new Sort.Order(Direction.DESC, "lastname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void combiningSortByNestedEmbeddedProperty() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> c = q.from(User.class);
Sort sort = new JpaSort(c.get("address").get("streetName"));
assertThat(sort, hasItems(new Sort.Order("address.streetName")));
}
/**
* @see DATAJPA-12
*/
@Test
public void buildJpaSortFromJpaMetaModelSingleAttribute() {
Sort sort = new JpaSort(Direction.ASC, path(User_.firstname).build(em));
assertThat(sort, hasItems(new Sort.Order("firstname")));
}
/**
* @see DATAJPA-12
*/
@Test
public void buildJpaSortFromJpaMetaModelNestedAttribute() {
Sort sort = new JpaSort(Direction.ASC, path(MailMessage_.mailSender).get(MailSender_.name).build(em));
assertThat(sort, hasItems(new Sort.Order("mailSender.name")));
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2013 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.domain.sample;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
/**
* @author Thomas Darimont
*/
@Entity
public class MailMessage {
@Id @GeneratedValue private Long id;
@OneToOne(cascade = CascadeType.ALL) private MailSender mailSender;
private String content;
public MailSender getMailSender() {
return mailSender;
}
public void setMailSender(MailSender sender) {
this.mailSender = sender;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2013 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.domain.sample;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Thomas Darimont
*/
@Entity
public class MailSender {
@Id @GeneratedValue private Long id;
private String name;
public MailSender() {}
public MailSender(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MailSender other = (MailSender) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}

View File

@@ -64,10 +64,7 @@ public class User {
* Creates a new empty instance of {@code User}.
*/
public User() {
this.roles = new HashSet<Role>();
this.colleagues = new HashSet<User>();
this.createdAt = new Date();
this(null, null, null);
}
/**
@@ -77,13 +74,15 @@ public class User {
* @param lastname
* @param emailAddress
*/
public User(final String firstname, final String lastname, final String emailAddress) {
public User(String firstname, String lastname, String emailAddress) {
this();
this.firstname = firstname;
this.lastname = lastname;
this.emailAddress = emailAddress;
this.active = true;
this.roles = new HashSet<Role>();
this.colleagues = new HashSet<User>();
this.createdAt = new Date();
}
/**

View File

@@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
*/
@Configuration
@EnableTransactionManagement
class InfrastructureConfig {
public class InfrastructureConfig {
@Bean
public DataSource dataSource() {

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2013 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.sample;
import java.util.List;
import org.springframework.data.jpa.domain.sample.MailMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Predicate;
/**
* @author Thomas Darimont
*/
public interface MailMessageRepository extends JpaRepository<MailMessage, Long>, QueryDslPredicateExecutor<MailMessage> {
List<MailMessage> findAll(Predicate predicate, OrderSpecifier<?>... orders);
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2013 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.support;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.jpa.support.JpaMetaModelPathBuilder.*;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.data.jpa.domain.sample.MailMessage;
import org.springframework.data.jpa.domain.sample.MailMessage_;
import org.springframework.data.jpa.domain.sample.MailSender;
import org.springframework.data.jpa.domain.sample.MailSender_;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.config.InfrastructureConfig;
import org.springframework.data.jpa.repository.sample.MailMessageRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Thomas Darimont
*/
@Transactional
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class JpaMetaModelRepositoryUnitTests {
@Configuration
@Import(InfrastructureConfig.class)
@EnableJpaRepositories(basePackageClasses = MailMessageRepository.class)
static class Config {}
private static final MailMessage_ jmail = null;
private static final MailSender_ jsender = null;
@PersistenceContext EntityManager em;
@Autowired MailMessageRepository mailMessageRepository;
/**
* @see DATAJPA-12
*/
@Test
public void shouldSortMailWithPageRequestAndJpaSortCriteriaNullsFirst() {
MailMessage message1 = new MailMessage();
message1.setContent("abc");
MailSender sender1 = new MailSender("foo");
message1.setMailSender(sender1);
MailMessage message2 = new MailMessage();
message2.setContent("abc");
mailMessageRepository.save(message1);
mailMessageRepository.save(message2);
Page<MailMessage> results = mailMessageRepository.findAll(new PageRequest(0, 20, //
new JpaSort(Direction.ASC, path(jmail.mailSender).get(jsender.name).build(em))));
List<MailMessage> messages = results.getContent();
assertThat(messages, hasSize(2));
assertThat(messages.get(0).getMailSender(), is(nullValue()));
assertThat(messages.get(1).getMailSender(), is(sender1));
}
}

View File

@@ -34,6 +34,8 @@ import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.sample.Address;
import org.springframework.data.jpa.domain.sample.QUser;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.querydsl.QPageRequest;
import org.springframework.data.querydsl.QSort;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@@ -215,4 +217,60 @@ public class QueryDslJpaRepositoryTests {
assertThat(page.getContent(), hasItems(dave, carter, oliver));
assertThat(page.getContent().get(2), is(oliver));
}
/**
* @see DATAJPA-12
*/
@Test
public void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequestAndQSort() {
QUser user = QUser.user;
Page<User> page = repository.findAll(user.firstname.isNotNull(),
new QPageRequest(0, 10, new QSort(user.firstname.asc())));
assertThat(page.getContent(), hasSize(3));
assertThat(page.getContent(), hasItems(carter, dave, oliver));
assertThat(page.getContent().get(0), is(carter));
assertThat(page.getContent().get(1), is(dave));
assertThat(page.getContent().get(2), is(oliver));
}
/**
* @see DATAJPA-12
*/
@Test
public void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequest() {
QUser user = QUser.user;
Page<User> page = repository.findAll(user.firstname.isNotNull(), new QPageRequest(0, 10, user.firstname.asc()));
assertThat(page.getContent(), hasSize(3));
assertThat(page.getContent(), hasItems(carter, dave, oliver));
assertThat(page.getContent().get(0), is(carter));
assertThat(page.getContent().get(1), is(dave));
assertThat(page.getContent().get(2), is(oliver));
}
/**
* @see DATAJPA-12
*/
@Test
public void findBySpecificationWithSortByQueryDslOrderSpecifierForAssociationShouldGenerateLeftJoinWithQPageRequest() {
oliver.setManager(dave);
dave.setManager(carter);
QUser user = QUser.user;
Page<User> page = repository.findAll(user.firstname.isNotNull(),
new QPageRequest(0, 10, user.manager.firstname.asc()));
assertThat(page.getContent(), hasSize(3));
assertThat(page.getContent(), hasItems(carter, dave, oliver));
assertThat(page.getContent().get(0), is(carter));
assertThat(page.getContent().get(1), is(dave));
assertThat(page.getContent().get(2), is(oliver));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2011-2013 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.
@@ -15,37 +15,96 @@
*/
package org.springframework.data.jpa.repository.support;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.sample.MailMessage;
import org.springframework.data.jpa.domain.sample.MailSender;
import org.springframework.data.jpa.domain.sample.QMailMessage;
import org.springframework.data.jpa.domain.sample.QMailSender;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.config.InfrastructureConfig;
import org.springframework.data.jpa.repository.sample.MailMessageRepository;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupportTests.UserRepository;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupportTests.UserRepositoryImpl;
import org.springframework.data.querydsl.QPageRequest;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
/**
* Integration test for the setup of beans extending {@link QueryDslRepositorySupport}.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Transactional
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:querydsl.xml")
public class QueryDslRepositorySupportIntegrationTests {
@Autowired
UserRepository repository;
@Configuration
@EnableJpaRepositories(basePackageClasses = MailMessageRepository.class, includeFilters = @Filter(
type = FilterType.ASSIGNABLE_TYPE, value = { MailMessageRepository.class }))
@EnableTransactionManagement
static class Config extends InfrastructureConfig {
@Bean
public UserRepositoryImpl userRepositoryImpl() {
@Autowired
ReconfiguringUserRepositoryImpl reconfiguredRepo;
return new UserRepositoryImpl() {
@Override
@PersistenceContext(unitName = "querydsl")
public void setEntityManager(EntityManager entityManager) {
super.setEntityManager(entityManager);
}
};
}
@PersistenceContext(unitName = "querydsl")
EntityManager em;
@Bean
public ReconfiguringUserRepositoryImpl reconfiguringUserRepositoryImpl() {
return new ReconfiguringUserRepositoryImpl();
}
@Bean
public EntityManagerContainer entityManagerContainer() {
return new EntityManagerContainer();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean emf = super.entityManagerFactory();
emf.setPersistenceUnitName("querydsl");
return emf;
}
}
@Autowired UserRepository repository;
@Autowired ReconfiguringUserRepositoryImpl reconfiguredRepo;
@Autowired MailMessageRepository mailMessageRepository;
@PersistenceContext(unitName = "querydsl") EntityManager em;
static final QMailMessage qmail = QMailMessage.mailMessage;
static final QMailSender qsender = QMailSender.mailSender;
@Test
public void createsRepoCorrectly() {
@@ -62,6 +121,56 @@ public class QueryDslRepositorySupportIntegrationTests {
assertThat(reconfiguredRepo.getEntityManager().getEntityManagerFactory(), is(em.getEntityManagerFactory()));
}
/**
* @see DATAJPA-12
*/
@Test
public void shouldSortMailWithQueryDslRepositoryAndQPageRequestDslSortCriteriaNullsFirst() {
MailMessage message1 = new MailMessage();
message1.setContent("abc");
MailSender sender1 = new MailSender("foo");
message1.setMailSender(sender1);
MailMessage message2 = new MailMessage();
message2.setContent("abc");
mailMessageRepository.save(message1);
mailMessageRepository.save(message2);
Page<MailMessage> results = mailMessageRepository.findAll(qmail.content.eq("abc"), new QPageRequest(0, 20,
qsender.name.asc()));
List<MailMessage> messages = results.getContent();
assertThat(messages, hasSize(2));
assertThat(messages.get(0).getMailSender(), is(nullValue()));
assertThat(messages.get(1).getMailSender(), is(sender1));
}
/**
* @see DATAJPA-12
*/
@Test
public void shouldSortMailWithQueryDslRepositoryAndDslSortCriteriaNullsFirst() {
MailMessage message1 = new MailMessage();
message1.setContent("abc");
MailSender sender1 = new MailSender("foo");
message1.setMailSender(sender1);
MailMessage message2 = new MailMessage();
message2.setContent("abc");
mailMessageRepository.save(message1);
mailMessageRepository.save(message2);
List<MailMessage> messages = mailMessageRepository.findAll(qmail.content.eq("abc"), qmail.mailSender.name.asc());
assertThat(messages, hasSize(2));
assertThat(messages.get(0).getMailSender(), is(nullValue()));
assertThat(messages.get(1).getMailSender(), is(sender1));
}
static class ReconfiguringUserRepositoryImpl extends QueryDslRepositorySupport {
public ReconfiguringUserRepositoryImpl() {
@@ -77,7 +186,6 @@ public class QueryDslRepositorySupportIntegrationTests {
static class EntityManagerContainer {
@PersistenceContext(unitName = "querydsl")
EntityManager em;
@PersistenceContext(unitName = "querydsl") EntityManager em;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2011-2013 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.
@@ -36,14 +36,14 @@ import org.springframework.transaction.annotation.Transactional;
* Integration test for {@link QueryDslRepositorySupport}.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:infrastructure.xml" })
@Transactional
public class QueryDslRepositorySupportTests {
@PersistenceContext
EntityManager em;
@PersistenceContext EntityManager em;
UserRepository repository;
User dave, carter;

View File

@@ -28,16 +28,22 @@
<class>org.springframework.data.jpa.domain.sample.Customer</class>
<class>org.springframework.data.jpa.domain.sample.Order</class>
<class>org.springframework.data.jpa.domain.sample.Address</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
<persistence-unit name="querydsl">
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
<persistence-unit name="cdi">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.repository.cdi.Person</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.connection.username" value="sa" />
@@ -54,6 +60,8 @@
<persistence-unit name="metadata">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<class>org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
@@ -63,12 +71,16 @@
<persistence-unit name="metadata_el">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<class>org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
<persistence-unit name="metadata_oj">
<provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<class>org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>

View File

@@ -6,6 +6,8 @@
<class>org.springframework.data.jpa.domain.sample.User</class>
<class>org.springframework.data.jpa.domain.sample.SpecialUser</class>
<class>org.springframework.data.jpa.domain.sample.Role</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
<persistence-unit name="second">
@@ -14,6 +16,8 @@
<class>org.springframework.data.jpa.domain.sample.Role</class>
<class>org.springframework.data.jpa.domain.sample.AuditableUser</class>
<class>org.springframework.data.jpa.domain.sample.AuditableRole</class>
<class>org.springframework.data.jpa.domain.sample.MailMessage</class>
<class>org.springframework.data.jpa.domain.sample.MailSender</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
</persistence>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="infrastructure.xml" />
<bean class="org.springframework.data.jpa.repository.support.QueryDslRepositorySupportTests.UserRepositoryImpl" />
<bean class="org.springframework.data.jpa.repository.support.QueryDslRepositorySupportIntegrationTests.ReconfiguringUserRepositoryImpl" />
<bean class="org.springframework.data.jpa.repository.support.QueryDslRepositorySupportIntegrationTests.EntityManagerContainer" />
<bean id="alternate" parent="entityManagerFactory">
<property name="persistenceUnitName" value="querydsl" />
</bean>
</beans>