DATAMONGO-2475 - MongodbDocumentSerializer: reduce nested ANDs and ORs.

Reduce the number of nested $and / $or clauses by combining them into a single document where possible. This allows to simplify the following statement

{
  "$or": [
    {
      "$or": [
        {
          "$or": [
            {
              "$or": [
                { "firstname": "Hencxjo" },
                { "lastname": "Leon" }
              ]
            },
            { "age": { "$lte": 30 } }
          ]
        },
        { "age": { "$gte": 20 } }
      ]
    },
    { "uniqueId": { "$exists": false } }
  ]
}

to just

{
 "$or": [
   { "firstname": "Hencxjo" },
   { "lastname": "Leon" },
   { "age": { "$lte": 30 } },
   { "age": { "$gte": 20 } },
   { "uniqueId": { "$exists": false } }
 ]
}

Original Pull Request: #834
This commit is contained in:
Enrique León Molina
2020-02-15 21:10:47 +01:00
committed by Christoph Strobl
parent a04821ff90
commit 7b5fea960f
2 changed files with 69 additions and 13 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
@@ -49,6 +50,7 @@ import com.querydsl.mongodb.MongodbOps;
* @author Mark Paluch
* @author Christoph Strobl
* @author Mikhail Kaduchka
* @author Enrique Leon Molina
* @since 2.1
*/
abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
@@ -181,19 +183,43 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
return asDocument(asDBKey(expr, 0), "");
} else if (op == Ops.AND) {
Map<Object, Object> lhs = (Map<Object, Object>) handle(expr.getArg(0));
Map<Object, Object> rhs = (Map<Object, Object>) handle(expr.getArg(1));
List<Map<Object, Object>> pendingDocuments = new LinkedList<>();
for (int i = 0; i < 2; i++) {
Map<Object, Object> document = (Map<Object, Object>) handle(expr.getArg(i));
if (document.keySet().size() == 1 && document.containsKey("$and")) {
pendingDocuments.addAll((Collection<Map<Object, Object>>) document.get("$and"));
} else {
pendingDocuments.add(document);
}
}
LinkedHashSet<Object> lhs2 = new LinkedHashSet<>(lhs.keySet());
lhs2.retainAll(rhs.keySet());
List<Map<Object, Object>> unmergeableDocuments = new LinkedList<>();
if (lhs2.isEmpty()) {
lhs.putAll(rhs);
return lhs;
List<Map<Object, Object>> generatedDocuments = new LinkedList<>();
do {
Map<Object, Object> lhs = pendingDocuments.remove(0);
for (Map<Object, Object> rhs : pendingDocuments) {
LinkedHashSet<Object> lhs2 = new LinkedHashSet<>(lhs.keySet());
lhs2.retainAll(rhs.keySet());
if (lhs2.isEmpty()) {
lhs.putAll(rhs);
} else {
unmergeableDocuments.add(rhs);
}
}
generatedDocuments.add(lhs);
pendingDocuments = unmergeableDocuments;
unmergeableDocuments = new LinkedList<>();
} while(!pendingDocuments.isEmpty());
if (generatedDocuments.size() == 1) {
return generatedDocuments.get(0);
} else {
List<Object> list = new ArrayList<>(2);
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
List<Object> list = new ArrayList<>(expr.getArgs().size());
list.addAll(generatedDocuments);
return asDocument("$and", list);
}
@@ -212,9 +238,15 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
} else if (op == Ops.OR) {
List<Object> list = new ArrayList<>(2);
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
List<Object> list = new LinkedList<>();
for (int i = 0; i < 2; i++) {
Map<Object, Object> document = (Map<Object, Object>) handle(expr.getArg(i));
if (document.keySet().size() == 1 && document.containsKey("$or")) {
list.addAll((Collection<Object>) document.get("$or"));
} else {
list.add(document);
}
}
return asDocument("$or", list);
} else if (op == Ops.NE) {

View File

@@ -43,6 +43,7 @@ import org.springframework.data.mongodb.repository.QAddress;
import org.springframework.data.mongodb.repository.QPerson;
import com.querydsl.core.types.Ops;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.PredicateOperation;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.BooleanOperation;
@@ -57,6 +58,7 @@ import com.querydsl.core.types.dsl.StringPath;
* @author Christoph Strobl
* @author Mark Paluch
* @author Mikhail Kaduchka
* @author Enrique Leon Molina
*/
@ExtendWith(MockitoExtension.class)
public class SpringDataMongodbSerializerUnitTests {
@@ -174,6 +176,28 @@ public class SpringDataMongodbSerializerUnitTests {
"{\"$and\": [{\"$or\": [{\"firstname\": \"John\"}, {\"firstname\": \"Sarah\"}]}, {\"$or\": [{\"lastname\": \"Smith\"}, {\"lastname\": \"Connor\"}]}]}"));
}
@Test // DATAMONGO-2475
public void chainedOrsInSameDocument() {
Predicate predicate = QPerson.person.firstname.eq("firstname_value").or(
QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.age.goe(30)).or(
QPerson.person.age.loe(20)).or(QPerson.person.uniqueId.isNull());
assertThat(serializer.handle(predicate)).isEqualTo(Document.parse(
"{\"$or\": [{\"firstname\": \"firstname_value\"}, {\"lastname\": \"lastname_value\"}, {\"age\": {\"$gte\": 30}}, {\"age\": {\"$lte\": 20}}, {\"uniqueId\": {\"$exists\": false}}]}"));
}
@Test // DATAMONGO-2475
public void chainedAndsInSameDocument() {
Predicate predicate = QPerson.person.firstname.eq("firstname_value").and(
QPerson.person.lastname.eq("lastname_value")).and(QPerson.person.age.goe(30)).and(
QPerson.person.age.loe(20)).and(QPerson.person.uniqueId.isNull());
assertThat(serializer.handle(predicate)).isEqualTo(Document.parse(
"{\"$and\": [{\"firstname\": \"firstname_value\", \"lastname\": \"lastname_value\", \"age\": {\"$gte\": 30}, \"uniqueId\": {\"$exists\": false}}, {\"age\": {\"$lte\": 20}}]}"));
}
class Address {
String id;
String street;