Skip to content

Commit

Permalink
new implementation of $elemMatch support (#985)
Browse files Browse the repository at this point in the history
* use a query for complex hasThisElement clauses?

* introduce elemMatch() with better semantics

* expand tests to ensure old and new methods are tested.
add support for $not with $elemMatch

* deprecate `doesNotHaveThisElement`

* clean up validations

* rename method
  • Loading branch information
evanchooly authored and Justin Lee committed Oct 10, 2016
1 parent 8f20d4f commit dea127f
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 41 deletions.
9 changes: 5 additions & 4 deletions morphia/src/main/java/org/mongodb/morphia/mapping/Mapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import org.mongodb.morphia.mapping.lazy.LazyProxyFactory;
import org.mongodb.morphia.mapping.lazy.proxy.ProxiedEntityReference;
import org.mongodb.morphia.mapping.lazy.proxy.ProxyHelper;
import org.mongodb.morphia.query.Query;
import org.mongodb.morphia.query.QueryImpl;
import org.mongodb.morphia.query.ValidationException;

import java.io.IOException;
Expand Down Expand Up @@ -584,10 +586,9 @@ public DBObject toDBObject(final Object entity, final Map<Object, DBObject> invo
public Object toMongoObject(final MappedField mf, final MappedClass mc, final Object value) {
Object mappedValue = value;

/* if (mf == null && mc != null && value != null) {
mappedValue = toMongoObject(mappedValue, false);
} else*/
if (isAssignable(mf, value) || isEntity(mc)) {
if (value instanceof Query) {
mappedValue = ((QueryImpl) value).getQueryObject();
} else if (isAssignable(mf, value) || isEntity(mc)) {
//convert the value to Key (DBRef) if the field is @Reference or type is Key/DBRef, or if the destination class is an @Entity
try {
if (value instanceof Iterable) {
Expand Down
13 changes: 13 additions & 0 deletions morphia/src/main/java/org/mongodb/morphia/query/FieldEnd.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,31 @@ public interface FieldEnd<T> {
* @return T
* @mongodb.driver.manual reference/operator/query/elemMatch/ $elemMatch
* @see MapperOptions
* @deprecated use {@link #elemMatch(Query)} instead
*/
@Deprecated
T hasThisElement(Object val);

/**
* Checks that a field matches the provided query definition
*
* @param query the query to find certain field values
* @return T
* @mongodb.driver.manual reference/operator/query/elemMatch/ $elemMatch
*/
T elemMatch(Query query);

/**
* Checks that a field does not have the value listed. The options to store null/empty values apply here so to do partial matches on
* embedded objects, pass a reference to a partially populated instance with only the values of interest set to the values to check.
*
* @param val the value to check against
* @return T
* @mongodb.driver.manual reference/operator/query/elemMatch/ $elemMatch
* @deprecated use {@link #elemMatch(Query)} instead
* @see MapperOptions
*/
@Deprecated
T doesNotHaveThisElement(Object val);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ public T hasThisElement(final Object val) {
return addCriteria(FilterOperator.ELEMENT_MATCH, val, false);
}

@Override
public T elemMatch(final Query query) {
Assert.parametersNotNull("query", query);
return addCriteria(FilterOperator.ELEMENT_MATCH, query, not);
}

@Override
public T hasThisOne(final Object val) {
return addCriteria(FilterOperator.EQUAL, val);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ public DBCursor prepareCursor() {

@Override
public String toString() {
return String.format("{ %s %s }", getQueryObject(), projections == null ? "" : ", " + getFieldsObject());
return String.format("{ query: %s %s }", getQueryObject(), projections == null ? "" : ", projection: " + getFieldsObject());
}

/**
Expand Down
136 changes: 100 additions & 36 deletions morphia/src/test/java/org/mongodb/morphia/TestQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,49 +357,88 @@ public void testElemMatchQuery() {
.hasThisElement(new Keyword("Randy"))
.get());
}
@Test
public void testComplexElemMatchQuery() {
Keyword oscar = new Keyword("Oscar", 42);
getDs().save(new PhotoWithKeywords(oscar, new Keyword("Jim", 12)));
assertNull(getDs().find(PhotoWithKeywords.class)
.field("keywords")
.elemMatch(getDs()
.createQuery(Keyword.class)
.filter("keyword = ", "Oscar")
.filter("score = ", 12))
.get());

List<PhotoWithKeywords> keywords = getDs().find(PhotoWithKeywords.class)
.field("keywords")
.elemMatch(getDs()
.createQuery(Keyword.class)
.filter("score > ", 20)
.filter("score < ", 100))
.asList();
assertEquals(1, keywords.size());
assertEquals(oscar, keywords.get(0).keywords.get(0));
}

@Test
public void testElemMatchQueryCanBeNegated() {
public void testElemMatchVariants() {
final PhotoWithKeywords pwk1 = new PhotoWithKeywords();
final PhotoWithKeywords pwk2 = new PhotoWithKeywords("Kevin");
final PhotoWithKeywords pwk3 = new PhotoWithKeywords("Scott", "Joe", "Sarah");
final PhotoWithKeywords pwk4 = new PhotoWithKeywords(new Keyword("Scott", 14));

getDs().save(pwk1, pwk2, pwk3, pwk4);

Mapper mapper = getMorphia().getMapper();

List<Key<PhotoWithKeywords>> keys = getDs().find(PhotoWithKeywords.class)
.field("keywords")
.hasThisElement(new Keyword("Scott"))
.asKeyList();

assertEquals(2, keys.size());
assertFalse("because it doesn't have any keywords.", keys.contains(mapper.getKey(pwk1)));
assertFalse("because it doesn't have a matching keyword.", keys.contains(mapper.getKey(pwk2)));
assertTrue("because it has matching keyword.", keys.contains(mapper.getKey(pwk3)));
assertTrue("because it has matching keyword.", keys.contains(mapper.getKey(pwk4)));

keys = getDs().find(PhotoWithKeywords.class)
.field("keywords")
.hasThisElement(new Keyword(14))
.asKeyList();

assertEquals(1, keys.size());
assertFalse(keys.contains(mapper.getKey(pwk1)));
assertFalse(keys.contains(mapper.getKey(pwk2)));
assertFalse(keys.contains(mapper.getKey(pwk3)));
assertTrue(keys.contains(mapper.getKey(pwk4)));

keys = getDs().find(PhotoWithKeywords.class)
.field("keywords")
.doesNotHaveThisElement(new Keyword("Scott"))
.asKeyList();

assertEquals(2, keys.size());
assertTrue("because it doesn't have any keywords.", keys.contains(mapper.getKey(pwk1)));
assertTrue("because it doesn't have a matching keyword.", keys.contains(mapper.getKey(pwk2)));
assertFalse("because it has matching keyword.", keys.contains(mapper.getKey(pwk3)));
Iterator<Key<PhotoWithKeywords>> iterator = getDs().save(pwk1, pwk2, pwk3, pwk4).iterator();
Key<PhotoWithKeywords> key1 = iterator.next();
Key<PhotoWithKeywords> key2 = iterator.next();
Key<PhotoWithKeywords> key3 = iterator.next();
Key<PhotoWithKeywords> key4 = iterator.next();

validate(asList(key3, key4), asList(key1, key2), getDs().find(PhotoWithKeywords.class)
.field("keywords")
.hasThisElement(new Keyword("Scott"))
.asKeyList());

validate(asList(key3, key4), asList(key1, key2), getDs().find(PhotoWithKeywords.class)
.field("keywords")
.elemMatch(getDs()
.createQuery(Keyword.class)
.field("keyword").equal("Scott"))
.asKeyList());

validate(asList(key4), asList(key1, key2, key3), getDs().find(PhotoWithKeywords.class)
.field("keywords")
.hasThisElement(new Keyword(14))
.asKeyList());

validate(asList(key4), asList(key1, key2, key3), getDs().find(PhotoWithKeywords.class)
.field("keywords")
.elemMatch(getDs()
.createQuery(Keyword.class)
.field("score").equal(14))
.asKeyList());

validate(asList(key1, key2), asList(key3, key4), getDs().find(PhotoWithKeywords.class)
.field("keywords")
.doesNotHaveThisElement(new Keyword("Scott"))
.asKeyList());

validate(asList(key1, key2), asList(key3, key4), getDs().find(PhotoWithKeywords.class)
.field("keywords").not()
.elemMatch(getDs()
.createQuery(Keyword.class)
.field("keyword").equal("Scott"))
.asKeyList());
}

private void validate(final List<Key<PhotoWithKeywords>> found, final List<Key<PhotoWithKeywords>> notFound,
final List<Key<PhotoWithKeywords>> keys) {
assertEquals(found.size(), keys.size());
for (Key<PhotoWithKeywords> key : found) {
assertTrue(keys.contains(key));
}
for (Key<PhotoWithKeywords> key : notFound) {
assertFalse(keys.contains(key));
}
}

@Test
Expand Down Expand Up @@ -1166,6 +1205,31 @@ public Keyword(final String k, final Integer score) {
public Keyword(final Integer score) {
this.score = score;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Keyword)) {
return false;
}

final Keyword keyword1 = (Keyword) o;

if (keyword != null ? !keyword.equals(keyword1.keyword) : keyword1.keyword != null) {
return false;
}
return score != null ? score.equals(keyword1.score) : keyword1.score == null;

}

@Override
public int hashCode() {
int result = keyword != null ? keyword.hashCode() : 0;
result = 31 * result + (score != null ? score.hashCode() : 0);
return result;
}
}

public static class ContainsPhotoKey {
Expand Down

0 comments on commit dea127f

Please sign in to comment.