Browse Source

DATAMONGO-2059 - Polishing.

Move query rewriting into CountQuery. Consider existing $and items during query rewrite.

Original pull request: #604.
pull/802/head
Mark Paluch 6 years ago
parent
commit
b5bc4320aa
No known key found for this signature in database
GPG Key ID: 51A00FA751B91849
  1. 227
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java
  2. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 106
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  5. 164
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java
  6. 90
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

227
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java

@ -0,0 +1,227 @@
/*
* Copyright 2019 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
*
* https://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.mongodb.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.springframework.data.geo.Point;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* Value object representing a count query. Count queries using {@code $near} or {@code $nearSphere} require a rewrite
* to {@code $geoWithin}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.3
*/
class CountQuery {
private Document source;
private CountQuery(Document source) {
this.source = source;
}
public static CountQuery of(Document source) {
return new CountQuery(source);
}
/**
* Returns the query {@link Document} that can be used with {@code countDocuments()}. Potentially rewrites the query
* to be usable with {@code countDocuments()}.
*
* @return the query {@link Document} that can be used with {@code countDocuments()}.
*/
public Document toQueryDocument() {
if (!requiresRewrite(source)) {
return source;
}
Document target = new Document();
for (Map.Entry<String, Object> entry : source.entrySet()) {
if (entry.getValue() instanceof Document && requiresRewrite(entry.getValue())) {
Document theValue = (Document) entry.getValue();
target.putAll(createGeoWithin(entry.getKey(), theValue, source.get("$and")));
continue;
}
if (entry.getValue() instanceof Collection && requiresRewrite(entry.getValue())) {
Collection<?> source = (Collection<?>) entry.getValue();
target.put(entry.getKey(), rewriteCollection(source));
continue;
}
if ("$and".equals(entry.getKey()) && target.containsKey("$and")) {
// Expect $and to be processed with Document and createGeoWithin.
continue;
}
target.put(entry.getKey(), entry.getValue());
}
return target;
}
/**
* @param valueToInspect
* @return {@code true} if the enclosing element needs to be rewritten.
*/
private boolean requiresRewrite(Object valueToInspect) {
if (valueToInspect instanceof Document) {
return requiresRewrite((Document) valueToInspect);
}
if (valueToInspect instanceof Collection) {
return requiresRewrite((Collection) valueToInspect);
}
return false;
}
private boolean requiresRewrite(Collection<?> collection) {
for (Object o : collection) {
if (o instanceof Document && requiresRewrite((Document) o)) {
return true;
}
}
return false;
}
private boolean requiresRewrite(Document document) {
if (containsNear(document)) {
return true;
}
for (Object entry : document.values()) {
if (requiresRewrite(entry)) {
return true;
}
}
return false;
}
private Collection<Object> rewriteCollection(Collection<?> source) {
Collection<Object> rewrittenCollection = new ArrayList<>(source.size());
for (Object item : source) {
if (item instanceof Document && requiresRewrite(item)) {
rewrittenCollection.add(CountQuery.of((Document) item).toQueryDocument());
} else {
rewrittenCollection.add(item);
}
}
return rewrittenCollection;
}
/**
* Rewrite the near query for field {@code key} to {@code $geoWithin}.
*
* @param key the queried field.
* @param source source {@link Document}.
* @param $and potentially existing {@code $and} condition.
* @return the rewritten query {@link Document}.
*/
private static Document createGeoWithin(String key, Document source, @Nullable Object $and) {
boolean spheric = source.containsKey("$nearSphere");
Object $near = spheric ? source.get("$nearSphere") : source.get("$near");
Number maxDistance = source.containsKey("$maxDistance") ? (Number) source.get("$maxDistance") : Double.MAX_VALUE;
List<Object> $centerMax = Arrays.asList(toCenterCoordinates($near), maxDistance);
Document $geoWithinMax = new Document("$geoWithin",
new Document(spheric ? "$centerSphere" : "$center", $centerMax));
if (!containsNearWithMinDistance(source)) {
return new Document(key, $geoWithinMax);
}
Number minDistance = (Number) source.get("$minDistance");
List<Object> $centerMin = Arrays.asList(toCenterCoordinates($near), minDistance);
Document $geoWithinMin = new Document("$geoWithin",
new Document(spheric ? "$centerSphere" : "$center", $centerMin));
List<Document> criteria = new ArrayList<>();
if ($and != null) {
if ($and instanceof Collection) {
criteria.addAll((Collection) $and);
} else {
throw new IllegalArgumentException(
"Cannot rewrite query as it contains an '$and' element that is not a Collection!: Offending element: "
+ $and);
}
}
criteria.add(new Document("$nor", Collections.singletonList(new Document(key, $geoWithinMin))));
criteria.add(new Document(key, $geoWithinMax));
return new Document("$and", criteria);
}
private static boolean containsNear(Document source) {
return source.containsKey("$near") || source.containsKey("$nearSphere");
}
private static boolean containsNearWithMinDistance(Document source) {
if (!containsNear(source)) {
return false;
}
return source.containsKey("$minDistance");
}
private static Object toCenterCoordinates(Object value) {
if (ObjectUtils.isArray(value)) {
return value;
}
if (value instanceof Point) {
return Arrays.asList(((Point) value).getX(), ((Point) value).getY());
}
if (value instanceof Document && ((Document) value).containsKey("x")) {
Document point = (Document) value;
return Arrays.asList(point.get("x"), point.get("y"));
}
return value;
}
}

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -1192,7 +1192,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
LOGGER.debug("Executing count: {} in collection: {}", serializeToJsonSafely(filter), collectionName); LOGGER.debug("Executing count: {} in collection: {}", serializeToJsonSafely(filter), collectionName);
} }
return execute(collectionName, collection -> collection.countDocuments(QueryMapper.processCountFilter(filter), options)); return execute(collectionName,
collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
} }
/* /*

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -1301,7 +1301,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
protected Mono<Long> doCount(String collectionName, Document filter, CountOptions options) { protected Mono<Long> doCount(String collectionName, Document filter, CountOptions options) {
return createMono(collectionName, return createMono(collectionName,
collection -> collection.countDocuments(QueryMapper.processCountFilter(filter), options)); collection -> collection.countDocuments(CountQuery.of(filter).toQueryDocument(), options));
} }
/* /*

106
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -15,8 +15,17 @@
*/ */
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -24,10 +33,10 @@ import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.Association; import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
@ -1282,97 +1291,4 @@ public class QueryMapper {
public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() { public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
return mappingContext; return mappingContext;
} }
public static Document processCountFilter(Document source) {
Document target = new Document();
for (Entry<String, Object> entry : source.entrySet()) {
if (entry.getValue() instanceof Document) {
Document theValue = (Document) entry.getValue();
if (containsNear(theValue)) {
target.putAll(createGeoWithin(entry.getKey(), theValue));
} else {
target.put(entry.getKey(), entry.getValue());
}
} else if (entry.getValue() instanceof Collection) {
Collection<Object> tmp = new ArrayList<>();
for (Object val : (Collection) entry.getValue()) {
if (val instanceof Document) {
tmp.add(processCountFilter((Document) val));
} else {
tmp.add(val);
}
}
target.put(entry.getKey(), tmp);
} else {
target.put(entry.getKey(), entry.getValue());
}
}
return target;
}
private static Document createGeoWithin(String key, Document source) {
boolean spheric = source.containsKey("$nearSphere");
Object $near = spheric ? source.get("$nearSphere") : source.get("$near");
Number maxDistance = source.containsKey("$maxDistance") ? (Number) source.get("$maxDistance") : Double.MAX_VALUE;
List<Object> $centerMax = Arrays.asList(toCenterCoordinates($near), maxDistance);
Document $geoWithinMax = new Document("$geoWithin",
new Document(spheric ? "$centerSphere" : "$center", $centerMax));
if (!containsNearWithMinDistance(source)) {
return new Document(key, $geoWithinMax);
}
Number minDistance = (Number) source.get("$minDistance");
List<Object> $centerMin = Arrays.asList(toCenterCoordinates($near), minDistance);
Document $geoWithinMin = new Document("$geoWithin",
new Document(spheric ? "$centerSphere" : "$center", $centerMin));
List<Document> criteria = new ArrayList<>();
criteria.add(new Document("$nor", Arrays.asList(new Document(key, $geoWithinMin))));
criteria.add(new Document(key, $geoWithinMax));
return new Document("$and", criteria);
}
private static boolean containsNear(Document source) {
if (source.containsKey("$near") || source.containsKey("$nearSphere")) {
return true;
}
return false;
}
private static boolean containsNearWithMinDistance(Document source) {
if (!containsNear(source)) {
return false;
}
return source.containsKey("$minDistance");
}
private static Object toCenterCoordinates(Object value) {
if (ObjectUtils.isArray(value)) {
return value;
}
if (value instanceof Point) {
return Arrays.asList(((Point) value).getX(), ((Point) value).getY());
}
if (value instanceof Document && ((Document) value).containsKey("x")) {
Document point = (Document) value;
return Arrays.asList(point.get("x"), point.get("y"));
}
return value;
}
} }

164
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java

@ -0,0 +1,164 @@
/*
* Copyright 2019 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
*
* https://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.mongodb.core;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
/**
* Unit tests for {@link CountQuery}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public class CountQueryUnitTests {
QueryMapper mapper;
MongoMappingContext context;
MappingMongoConverter converter;
MongoDbFactory factory = mock(MongoDbFactory.class);
@Before
public void setUp() {
this.context = new MongoMappingContext();
this.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), context);
this.converter.afterPropertiesSet();
this.mapper = new QueryMapper(converter);
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithoutDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}}"));
}
@Test // DATAMONGO-2059
public void nearAndExisting$and() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)).minDistance(0.01))
.addCriteria(new Criteria().andOperator(where("foo").is("bar")));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse("{\"$and\":[" //
+ "{\"foo\":\"bar\"}" //
+ "{\"$nor\":[{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 0.01]}}}]},"//
+ " {\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 1.7976931348623157E308]}}},"//
+ "]}"));
}
@Test // DATAMONGO-2059
public void nearSphereToGeoWithinWithoutDistance() {
Query source = query(where("location").nearSphere(new Point(-73.99171, 40.738868)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(
org.bson.Document.parse("{\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearSphereToGeoWithinWithMaxDistance() {
Query source = query(where("location").nearSphere(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMinDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)).minDistance(0.01));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$and\":[{\"$nor\":[{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 0.01]}}}]},"
+ " {\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 1.7976931348623157E308]}}}]}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistanceAndCombinedWithOtherCriteria() {
Query source = query(
where("name").is("food").and("location").near(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"name\": \"food\", \"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMinDistanceOrCombinedWithOtherCriteria() {
Query source = query(new Criteria().orOperator(where("name").is("food"),
where("location").near(new Point(-73.99171, 40.738868)).minDistance(0.01)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$or\" : [ { \"name\": \"food\" }, {\"$and\":[{\"$nor\":[{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 0.01]}}}]},{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 1.7976931348623157E308]}}}]} ]}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistanceOrCombinedWithOtherCriteria() {
Query source = query(new Criteria().orOperator(where("name").is("food"),
where("location").near(new Point(-73.99171, 40.738868)).maxDistance(10)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}"));
}
private org.bson.Document postProcessQueryForCount(Query source) {
org.bson.Document intermediate = mapper.getMappedObject(source.getQueryObject(), (MongoPersistentEntity<?>) null);
return CountQuery.of(intermediate).toQueryDocument();
}
}

90
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

@ -890,96 +890,6 @@ public class QueryMapperUnitTests {
assertThat(target).isEqualTo(new org.bson.Document("_id", "id-1")); assertThat(target).isEqualTo(new org.bson.Document("_id", "id-1"));
} }
@Test // DATAMONGO-2059
public void nearToGeoWithinWithoutDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}}"));
}
@Test // DATAMONGO-2059
public void nearSphereToGeoWithinWithoutDistance() {
Query source = query(where("location").nearSphere(new Point(-73.99171, 40.738868)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(
org.bson.Document.parse("{\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearSphereToGeoWithinWithMaxDistance() {
Query source = query(where("location").nearSphere(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMinDistance() {
Query source = query(where("location").near(new Point(-73.99171, 40.738868)).minDistance(0.01));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$and\":[{\"$nor\":[{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 0.01]}}}]},"
+ " {\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 1.7976931348623157E308]}}}]}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistanceAndCombinedWithOtherCriteria() {
Query source = query(
where("name").is("food").and("location").near(new Point(-73.99171, 40.738868)).maxDistance(10));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document
.parse("{\"name\": \"food\", \"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMinDistanceOrCombinedWithOtherCriteria() {
Query source = query(new Criteria().orOperator(where("name").is("food"),
where("location").near(new Point(-73.99171, 40.738868)).minDistance(0.01)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$or\" : [ { \"name\": \"food\" }, {\"$and\":[{\"$nor\":[{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 0.01]}}}]},{\"location\":{\"$geoWithin\":{\"$center\":[ [ -73.99171, 40.738868 ], 1.7976931348623157E308]}}}]} ]}"));
}
@Test // DATAMONGO-2059
public void nearToGeoWithinWithMaxDistanceOrCombinedWithOtherCriteria() {
Query source = query(new Criteria().orOperator(where("name").is("food"),
where("location").near(new Point(-73.99171, 40.738868)).maxDistance(10)));
org.bson.Document target = postProcessQueryForCount(source);
assertThat(target).isEqualTo(org.bson.Document.parse(
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}"));
}
private org.bson.Document postProcessQueryForCount(Query source) {
org.bson.Document intermediate = mapper.getMappedObject(source.getQueryObject(), (MongoPersistentEntity<?>) null);
return QueryMapper.processCountFilter(intermediate);
}
@Document @Document
public class Foo { public class Foo {
@Id private ObjectId id; @Id private ObjectId id;

Loading…
Cancel
Save