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

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;