DATAMONGO-766 - Support for nested field references through @Field.

The MappingMongoConverter now supports reading and writing nested fields from and to documents by using @Field(…) with a path expression, e.g. @Field("a.b"). We now correctly create the nested objects and also reuse the previously created intermediates when populating further properties.

Not that this might cause the need to define field ordering explicitly as later properties might override the values set using a path reference in @Field.
This commit is contained in:
Oliver Gierke
2013-10-08 12:23:24 +02:00
parent fe41202f96
commit 1b83ff0382
4 changed files with 263 additions and 11 deletions

View File

@@ -0,0 +1,123 @@
/*
* 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.mongodb.core.convert;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.util.Assert;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Wrapper value object for a {@link BasicDBObject} to be able to access raw values by {@link MongoPersistentProperty}
* references. The accessors will transparently resolve nested document values that a {@link MongoPersistentProperty}
* might refer to through a path expression in field names.
*
* @author Oliver Gierke
*/
class DBObjectAccessor {
private final DBObject dbObject;
/**
* Creates a new {@link DBObjectAccessor} for the given {@link DBObject}.
*
* @param dbObject must be a {@link BasicDBObject} effectively, must not be {@literal null}.
*/
public DBObjectAccessor(DBObject dbObject) {
Assert.notNull(dbObject, "DBObject must not be null!");
Assert.isInstanceOf(BasicDBObject.class, dbObject, "Given DBObject must be a BasicDBObject!");
this.dbObject = dbObject;
}
/**
* Puts the given value into the backing {@link DBObject} based on the coordinates defined through the given
* {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist
* of path traversals so we might need to create intermediate {@link BasicDBObject}s.
*
* @param prop must not be {@literal null}.
* @param value
*/
public void put(MongoPersistentProperty prop, Object value) {
Assert.notNull(prop, "MongoPersistentProperty must not be null!");
String fieldName = prop.getFieldName();
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
DBObject dbObject = this.dbObject;
while (parts.hasNext()) {
String part = parts.next();
if (parts.hasNext()) {
BasicDBObject nestedDbObject = new BasicDBObject();
dbObject.put(part, nestedDbObject);
dbObject = nestedDbObject;
} else {
dbObject.put(part, value);
}
}
}
/**
* Returns the value the given {@link MongoPersistentProperty} refers to. By default this will be a direct field but
* the method will also transparently resolve nested values the {@link MongoPersistentProperty} might refer to through
* a path expression in the field name metadata.
*
* @param property must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public Object get(MongoPersistentProperty property) {
String fieldName = property.getFieldName();
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<Object, Object> source = this.dbObject.toMap();
Object result = null;
while (source != null && parts.hasNext()) {
result = source.get(parts.next());
if (parts.hasNext()) {
source = getAsMap(result);
}
}
return result;
}
@SuppressWarnings("unchecked")
private Map<Object, Object> getAsMap(Object source) {
if (source instanceof BasicDBObject) {
return ((DBObject) source).toMap();
}
if (source instanceof Map) {
return (Map<Object, Object>) source;
}
return null;
}
}

View File

@@ -385,7 +385,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (!conversions.isSimpleType(propertyObj.getClass())) {
writePropertyInternal(propertyObj, dbo, prop);
} else {
writeSimpleInternal(propertyObj, dbo, prop.getFieldName());
writeSimpleInternal(propertyObj, dbo, prop);
}
}
}
@@ -410,26 +410,27 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return;
}
String name = prop.getFieldName();
DBObjectAccessor accessor = new DBObjectAccessor(dbo);
TypeInformation<?> valueType = ClassTypeInformation.from(obj.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (valueType.isCollectionLike()) {
DBObject collectionInternal = createCollection(asCollection(obj), prop);
dbo.put(name, collectionInternal);
accessor.put(prop, collectionInternal);
return;
}
if (valueType.isMap()) {
DBObject mapDbObj = createMap((Map<Object, Object>) obj, prop);
dbo.put(name, mapDbObj);
accessor.put(prop, mapDbObj);
return;
}
if (prop.isDbReference()) {
DBRef dbRefObj = createDBRef(obj, prop.getDBRef());
if (null != dbRefObj) {
dbo.put(name, dbRefObj);
accessor.put(prop, dbRefObj);
return;
}
}
@@ -438,18 +439,20 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Class<?> basicTargetType = conversions.getCustomWriteTarget(obj.getClass(), null);
if (basicTargetType != null) {
dbo.put(name, conversionService.convert(obj, basicTargetType));
accessor.put(prop, conversionService.convert(obj, basicTargetType));
return;
}
BasicDBObject propDbObj = new BasicDBObject();
Object existingValue = accessor.get(prop);
BasicDBObject propDbObj = existingValue instanceof BasicDBObject ? (BasicDBObject) existingValue
: new BasicDBObject();
addCustomTypeKeyIfNecessary(type, obj, propDbObj);
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass()) ? mappingContext
.getPersistentEntity(obj.getClass()) : mappingContext.getPersistentEntity(type);
writeInternal(obj, propDbObj, entity);
dbo.put(name, propDbObj);
accessor.put(prop, propDbObj);
}
private boolean isSubtype(Class<?> left, Class<?> right) {
@@ -667,6 +670,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
dbObject.put(key, getPotentiallyConvertedSimpleWrite(value));
}
private void writeSimpleInternal(Object value, DBObject dbObject, MongoPersistentProperty property) {
DBObjectAccessor accessor = new DBObjectAccessor(dbObject);
accessor.put(property, getPotentiallyConvertedSimpleWrite(value));
}
/**
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type.
* Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
@@ -965,7 +973,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private class MongoDbPropertyValueProvider implements PropertyValueProvider<MongoPersistentProperty> {
private final DBObject source;
private final DBObjectAccessor source;
private final SpELExpressionEvaluator evaluator;
private final Object parent;
@@ -978,7 +986,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(source);
Assert.notNull(evaluator);
this.source = source;
this.source = new DBObjectAccessor(source);
this.evaluator = evaluator;
this.parent = parent;
}
@@ -990,7 +998,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
public <T> T getPropertyValue(MongoPersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property);
if (value == null) {
return null;
@@ -1052,4 +1060,5 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (T) getPotentiallyConvertedSimpleRead(value, rawType);
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.mongodb.core.convert;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectUtils;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Unit tests for {@link DbObjectAccessor}.
*
* @see DATAMONGO-766
* @author Oliver Gierke
*/
public class DBObjectAccessorUnitTests {
MongoMappingContext context = new MongoMappingContext();
MongoPersistentEntity<?> projectingTypeEntity = context.getPersistentEntity(ProjectingType.class);
MongoPersistentProperty fooProperty = projectingTypeEntity.getPersistentProperty("foo");
@Test
public void putsNestedFieldCorrectly() {
DBObject dbObject = new BasicDBObject();
DBObjectAccessor accessor = new DBObjectAccessor(dbObject);
accessor.put(fooProperty, "FooBar");
DBObject aDbObject = DBObjectUtils.getAsDBObject(dbObject, "a");
assertThat(aDbObject.get("b"), is((Object) "FooBar"));
}
@Test
public void getsNestedFieldCorrectly() {
DBObject source = new BasicDBObject("a", new BasicDBObject("b", "FooBar"));
DBObjectAccessor accessor = new DBObjectAccessor(source);
assertThat(accessor.get(fooProperty), is((Object) "FooBar"));
}
@Test
public void returnsNullForNonExistingFieldPath() {
DBObjectAccessor accessor = new DBObjectAccessor(new BasicDBObject());
assertThat(accessor.get(fooProperty), is(nullValue()));
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNonBasicDBObjects() {
new DBObjectAccessor(new BasicDBList());
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullDBObject() {
new DBObjectAccessor(null);
}
static class ProjectingType {
String name;
@Field("a.b") String foo;
NestedType a;
}
static class NestedType {
String b;
String c;
}
}

View File

@@ -53,6 +53,9 @@ import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.DBObjectUtils;
import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.NestedType;
import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.ProjectingType;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -1425,6 +1428,29 @@ public class MappingMongoConverterUnitTests {
assertThat(converter.read(String.class, dbObject), is("{ \"firstname\" : \"Dave\"}"));
}
/**
* @see DATAMONGO-766
*/
@Test
public void writesProjectingTypeCorrectly() {
NestedType nested = new NestedType();
nested.c = "C";
ProjectingType type = new ProjectingType();
type.name = "name";
type.foo = "bar";
type.a = nested;
BasicDBObject result = new BasicDBObject();
converter.write(type, result);
assertThat(result.get("name"), is((Object) "name"));
DBObject aValue = DBObjectUtils.getAsDBObject(result, "a");
assertThat(aValue.get("b"), is((Object) "bar"));
assertThat(aValue.get("c"), is((Object) "C"));
}
@Document
class MapDBRef {
@org.springframework.data.mongodb.core.mapping.DBRef Map<String, MapDBRefVal> map;