From 12ddfcc9f94cf8226f5f4fc863d33f9e18f872b2 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 10 May 2011 18:21:47 +0200 Subject: [PATCH] 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. --- .../convert/MappingMongoConverter.java | 97 ++++++++++++++----- .../MappingMongoConverterUnitTests.java | 50 +++++++++- .../document/mongodb/repository/Contact.java | 40 ++++++++ .../mongodb/repository/ContactRepository.java | 25 +++++ .../ContactRepositoryIntegrationTests.java | 46 +++++++++ .../document/mongodb/repository/Person.java | 38 +++----- 6 files changed, 242 insertions(+), 54 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Contact.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepository.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepositoryIntegrationTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java index 68bd0e0e5..8d49acf7b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java @@ -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> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class, DBObject.class); private static final List> 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 rawType = type.getType(); + + TypeInformation typeToUse = getMoreConcreteTargetType(dbo, type); + Class 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 l = new ArrayList(); 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 persistentEntity = (MongoPersistentEntity) mappingContext.getPersistentEntity(type); + MongoPersistentEntity persistentEntity = (MongoPersistentEntity) 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) 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) 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 TypeInformation getMoreConcreteTargetType(DBObject dbObject, TypeInformation basicType) { + Class documentsTargetType = findTypeToBeUsed(dbObject); + Class rawType = basicType.getType(); + boolean isMoreConcreteCustomType = documentsTargetType != null && rawType.isAssignableFrom(documentsTargetType); + return isMoreConcreteCustomType ? (TypeInformation) ClassTypeInformation.from(documentsTargetType) + : basicType; + } + protected List unwrapList(BasicDBList dbList, TypeInformation targetType) { List rootList = new LinkedList(); for (int i = 0; i < dbList.size(); i++) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java index 88443700f..bdddc357b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java @@ -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; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Contact.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Contact.java new file mode 100644 index 000000000..a644fa5e3 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Contact.java @@ -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; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepository.java new file mode 100644 index 000000000..f5e1b5ec1 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepository.java @@ -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 { + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepositoryIntegrationTests.java new file mode 100644 index 000000000..099d972bb --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/ContactRepositoryIntegrationTests.java @@ -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); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Person.java index 427246b6e..4f9cd5e05 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/repository/Person.java @@ -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)