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:
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user