DATADOC-128 - Enable storing inheritance trees.

The document being persisted now gets a _class attribute to carry the actual type. That field key will be made configurable by a subsequent commit and its value should be interpreted as type hint to a client and  might be interpreted using a type mapper at a later stage as well (see DATADOC-63). For now it carries the fully-qualified Java class name.

On reads MappingMongoConverter will consider this field when choosing a type to bind the data to if - and only if - the type stored in there is a subtype of the actually requested one. So if we have a document carrying Person type information and you query for Contact you would get back a Person object. If you query for any other type not extending Contact you would get this custom type instead.

Added unit tests and an integration tests covering the Contact/Person scenario.
This commit is contained in:
Oliver Gierke
2011-05-10 18:21:47 +02:00
parent 47f184dbf0
commit 12ddfcc9f9
6 changed files with 242 additions and 54 deletions

View File

@@ -78,7 +78,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
*/
public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware, InitializingBean {
private static final String CUSTOM_TYPE_KEY = "_class";
public static final String CUSTOM_TYPE_KEY = "_class";
@SuppressWarnings({"unchecked"})
private static final List<Class<?>> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class, DBObject.class);
private static final List<Class<?>> VALID_ID_TYPES = Arrays.asList(new Class<?>[]{ObjectId.class, String.class, BigInteger.class, byte[].class});
@@ -113,7 +113,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (null != converters) {
for (Converter<?, ?> c : converters) {
registerConverter(c);
}
}
}
@@ -185,22 +184,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (null == dbo) {
return null;
}
Class<S> rawType = type.getType();
TypeInformation<? extends S> typeToUse = getMoreConcreteTargetType(dbo, type);
Class<? extends S> rawType = typeToUse.getType();
Class<?> customTarget = getCustomTarget(rawType, DBObject.class);
if (customTarget != null) {
return conversionService.convert(dbo, rawType);
}
if (type.isCollectionLike() && dbo instanceof BasicDBList) {
if (typeToUse.isCollectionLike() && dbo instanceof BasicDBList) {
List<Object> l = new ArrayList<Object>();
BasicDBList dbList = (BasicDBList) dbo;
for (Object o : dbList) {
if (o instanceof DBObject) {
Object newObj = read(type.getComponentType(), (DBObject) o);
Class<?> rawComponentType = type.getComponentType().getType();
Object newObj = read(typeToUse.getComponentType(), (DBObject) o);
Class<?> rawComponentType = typeToUse.getComponentType().getType();
if (newObj.getClass().isAssignableFrom(rawComponentType)) {
l.add(newObj);
@@ -215,7 +215,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
// Retrieve persistent entity info
MongoPersistentEntity<S> persistentEntity = (MongoPersistentEntity<S>) mappingContext.getPersistentEntity(type);
MongoPersistentEntity<S> persistentEntity = (MongoPersistentEntity<S>) mappingContext.getPersistentEntity(typeToUse);
if (persistentEntity == null) {
throw new MappingException("No mapping metadata found for " + rawType.getName());
}
@@ -306,30 +306,58 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return instance;
}
@SuppressWarnings("unchecked")
/**
* Root entry method into write conversion. Adds a type discriminator to the {@link DBObject}. Shouldn't be called for
* nested conversions.
*
* @see org.springframework.data.document.mongodb.MongoWriter#write(java.lang.Object, com.mongodb.DBObject)
*/
public void write(final Object obj, final DBObject dbo) {
if (null == obj) {
return;
}
Class<?> customTarget = getCustomTarget(obj.getClass(), DBObject.class);
if (customTarget != null) {
DBObject result = conversionService.convert(obj, DBObject.class);
dbo.putAll(result);
return;
}
boolean handledByCustomConverter = getCustomTarget(obj.getClass(), DBObject.class) != null;
if (Map.class.isAssignableFrom(obj.getClass())) {
writeMapInternal((Map<Object, Object>) obj, dbo);
return;
if (!handledByCustomConverter) {
dbo.put(CUSTOM_TYPE_KEY, obj.getClass().getName());
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(obj.getClass());
write(obj, dbo, entity);
writeInternal(obj, dbo);
}
/**
* Internal write conversion method which should be used for nested invocations.
*
* @param obj
* @param dbo
*/
@SuppressWarnings("unchecked")
protected void writeInternal(final Object obj, final DBObject dbo) {
if (null == obj) {
return;
}
protected void write(final Object obj, final DBObject dbo, MongoPersistentEntity<?> entity) {
Class<?> customTarget = getCustomTarget(obj.getClass(), DBObject.class);
if (customTarget != null) {
DBObject result = conversionService.convert(obj, DBObject.class);
dbo.putAll(result);
return;
}
if (Map.class.isAssignableFrom(obj.getClass())) {
writeMapInternal((Map<Object, Object>) obj, dbo);
return;
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(obj.getClass());
writeInternal(obj, dbo, entity);
}
protected void writeInternal(final Object obj, final DBObject dbo, MongoPersistentEntity<?> entity) {
if (obj == null) {
return;
@@ -464,7 +492,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
BasicDBList propNestedDbList = new BasicDBList();
for (Object propNestedObjItem : propObjColl) {
BasicDBObject propDbObj = new BasicDBObject();
write(propNestedObjItem, propDbObj);
writeInternal(propNestedObjItem, propDbObj);
propNestedDbList.add(propDbObj);
}
dbList.add(propNestedDbList);
@@ -473,7 +501,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
dbList.add(propObjItem);
} else {
BasicDBObject propDbObj = new BasicDBObject();
write(propObjItem, propDbObj, mappingContext.getPersistentEntity(prop.getComponentType()));
writeInternal(propObjItem, propDbObj, mappingContext.getPersistentEntity(prop.getComponentType()));
dbList.add(propDbObj);
}
}
@@ -505,7 +533,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
BasicDBObject propDbObj = new BasicDBObject();
write(obj, propDbObj, mappingContext.getPersistentEntity(prop.getTypeInformation()));
writeInternal(obj, propDbObj, mappingContext.getPersistentEntity(prop.getTypeInformation()));
dbo.put(name, propDbObj);
}
@@ -530,7 +558,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} else {
dbo.put("_class", componentType.getName());
}
write(val, newDbo);
writeInternal(val, newDbo);
dbo.put(simpleKey, newDbo);
}
} else {
@@ -674,6 +702,23 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
}
/**
* Inspects the a custom class definition stored inside the given {@link DBObject} and returns that in case it's a
* subtype of the given basic one.
*
* @param dbObject
* @param basicType
* @return
*/
@SuppressWarnings("unchecked")
private <S> TypeInformation<? extends S> getMoreConcreteTargetType(DBObject dbObject, TypeInformation<S> basicType) {
Class<?> documentsTargetType = findTypeToBeUsed(dbObject);
Class<S> rawType = basicType.getType();
boolean isMoreConcreteCustomType = documentsTargetType != null && rawType.isAssignableFrom(documentsTargetType);
return isMoreConcreteCustomType ? (TypeInformation<? extends S>) ClassTypeInformation.from(documentsTargetType)
: basicType;
}
protected <T> List<?> unwrapList(BasicDBList dbList, TypeInformation<T> targetType) {
List<Object> rootList = new LinkedList<Object>();
for (int i = 0; i < dbList.size(); i++) {

View File

@@ -109,12 +109,60 @@ public class MappingMongoConverterUnitTests {
assertThat(dbObject.get(Locale.US.toString()).toString(), is("Foo"));
}
/**
* @see DATADOC-128
*/
@Test
public void usesDocumentsStoredTypeIfSubtypeOfRequest() {
DBObject dbObject = new BasicDBObject();
dbObject.put("birthDate", new LocalDate());
dbObject.put(MappingMongoConverter.CUSTOM_TYPE_KEY, Person.class.getName());
assertThat(converter.read(Contact.class, dbObject), is(Person.class));
}
/**
* @see DATADOC-128
*/
@Test
public void ignoresDocumentsStoredTypeIfCompletelyDifferentTypeRequested() {
DBObject dbObject = new BasicDBObject();
dbObject.put("birthDate", new LocalDate());
dbObject.put(MappingMongoConverter.CUSTOM_TYPE_KEY, Person.class.getName());
assertThat(converter.read(BirthDateContainer.class, dbObject), is(BirthDateContainer.class));
}
@Test
public void writesTypeDiscriminatorIntoRootObject() {
Person person = new Person();
person.birthDate = new LocalDate();
DBObject result = new BasicDBObject();
converter.write(person, result);
assertThat(result.containsField(MappingMongoConverter.CUSTOM_TYPE_KEY), is(true));
assertThat(result.get(MappingMongoConverter.CUSTOM_TYPE_KEY).toString(), is(Person.class.getName()));
}
public static class Address {
String street;
String city;
}
interface Contact {
}
public static class Person {
public static class Person implements Contact {
LocalDate birthDate;
}
public static class BirthDateContainer {
LocalDate birthDate;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2011 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.document.mongodb.repository;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.document.mongodb.mapping.Document;
/**
* Sample contactt domain class.
*
* @author Oliver Gierke
*/
@Document
abstract class Contact {
@Id
protected final String id;
public Contact() {
this.id = new ObjectId().toString();
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2010-2011 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.document.mongodb.repository;
/**
* Simple repository interface managing {@link Contact}s.
*
* @author Oliver Gierke
*/
public interface ContactRepository extends MongoRepository<Contact, String> {
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2010-2011 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.document.mongodb.repository;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Integration tests for {@link ContactRepository}. Mostly related to mapping inheritance.
*
* @author Oliver Gierke
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("config/MongoNamespaceIntegrationTests-context.xml")
public class ContactRepositoryIntegrationTests {
@Autowired
ContactRepository repository;
@Test
public void readsAndWritesContactCorrectly() {
Person person = new Person("Oliver", "Gierke");
Contact result = repository.save(person);
assertTrue(repository.findOne(result.getId()) instanceof Person);
}
}

View File

@@ -17,8 +17,6 @@ package org.springframework.data.document.mongodb.repository;
import java.util.Set;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.document.mongodb.geo.Point;
import org.springframework.data.document.mongodb.index.GeoSpatialIndexed;
import org.springframework.data.document.mongodb.mapping.Document;
@@ -30,10 +28,8 @@ import org.springframework.data.document.mongodb.mapping.Document;
* @author Oliver Gierke
*/
@Document
public class Person {
public class Person extends Contact {
@Id
private String id;
private String firstname;
private String lastname;
private Integer age;
@@ -59,31 +55,12 @@ public class Person {
public Person(String firstname, String lastname, Integer age) {
this.id = new ObjectId().toString();
super();
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}
/**
* @return the id
*/
public String getId() {
return id;
}
/**
* @return the firstname
*/
@@ -182,6 +159,13 @@ public class Person {
this.shippingAddresses = addresses;
}
/* (non-Javadoc)
* @see org.springframework.data.document.mongodb.repository.Contact#getName()
*/
public String getName() {
return String.format("%s %s", firstname, lastname);
}
/*
* (non-Javadoc)
@@ -201,7 +185,7 @@ public class Person {
Person that = (Person) obj;
return this.id.equals(that.id);
return this.getId().equals(that.getId());
}
@@ -213,7 +197,7 @@ public class Person {
@Override
public int hashCode() {
return id.hashCode();
return getId().hashCode();
}
/* (non-Javadoc)