diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Join.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Join.java new file mode 100644 index 0000000000..cc2f600dd4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Join.java @@ -0,0 +1,47 @@ +package org.springframework.data.mongodb.domain; + +import com.mysema.query.types.Path; +import com.mysema.query.types.Predicate; + +public class Join { + + + private QueryDslMongodbPredicate queryDslMongodbPredicate; + private Path ref; + private Path target; + private Predicate[] predicate; + + Join(Path ref, + Path target,QueryDslMongodbPredicate queryDslMongodbPredicate) { + this.queryDslMongodbPredicate = queryDslMongodbPredicate; + this.ref = ref; + this.target = target; + } + + + /** + * Apply {@link Predicate} on query to filter {@link T} documents based on joined DBRef properties. + * + * @param conditions + * @return + */ + public QueryDslMongodbPredicate on(Predicate... conditions) { + predicate = conditions; + return this.queryDslMongodbPredicate; + } + + @SuppressWarnings("unchecked") + Path getRef() { + return (Path) ref; + } + + @SuppressWarnings("unchecked") + Path getTarget() { + return (Path) target; + } + + Predicate[] getPredicate() { + return predicate; + } + +} \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/PredicateBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/PredicateBuilder.java new file mode 100644 index 0000000000..aba45acc73 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/PredicateBuilder.java @@ -0,0 +1,41 @@ +package org.springframework.data.mongodb.domain; + +import com.mysema.query.types.ExpressionUtils; +import com.mysema.query.types.Predicate; + +public class PredicateBuilder { + + private QueryDslMongodbPredicate queryDslMongodbPredicate; + + /** + * Apply {@link Predicate}s (if any) on query to filter on {@link T} document properties + * + * @param predicate + * @return + */ + public QueryDslMongodbPredicate where(Predicate... predicate) { + queryDslMongodbPredicate = new QueryDslMongodbPredicate(this); + queryDslMongodbPredicate.predicate = ExpressionUtils.allOf(predicate); + return queryDslMongodbPredicate; + } + + /** + * Create a conjunction of the given expressions + * + * @param first + * @param second + * @return + */ + public QueryDslMongodbPredicate and(QueryDslMongodbPredicate first,QueryDslMongodbPredicate second) { + queryDslMongodbPredicate = new QueryDslMongodbPredicate(this); + for (Join join : first.joins) { + queryDslMongodbPredicate.join(join.getRef(), join.getTarget()).on(join.getPredicate()); + } + for (Join join : second.joins) { + queryDslMongodbPredicate.join(join.getRef(), join.getTarget()).on(join.getPredicate()); + } + queryDslMongodbPredicate.predicate = ExpressionUtils.allOf(new Predicate[]{first.predicate,second.predicate}); + return queryDslMongodbPredicate; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/QueryDslMongodbPredicate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/QueryDslMongodbPredicate.java new file mode 100644 index 0000000000..b1a42eb452 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/QueryDslMongodbPredicate.java @@ -0,0 +1,48 @@ +package org.springframework.data.mongodb.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.mongodb.core.mapping.DBRef; + +import com.mysema.query.mongodb.MongodbQuery; +import com.mysema.query.types.Path; +import com.mysema.query.types.Predicate; + +public class QueryDslMongodbPredicate { + PredicateBuilder predicateBuilder; + + QueryDslMongodbPredicate(PredicateBuilder predicateBuilder) { + this.predicateBuilder = predicateBuilder; + } + + Predicate predicate; + List> joins = new ArrayList>(); + + private Join addJoin(Path ref, Path target){ + Join join = new Join(ref, target,this); + joins.add(join); + return join; + } + + /** + * Create a pathway to {@link DBRef} properties on {@link T} document + * + * @param ref + * @param target + * @return + */ + public Join join(Path ref, Path target) { + return addJoin(ref, target); + } + + public void applyPredicate(MongodbQuery mongodbQuery) { + for (Join join : joins) { + mongodbQuery.join(join.getRef(), join.getTarget()).on(join.getPredicate()); + } + if(predicate != null){ + mongodbQuery.where(predicate); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specification.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specification.java new file mode 100644 index 0000000000..b03b6799be --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specification.java @@ -0,0 +1,15 @@ +package org.springframework.data.mongodb.domain; + + + + +public interface Specification { + + /** + * Build WHERE clause based on related Document. + * + * @param predicateBuilder + */ + QueryDslMongodbPredicate buildPredicate(PredicateBuilder predicateBuilder); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specifications.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specifications.java new file mode 100644 index 0000000000..7324dec71a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/domain/Specifications.java @@ -0,0 +1,26 @@ +package org.springframework.data.mongodb.domain; + +public class Specifications implements Specification { + + private Specification specification; + + public Specifications(Specification specification) { + this.specification = specification; + } + + public static Specifications where(Specification specification) { + return new Specifications(specification); + } + public Specifications and(final Specification spec) { + return new Specifications(new Specification() { + public QueryDslMongodbPredicate buildPredicate(PredicateBuilder predicateBuilder) { + return predicateBuilder.and(specification.buildPredicate(predicateBuilder), spec.buildPredicate(predicateBuilder)); + } + }); + } + + public QueryDslMongodbPredicate buildPredicate(PredicateBuilder predicateBuilder) { + return specification.buildPredicate(predicateBuilder); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/QueryDslMongoSpecificationExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/QueryDslMongoSpecificationExecutor.java new file mode 100644 index 0000000000..6f7960f9d2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/QueryDslMongoSpecificationExecutor.java @@ -0,0 +1,59 @@ +package org.springframework.data.mongodb.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.domain.Specification; + + +/** + * Interface to allow execution of {@link Specification}s based on the QueryDSL DbRef predicate. + * + * @author Patryk Wasik + */ +public interface QueryDslMongoSpecificationExecutor { + + /** + * Returns a single entity matching the given {@link Specification}. + * + * @param spec + * @return + */ + T findOne(Specification spec); + + /** + * Returns all entities matching the given {@link Specification}. + * + * @param spec + * @return + */ + List findAll(Specification spec); + + /** + * Returns a {@link Page} of entities matching the given {@link Specification}. + * + * @param spec + * @param pageable + * @return + */ + Page findAll(Specification spec, Pageable pageable); + + /** + * Returns all entities matching the given {@link Specification} and {@link Sort}. + * + * @param spec + * @param sort + * @return + */ + List findAll(Specification spec, Sort sort); + + /** + * Returns the number of instances that the given {@link Specification} will return. + * + * @param spec the {@link Specification} to count instances for + * @return the number of instances + */ + long count(Specification spec); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QueryDslMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QueryDslMongoRepository.java index f5afebc0e9..72925d29df 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QueryDslMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QueryDslMongoRepository.java @@ -25,6 +25,9 @@ import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.domain.PredicateBuilder; +import org.springframework.data.mongodb.domain.Specification; +import org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QueryDslPredicateExecutor; @@ -45,7 +48,7 @@ * @author Oliver Gierke */ public class QueryDslMongoRepository extends SimpleMongoRepository implements - QueryDslPredicateExecutor { + QueryDslPredicateExecutor, QueryDslMongoSpecificationExecutor { private final PathBuilder builder; @@ -187,4 +190,57 @@ private OrderSpecifier toOrder(Order order) { return new OrderSpecifier(order.isAscending() ? com.mysema.query.types.Order.ASC : com.mysema.query.types.Order.DESC, property); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor#findOne(org.springframework.data.mongodb.domain.Specification) + */ + public T findOne(Specification spec) { + return createQueryFor(spec).uniqueResult(); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor#findAll(org.springframework.data.mongodb.domain.Specification) + */ + public List findAll(Specification spec) { + return createQueryFor(spec).list(); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor#findAll(org.springframework.data.mongodb.domain.Specification, org.springframework.data.domain.Pageable) + */ + public Page findAll(Specification spec, Pageable pageable) { + MongodbQuery countQuery = createQueryFor(spec); + MongodbQuery query = createQueryFor(spec); + return new PageImpl(applyPagination(query, pageable).list(),pageable,countQuery.count()); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor#findAll(org.springframework.data.mongodb.domain.Specification, org.springframework.data.domain.Sort) + */ + public List findAll(Specification spec, Sort sort) { + MongodbQuery query = createQueryFor(spec); + return applySorting(query, sort).list(); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.QueryDslMongoSpecificationExecutor#count(org.springframework.data.mongodb.domain.Specification) + */ + public long count(Specification spec) { + return createQueryFor(spec).count(); + } + + /** + * Creates a {@link MongodbQuery} for the given {@link Specification}. + * + * @param predicate + * @return + */ + private MongodbQuery createQueryFor(Specification specification) { + + Class domainType = getEntityInformation().getJavaType(); + + MongodbQuery query = new SpringDataMongodbQuery(getMongoOperations(), domainType); + specification.buildPredicate(new PredicateBuilder()).applyPredicate(query); + return query; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Boss.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Boss.java new file mode 100644 index 0000000000..6001caf056 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Boss.java @@ -0,0 +1,13 @@ +package org.springframework.data.mongodb.repository; + +import java.math.BigInteger; + +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +public class Boss { + + BigInteger id; + String name; + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 34d6f2366f..231232cb36 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -53,6 +53,9 @@ public enum Sex { @DBRef User creator; + + @DBRef + Boss boss; public Person() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index bf35a99b6b..85aaa8fecd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -37,7 +37,7 @@ * * @author Oliver Gierke */ -public interface PersonRepository extends MongoRepository, QueryDslPredicateExecutor { +public interface PersonRepository extends MongoRepository, QueryDslPredicateExecutor, QueryDslMongoSpecificationExecutor { /** * Returns all {@link Person}s with the given lastname. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java index 51196811ec..9e68404011 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java @@ -15,6 +15,16 @@ */ package org.springframework.data.mongodb.repository; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.springframework.data.mongodb.domain.PredicateBuilder; +import org.springframework.data.mongodb.domain.QueryDslMongodbPredicate; +import org.springframework.data.mongodb.domain.Specification; +import org.springframework.data.mongodb.domain.Specifications; import org.springframework.test.context.ContextConfiguration; /** @@ -23,5 +33,97 @@ * @author Oliver Gierke */ @ContextConfiguration -public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { +public class PersonRepositoryIntegrationTests extends + AbstractPersonRepositoryIntegrationTests { + + @Test + public void testSpecification() { + + User user = new User(); + user.username = "Patryk"; + operations.save(user); + + Boss jozef = new Boss(); + jozef.name = "Jozef"; + operations.save(jozef); + + Boss maks = new Boss(); + maks.name = "Maks"; + operations.save(maks); + + Person wojtek = new Person("Wojtek", "Krzaklewski", 29); + wojtek.creator = user; + wojtek.boss = jozef; + repository.save(wojtek); + + Person jacek = new Person("Jacek", "Mroz", 29); + jacek.creator = user; + jacek.boss = jozef; + repository.save(jacek); + + Person michal = new Person("Michal", "Ktostam", 31); + michal.creator = user; + michal.boss = maks; + repository.save(michal); + + Specification specCreatorPatryk = new Specification() { + + public QueryDslMongodbPredicate buildPredicate( + PredicateBuilder predicateBuilder) { + return predicateBuilder.where().join(QPerson.person.creator, QUser.user) + .on(QUser.user.username.eq("Patryk")); + } + }; + List findAll = repository.findAll(specCreatorPatryk); + + assertThat(findAll, hasSize(3)); + assertThat(findAll, hasItem(wojtek)); + assertThat(findAll, hasItem(michal)); + assertThat(findAll, hasItem(jacek)); + + Specification specPersonWojtek = new Specification() { + + public QueryDslMongodbPredicate buildPredicate( + PredicateBuilder predicateBuilder) { + return predicateBuilder.where(QPerson.person.firstname.eq("Wojtek")); + } + }; + + findAll = repository.findAll(specPersonWojtek); + + assertThat(findAll, hasSize(1)); + assertThat(findAll, hasItem(wojtek)); + + findAll = repository.findAll(Specifications.where(specCreatorPatryk).and(specPersonWojtek)); + + assertThat(findAll, hasSize(1)); + assertThat(findAll, hasItem(wojtek)); + + + Specification specBossJozef = new Specification() { + + public QueryDslMongodbPredicate buildPredicate( + PredicateBuilder predicateBuilder) { + return predicateBuilder.where().join(QPerson.person.boss, QBoss.boss) + .on(QBoss.boss.name.eq("Jozef")); + } + }; + findAll = repository.findAll(specBossJozef); + + assertThat(findAll, hasSize(2)); + assertThat(findAll, hasItem(wojtek)); + assertThat(findAll, hasItem(jacek)); + + findAll = repository.findAll(Specifications.where(specCreatorPatryk).and(new Specification() { + + public QueryDslMongodbPredicate buildPredicate( + PredicateBuilder predicateBuilder) { + return predicateBuilder.where().join(QPerson.person.boss, QBoss.boss).on(QBoss.boss.name.eq("Maks")); + } + })); + + assertThat(findAll, hasSize(1)); + assertThat(findAll, hasItem(michal)); + + } }