diff --git a/spring-data-mongodb-parent/pom.xml b/spring-data-mongodb-parent/pom.xml index 56c556b58..3261040c1 100644 --- a/spring-data-mongodb-parent/pom.xml +++ b/spring-data-mongodb-parent/pom.xml @@ -187,6 +187,9 @@ **/*Tests.java + + **/PerformanceTests.java + junit:junit diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 48342b594..6400d7567 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -148,6 +148,29 @@ + + + performance-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.8 + + + **/PerformanceTests.java + + + none + + + + + + + + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 0bdbf9364..3bc7876d9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -720,20 +720,24 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Assert.notNull(targetType); + if (sourceValue.isEmpty()) { + return Collections.emptySet(); + } + Class collectionType = targetType.getType(); collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class; Collection items = targetType.getType().isArray() ? new ArrayList() : CollectionFactory .createCollection(collectionType, sourceValue.size()); + TypeInformation componentType = targetType.getComponentType(); for (int i = 0; i < sourceValue.size(); i++) { Object dbObjItem = sourceValue.get(i); if (dbObjItem instanceof DBRef) { - items.add(read(targetType.getComponentType(), ((DBRef) dbObjItem).fetch(), parent)); + items.add(read(componentType, ((DBRef) dbObjItem).fetch(), parent)); } else if (dbObjItem instanceof DBObject) { - items.add(read(targetType.getComponentType(), (DBObject) dbObjItem, parent)); + items.add(read(componentType, (DBObject) dbObjItem, parent)); } else { - TypeInformation componentType = targetType.getComponentType(); items.add(getPotentiallyConvertedSimpleRead(dbObjItem, componentType == null ? null : componentType.getType())); } } @@ -923,24 +927,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App public T getPropertyValue(MongoPersistentProperty property) { String expression = property.getSpelExpression(); - TypeInformation type = property.getTypeInformation(); Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName()); if (value == null) { return null; } - if (conversions.hasCustomReadTarget(value.getClass(), type.getType())) { - return (T) conversionService.convert(value, type.getType()); + TypeInformation type = property.getTypeInformation(); + Class rawType = type.getType(); + + if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { + return (T) conversionService.convert(value, rawType); } else if (value instanceof DBRef) { return (T) read(type, ((DBRef) value).fetch(), parent); } else if (value instanceof BasicDBList) { - return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value, parent), - type.getType()); + return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value, parent), rawType); } else if (value instanceof DBObject) { return (T) read(type, (DBObject) value, parent); } else { - return (T) getPotentiallyConvertedSimpleRead(value, type.getType()); + return (T) getPotentiallyConvertedSimpleRead(value, rawType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java index b8d831e4c..2e83dd984 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java @@ -28,6 +28,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty { private Boolean isIdProperty; + private Boolean isAssociation; private String fieldName; /** @@ -57,6 +58,18 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty return this.isIdProperty; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#isAssociation() + */ + @Override + public boolean isAssociation() { + if (this.isAssociation == null) { + this.isAssociation = super.isAssociation(); + } + return this.isAssociation; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#getFieldName() diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java new file mode 100644 index 000000000..28804dd7e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java @@ -0,0 +1,589 @@ +/* + * Copyright 2012 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.performance; + +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.regex.Pattern; + +import org.bson.types.ObjectId; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.Constants; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean; +import org.springframework.util.Assert; +import org.springframework.util.StopWatch; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.Mongo; +import com.mongodb.WriteConcern; + +/** + * Test class to execute performance tests for plain MongoDB driver usage, {@link MongoTemplate} and the repositories + * abstraction. + * + * @author Oliver Gierke + */ +public class PerformanceTests { + + private static final String DATABASE_NAME = "performance"; + private static final int NUMBER_OF_PERSONS = 30000; + private static final StopWatch watch = new StopWatch(); + private static final Collection IGNORED_WRITE_CONCERNS = Arrays.asList("MAJORITY", "REPLICAS_SAFE", + "FSYNC_SAFE", "JOURNAL_SAFE"); + private static final int COLLECTION_SIZE = 1024 * 1024 * 256; // 256 MB + private static final Collection COLLECTION_NAMES = Arrays.asList("template", "driver", "person"); + + Mongo mongo; + MongoTemplate operations; + PersonRepository repository; + + @Before + public void setUp() throws Exception { + + this.mongo = new Mongo(); + this.operations = new MongoTemplate(new SimpleMongoDbFactory(this.mongo, DATABASE_NAME)); + + MongoRepositoryFactoryBean factory = new MongoRepositoryFactoryBean(); + factory.setMongoOperations(operations); + factory.setRepositoryInterface(PersonRepository.class); + factory.afterPropertiesSet(); + + repository = factory.getObject(); + } + + @Test + public void writeWithWriteConcerns() { + executeWithWriteConcerns(new WriteConcernCallback() { + public void doWithWriteConcern(String constantName, WriteConcern concern) { + writeHeadline("WriteConcern: " + constantName); + writingObjectsUsingPlainDriver("Writing %s objects using plain driver"); + writingObjectsUsingMongoTemplate("Writing %s objects using template"); + writingObjectsUsingRepositories("Writing %s objects using repository"); + writeFooter(); + } + }); + } + + @Test + public void writeAndRead() { + + mongo.setWriteConcern(WriteConcern.SAFE); + + for (int i = 3; i > 0; i--) { + + setupCollections(); + + writeHeadline("Plain driver"); + writingObjectsUsingPlainDriver("Writing %s objects using plain driver"); + readingUsingPlainDriver("Reading all objects using plain driver"); + queryUsingPlainDriver("Executing query using plain driver"); + writeFooter(); + + writeHeadline("Template"); + writingObjectsUsingMongoTemplate("Writing %s objects using template"); + readingUsingTemplate("Reading all objects using template"); + queryUsingTemplate("Executing query using template"); + writeFooter(); + + writeHeadline("Repositories"); + writingObjectsUsingRepositories("Writing %s objects using repository"); + readingUsingRepository("Reading all objects using repository"); + queryUsingRepository("Executing query using repository"); + writeFooter(); + + writeFooter(); + } + } + + private void writeHeadline(String headline) { + System.out.println(headline); + System.out.println("---------------------------------".substring(0, headline.length())); + } + + private void writeFooter() { + System.out.println(); + } + + private void queryUsingTemplate(String template) { + executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + public List doInWatch() { + Query query = query(where("addresses.zipCode").regex(".*1.*")); + return operations.find(query, Person.class, "template"); + } + }); + } + + private void queryUsingRepository(String template) { + executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + public List doInWatch() { + return repository.findByAddressesZipCodeContaining("1"); + } + }); + } + + private void executeWithWriteConcerns(WriteConcernCallback callback) { + + Constants constants = new Constants(WriteConcern.class); + + for (String constantName : constants.getNames(null)) { + + if (IGNORED_WRITE_CONCERNS.contains(constantName)) { + continue; + } + + WriteConcern writeConcern = (WriteConcern) constants.asObject(constantName); + mongo.setWriteConcern(writeConcern); + + setupCollections(); + + callback.doWithWriteConcern(constantName, writeConcern); + } + } + + private void setupCollections() { + + DB db = this.mongo.getDB(DATABASE_NAME); + + for (String collectionName : COLLECTION_NAMES) { + DBCollection collection = db.getCollection(collectionName); + collection.drop(); + db.command(getCreateCollectionCommand(collectionName)); + collection.ensureIndex(new BasicDBObject("firstname", -1)); + collection.ensureIndex(new BasicDBObject("lastname", -1)); + } + } + + private DBObject getCreateCollectionCommand(String name) { + BasicDBObject dbObject = new BasicDBObject(); + dbObject.put("createCollection", name); + dbObject.put("capped", false); + dbObject.put("size", COLLECTION_SIZE); + return dbObject; + } + + private void writingObjectsUsingPlainDriver(String template) { + + final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); + final List persons = getPersonDBObjects(); + + executeWatchedWithTime(template, new WatchCallback() { + public Void doInWatch() { + for (DBObject person : persons) { + collection.save(person); + } + return null; + } + }); + } + + private void writingObjectsUsingRepositories(String template) { + + final List persons = getPersonObjects(); + + executeWatchedWithTime(template, new WatchCallback() { + public Void doInWatch() { + repository.save(persons); + return null; + } + }); + } + + private void writingObjectsUsingMongoTemplate(String template) { + + final List persons = getPersonObjects(); + + executeWatchedWithTime(template, new WatchCallback() { + public Void doInWatch() { + for (Person person : persons) { + operations.save(person, "template"); + } + return null; + } + }); + } + + private void readingUsingPlainDriver(String template) { + + final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); + + executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + public List doInWatch() { + return toPersons(collection.find()); + } + }); + } + + private void readingUsingTemplate(String template) { + executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + public List doInWatch() { + return operations.findAll(Person.class, "template"); + } + }); + } + + private void readingUsingRepository(String template) { + executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + public List doInWatch() { + return repository.findAll(); + } + }); + } + + private void queryUsingPlainDriver(String template) { + + final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); + + executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + public List doInWatch() { + DBObject regex = new BasicDBObject("$regex", Pattern.compile(".*1.*")); + DBObject query = new BasicDBObject("addresses.zipCode", regex); + return toPersons(collection.find(query)); + } + }); + } + + private List getPersonDBObjects() { + + List result = new ArrayList(NUMBER_OF_PERSONS); + + for (Person person : getPersonObjects()) { + result.add(person.toDBObject()); + } + + return result; + } + + private List getPersonObjects() { + + List result = new ArrayList(NUMBER_OF_PERSONS); + + watch.start("Created " + NUMBER_OF_PERSONS + " Persons"); + + for (int i = 0; i < NUMBER_OF_PERSONS; i++) { + + Address address = new Address("zip" + i, "city" + i); + Person person = new Person("Firstname" + i, "Lastname" + i, Arrays.asList(address)); + person.orders.add(new Order(LineItem.generate())); + person.orders.add(new Order(LineItem.generate())); + result.add(person); + } + + watch.stop(); + + return result; + } + + private T executeWatched(String template, WatchCallback callback) { + + watch.start(String.format(template, NUMBER_OF_PERSONS)); + + try { + return callback.doInWatch(); + } finally { + watch.stop(); + } + } + + private void executeWatchedWithTime(String template, WatchCallback callback) { + executeWatched(template, callback); + printStatistics(null); + } + + private void executeWatchedWithTimeAndResultSize(String template, WatchCallback> callback) { + printStatistics(executeWatched(template, callback)); + } + + private void printStatistics(Collection result) { + + long time = watch.getLastTaskTimeMillis(); + StringBuilder builder = new StringBuilder(watch.getLastTaskName()); + + if (result != null) { + builder.append(" returned ").append(result.size()).append(" results and"); + } + + builder.append(" took ").append(time).append(" milliseconds"); + System.out.println(builder); + } + + private static List toPersons(DBCursor cursor) { + + List persons = new ArrayList(); + + while (cursor.hasNext()) { + persons.add(Person.from(cursor.next())); + } + + return persons; + } + + static class Person { + + ObjectId id; + @Indexed + final String firstname, lastname; + final List
addresses; + final Set orders; + + public Person(String firstname, String lastname, List
addresses) { + this.firstname = firstname; + this.lastname = lastname; + this.addresses = addresses; + this.orders = new HashSet(); + } + + public static Person from(DBObject source) { + + BasicDBList addressesSource = (BasicDBList) source.get("addresses"); + List
addresses = new ArrayList
(addressesSource.size()); + for (Object addressSource : addressesSource) { + addresses.add(Address.from((DBObject) addressSource)); + } + + BasicDBList ordersSource = (BasicDBList) source.get("orders"); + Set orders = new HashSet(ordersSource.size()); + for (Object orderSource : ordersSource) { + orders.add(Order.from((DBObject) orderSource)); + } + + Person person = new Person((String) source.get("firstname"), (String) source.get("lastname"), addresses); + person.orders.addAll(orders); + return person; + } + + public DBObject toDBObject() { + + DBObject dbObject = new BasicDBObject(); + dbObject.put("firstname", firstname); + dbObject.put("lastname", lastname); + dbObject.put("addresses", writeAll(addresses)); + dbObject.put("orders", writeAll(orders)); + return dbObject; + } + } + + static class Address implements Convertible { + + final String zipCode; + final String city; + final Set types; + + public Address(String zipCode, String city) { + this(zipCode, city, new HashSet(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values())))); + } + + @PersistenceConstructor + public Address(String zipCode, String city, Set types) { + this.zipCode = zipCode; + this.city = city; + this.types = types; + } + + public static Address from(DBObject source) { + String zipCode = (String) source.get("zipCode"); + String city = (String) source.get("city"); + BasicDBList types = (BasicDBList) source.get("types"); + + return new Address(zipCode, city, new HashSet(readFromBasicDBList(types, AddressType.class))); + } + + public DBObject toDBObject() { + BasicDBObject dbObject = new BasicDBObject(); + dbObject.put("zipCode", zipCode); + dbObject.put("city", city); + dbObject.put("types", toBasicDBList(types)); + return dbObject; + } + } + + private static > List readFromBasicDBList(BasicDBList source, Class type) { + + List result = new ArrayList(source.size()); + for (Object object : source) { + result.add(Enum.valueOf(type, object.toString())); + } + return result; + } + + private static > BasicDBList toBasicDBList(Collection enums) { + BasicDBList result = new BasicDBList(); + for (T element : enums) { + result.add(element.toString()); + } + + return result; + } + + static class Order implements Convertible { + + static enum Status { + ORDERED, PAYED, SHIPPED; + } + + Date createdAt; + List lineItems; + Status status; + + public Order(List lineItems, Date createdAt) { + this.lineItems = lineItems; + this.createdAt = createdAt; + this.status = Status.ORDERED; + } + + @PersistenceConstructor + public Order(List lineItems, Date createdAt, Status status) { + this.lineItems = lineItems; + this.createdAt = createdAt; + this.status = status; + } + + public static Order from(DBObject source) { + + BasicDBList lineItemsSource = (BasicDBList) source.get("lineItems"); + List lineItems = new ArrayList(lineItemsSource.size()); + for (Object lineItemSource : lineItemsSource) { + lineItems.add(LineItem.from((DBObject) lineItemSource)); + } + + Date date = (Date) source.get("createdAt"); + Status status = Status.valueOf((String) source.get("status")); + return new Order(lineItems, date, status); + } + + public Order(List lineItems) { + this(lineItems, new Date()); + } + + public DBObject toDBObject() { + DBObject result = new BasicDBObject(); + result.put("createdAt", createdAt); + result.put("lineItems", writeAll(lineItems)); + result.put("status", status.toString()); + return result; + } + } + + static class LineItem implements Convertible { + + String description; + double price; + int amount; + + public LineItem(String description, int amount, double price) { + this.description = description; + this.amount = amount; + this.price = price; + } + + public static List generate() { + + LineItem iPad = new LineItem("iPad", 1, 649); + LineItem iPhone = new LineItem("iPhone", 1, 499); + LineItem macBook = new LineItem("MacBook", 2, 1299); + + return pickRandomNumerOfItemsFrom(Arrays.asList(iPad, iPhone, macBook)); + } + + public static LineItem from(DBObject source) { + + String description = (String) source.get("description"); + double price = (Double) source.get("price"); + int amount = (Integer) source.get("amount"); + + return new LineItem(description, amount, price); + } + + public DBObject toDBObject() { + + BasicDBObject dbObject = new BasicDBObject(); + dbObject.put("description", description); + dbObject.put("price", price); + dbObject.put("amount", amount); + return dbObject; + } + } + + private static List pickRandomNumerOfItemsFrom(List source) { + + Assert.isTrue(!source.isEmpty()); + + Random random = new Random(); + int numberOfItems = random.nextInt(source.size()); + numberOfItems = numberOfItems == 0 ? 1 : numberOfItems; + + List result = new ArrayList(numberOfItems); + while (result.size() < numberOfItems) { + int index = random.nextInt(source.size()); + T candidate = source.get(index); + if (!result.contains(candidate)) { + result.add(candidate); + } + } + + return result; + } + + static enum AddressType { + SHIPPING, BILLING; + } + + private interface WriteConcernCallback { + void doWithWriteConcern(String constantName, WriteConcern concern); + } + + private interface WatchCallback { + T doInWatch(); + } + + private interface PersonRepository extends MongoRepository { + + List findByAddressesZipCodeContaining(String parameter); + } + + private interface Convertible { + + DBObject toDBObject(); + } + + private static List writeAll(Collection convertibles) { + List result = new ArrayList(); + for (Convertible convertible : convertibles) { + result.add(convertible.toDBObject()); + } + return result; + } +}