diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index d30c6504f..189d95da9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -28,8 +28,11 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.geo.GeoResult; import org.springframework.data.mongodb.core.geo.GeoResults; import org.springframework.data.mongodb.core.index.IndexDefinition; +import org.springframework.data.mongodb.core.mapreduce.GroupBy; +import org.springframework.data.mongodb.core.mapreduce.GroupByResults; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; @@ -267,7 +270,31 @@ public interface MongoOperations { List findAll(Class entityClass, String collectionName); + /** + * Execute a group operation over the entire collection. + * The group operation entity class should match the 'shape' of the returned object that takes int account the + * initial document structure as well as any finalize functions. + * + * @param criteria The criteria that restricts the row that are considered for grouping. If not specified all rows are considered. + * @param inputCollectionName the collection where the group operation will read from + * @param groupBy the conditions under which the group operation will be performed, e.g. keys, initial document, reduce function. + * @param entityClass The parameterized type of the returned list + * @return The results of the group operation + */ + GroupByResults group(String inputCollectionName, GroupBy groupBy, Class entityClass); + /** + * Execute a group operation restricting the rows to those which match the provided Criteria. + * The group operation entity class should match the 'shape' of the returned object that takes int account the + * initial document structure as well as any finalize functions. + * + * @param criteria The criteria that restricts the row that are considered for grouping. If not specified all rows are considered. + * @param inputCollectionName the collection where the group operation will read from + * @param groupBy the conditions under which the group operation will be performed, e.g. keys, initial document, reduce function. + * @param entityClass The parameterized type of the returned list + * @return The results of the group operation + */ + GroupByResults group(Criteria criteria, String inputCollectionName, GroupBy groupBy, Class entityClass); /** * Execute a map-reduce operation. The map-reduce operation will be formed with an output type of INLINE * @param inputCollectionName the collection where the map-reduce will read from diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index d5dec787a..43d5662d4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -84,8 +84,11 @@ import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; +import org.springframework.data.mongodb.core.mapreduce.GroupBy; +import org.springframework.data.mongodb.core.mapreduce.GroupByResults; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; @@ -960,6 +963,75 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { MapReduceResults mapReduceResult = new MapReduceResults(mappedResults, commandResult); return mapReduceResult; + } + + public GroupByResults group(String inputCollectionName, GroupBy groupBy, Class entityClass) { + return group(null, inputCollectionName, groupBy, entityClass); + } + + public GroupByResults group(Criteria criteria, String inputCollectionName, GroupBy groupBy, Class entityClass) { + + DBObject dbo = groupBy.getGroupByObject(); + dbo.put("ns", inputCollectionName); + + if (criteria == null) { + dbo.put("cond", null); + } else { + dbo.put("cond", criteria.getCriteriaObject()); + } + //If initial document was a JavaScript string, potentially loaded by Spring's Resource abstraction, load it and convert to DBObject + + if (dbo.containsField("initial")) { + Object initialObj = dbo.get("initial"); + if (initialObj instanceof String) { + String initialAsString = replaceWithResourceIfNecessary((String)initialObj); + dbo.put("initial", JSON.parse(initialAsString)); + } + } + + if (dbo.containsField("$reduce")) { + dbo.put("$reduce", replaceWithResourceIfNecessary(dbo.get("$reduce").toString())); + } + if (dbo.containsField("$keyf")) { + dbo.put("$keyf", replaceWithResourceIfNecessary(dbo.get("$keyf").toString())); + } + if (dbo.containsField("finalize")) { + dbo.put("finalize", replaceWithResourceIfNecessary(dbo.get("finalize").toString())); + } + + DBObject commandObject = new BasicDBObject("group", dbo); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Executing Group with DBObject [" + commandObject.toString() + "]"); + } + CommandResult commandResult = null; + try { + commandResult = executeCommand(commandObject, getDb().getOptions()); + commandResult.throwOnError(); + } catch (RuntimeException ex) { + this.potentiallyConvertRuntimeException(ex); + } + String error = commandResult.getErrorMessage(); + if (error != null) { + throw new InvalidDataAccessApiUsageException("Command execution failed: Error [" + error + "], Command = " + + commandObject); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Group command result = [" + commandResult + "]"); + } + + Iterable resultSet = (Iterable) commandResult.get( "retval" ); + + List mappedResults = new ArrayList(); + DbObjectCallback callback = new ReadDbObjectCallback(mongoConverter, entityClass); + for (DBObject dbObject : resultSet) { + mappedResults.add(callback.doWith(dbObject)); + } + GroupByResults groupByResult = new GroupByResults(mappedResults, commandResult); + return groupByResult; + + } protected String replaceWithResourceIfNecessary(String function) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java new file mode 100644 index 000000000..7924c3401 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java @@ -0,0 +1,118 @@ +/* + * 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.mongodb.core.mapreduce; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Collects the parameters required to perform a group operation on a collection. The query condition and the input collection are specified on the group method as method arguments + * to be consistent with other operations, e.g. map-reduce. + * + * @author Mark Pollack + * + */ +public class GroupBy { + + private DBObject dboKeys; + + private String keyFunction; + + private String initial; + + private DBObject initialDbObject; + + private String reduce; + + private String finalize; + + public GroupBy(String... keys) { + DBObject dbo = new BasicDBObject(); + for (String key : keys) { + dbo.put(key, 1); + } + dboKeys = dbo; + } + + // NOTE GroupByCommand does not handle keyfunction. + + public GroupBy(String key, boolean isKeyFunction) { + DBObject dbo = new BasicDBObject(); + if (isKeyFunction) { + keyFunction = key; + } else { + dbo.put(key, 1); + dboKeys = dbo; + } + } + + public static GroupBy keyFunction(String key) { + return new GroupBy(key, true); + } + + public static GroupBy key(String... keys) { + return new GroupBy(keys); + } + + public GroupBy initialDocument(String initialDocument) { + initial = initialDocument; + return this; + } + + public GroupBy initialDocument(DBObject initialDocument) { + initialDbObject = initialDocument; + return this; + } + + public GroupBy reduceFunction(String reduceFunction) { + reduce = reduceFunction; + return this; + } + + public GroupBy finalizeFunction(String finalizeFunction) { + finalize = finalizeFunction; + return this; + } + + + + public DBObject getGroupByObject() { + // return new GroupCommand(dbCollection, dboKeys, condition, initial, reduce, finalize); + BasicDBObject dbo = new BasicDBObject(); + if (dboKeys != null) { + dbo.put("key", dboKeys); + } + if (keyFunction != null) { + dbo.put("$keyf", keyFunction); + } + + dbo.put("$reduce", reduce); + + dbo.put("initial", initialDbObject); + if (initial != null) { + dbo.put("initial", initial); + } + if (finalize != null) { + dbo.put("finalize", finalize); + } + return dbo; + } + + + + + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java new file mode 100644 index 000000000..577f79133 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java @@ -0,0 +1,82 @@ +/* + * 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.mongodb.core.mapreduce; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * Collects the results of executing a group operation. + * + * @author Mark Pollack + * + * @param The class in which the results are mapped onto, accessible via an interator. + */ +public class GroupByResults implements Iterable { + + private final List mappedResults; + + private DBObject rawResults; + + private double count; + + private int keys; + + public GroupByResults(List mappedResults, DBObject rawResults) { + Assert.notNull(mappedResults); + Assert.notNull(rawResults); + this.mappedResults = mappedResults; + this.rawResults = rawResults; + parseKeys(); + parseCount(); + } + + public double getCount() { + return count; + } + + public int getKeys() { + return keys; + } + + public Iterator iterator() { + return mappedResults.iterator(); + } + + public DBObject getRawResults() { + return rawResults; + } + + private void parseCount() { + Object object = (Object) rawResults.get("count"); + if (object instanceof Double) { + count = (Double)object; + } + + } + + private void parseKeys() { + Object object = (Object) rawResults.get("keys"); + if (object instanceof Integer) { + keys = (Integer)object; + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java index 0c2cd1459..4d2f4d12c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java @@ -22,6 +22,12 @@ import org.springframework.util.Assert; import com.mongodb.DBObject; +/** + * Collects the results of performing a MapReduce operations. + * @author Mark Pollack + * + * @param The class in which the results are mapped onto, accessible via an interator. + */ public class MapReduceResults implements Iterable { private final List mappedResults; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java new file mode 100644 index 000000000..9be6a8a1d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java @@ -0,0 +1,196 @@ +/* + * 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.mongodb.core.mapreduce; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.Mongo; + +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.mapreduce.GroupBy.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:infrastructure.xml") +public class GroupByTests { + + @Autowired + MongoDbFactory factory; + + @Autowired + ApplicationContext applicationContext; + + //@Autowired + //MongoTemplate mongoTemplate; + + + MongoTemplate mongoTemplate; + + @Autowired + @SuppressWarnings("unchecked") + public void setMongo(Mongo mongo) throws Exception { + + + MongoMappingContext mappingContext = new MongoMappingContext(); + mappingContext.setInitialEntitySet(new HashSet>(Arrays.asList(XObject.class))); + mappingContext.afterPropertiesSet(); + + MappingMongoConverter mappingConverter = new MappingMongoConverter(factory, mappingContext); + mappingConverter.afterPropertiesSet(); + this.mongoTemplate = new MongoTemplate(factory, mappingConverter); + mongoTemplate.setApplicationContext(applicationContext); + + } + + + @Before + public void setUp() { + cleanDb(); + } + + @After + public void cleanUp() { + cleanDb(); + } + + protected void cleanDb() { + mongoTemplate.dropCollection(mongoTemplate.getCollectionName(XObject.class)); + mongoTemplate.dropCollection("group_test_collection"); + } + + @Test + public void singleKeyCreation() { + DBObject gc = new GroupBy("a").getGroupByObject(); + //String expected = "{ \"group\" : { \"ns\" : \"test\" , \"key\" : { \"a\" : 1} , \"cond\" : null , \"$reduce\" : null , \"initial\" : null }}"; + String expected = "{ \"key\" : { \"a\" : 1} , \"$reduce\" : null , \"initial\" : null }"; + Assert.assertEquals(expected, gc.toString()); + } + + @Test + public void multipleKeyCreation() { + DBObject gc = GroupBy.key("a","b").getGroupByObject(); + //String expected = "{ \"group\" : { \"ns\" : \"test\" , \"key\" : { \"a\" : 1 , \"b\" : 1} , \"cond\" : null , \"$reduce\" : null , \"initial\" : null }}"; + String expected = "{ \"key\" : { \"a\" : 1 , \"b\" : 1} , \"$reduce\" : null , \"initial\" : null }"; + Assert.assertEquals(expected, gc.toString()); + } + + @Test + public void keyFunctionCreation() { + DBObject gc = GroupBy.keyFunction("classpath:keyFunction.js").getGroupByObject(); + String expected = "{ \"$keyf\" : \"classpath:keyFunction.js\" , \"$reduce\" : null , \"initial\" : null }"; + Assert.assertEquals(expected, gc.toString()); + } + + @Test + public void SimpleGroup() { + createGroupByData(); + GroupByResults results; + + results = mongoTemplate.group("group_test_collection", + GroupBy.key("x").initialDocument(new BasicDBObject("count", 0)).reduceFunction("function(doc, prev) { prev.count += 1 }"), XObject.class); + + assertMapReduceResults(results); + + } + + @Test + public void SimpleGroupWithKeyFunction() { + createGroupByData(); + GroupByResults results; + + results = mongoTemplate.group("group_test_collection", + GroupBy.keyFunction("function(doc) { return { x : doc.x }; }").initialDocument("{ count: 0 }").reduceFunction("function(doc, prev) { prev.count += 1 }"), XObject.class); + + assertMapReduceResults(results); + } + + @Test + public void SimpleGroupWithFunctionsAsResources() { + createGroupByData(); + GroupByResults results; + + results = mongoTemplate.group("group_test_collection", + GroupBy.keyFunction("classpath:keyFunction.js").initialDocument("{ count: 0 }").reduceFunction("classpath:groupReduce.js"), XObject.class); + + assertMapReduceResults(results); + } + + + @Test + public void SimpleGroupWithQueryAndFunctionsAsResources() { + createGroupByData(); + GroupByResults results; + + results = mongoTemplate.group(where("x").gt(0), + "group_test_collection", + keyFunction("classpath:keyFunction.js").initialDocument("{ count: 0 }").reduceFunction("classpath:groupReduce.js"), XObject.class); + + assertMapReduceResults(results); + } + + + private void assertMapReduceResults(GroupByResults results) { + DBObject dboRawResults = results.getRawResults(); + String expected = "{ \"retval\" : [ { \"x\" : 1.0 , \"count\" : 2.0} , { \"x\" : 2.0 , \"count\" : 1.0} , { \"x\" : 3.0 , \"count\" : 3.0}] , \"count\" : 6.0 , \"keys\" : 3 , \"ok\" : 1.0}"; + Assert.assertEquals(expected, dboRawResults.toString()); + + int numResults = 0; + for (XObject xObject : results) { + if (xObject.getX() == 1) { + Assert.assertEquals(2, xObject.getCount(), 0.001); + } + if (xObject.getX() == 2) { + Assert.assertEquals(1, xObject.getCount(), 0.001); + } + if (xObject.getX() == 3) { + Assert.assertEquals(3, xObject.getCount(), 0.001); + } + numResults++; + } + Assert.assertEquals(3, numResults); + Assert.assertEquals(6, results.getCount(), 0.001); + Assert.assertEquals(3, results.getKeys()); + } + + + private void createGroupByData() { + DBCollection c = mongoTemplate.getDb().getCollection("group_test_collection"); + c.save(new BasicDBObject("x", 1)); + c.save(new BasicDBObject("x", 1)); + c.save(new BasicDBObject("x", 2)); + c.save(new BasicDBObject("x", 3)); + c.save(new BasicDBObject("x", 3)); + c.save(new BasicDBObject("x", 3)); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java index 01f17f12b..9d139a9aa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java @@ -60,6 +60,7 @@ public class MapReduceTests { MongoTemplate template; @Autowired MongoDbFactory factory; + MongoTemplate mongoTemplate; @Autowired diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/XObject.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/XObject.java new file mode 100644 index 000000000..61b1138ab --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/XObject.java @@ -0,0 +1,35 @@ +package org.springframework.data.mongodb.core.mapreduce; + +public class XObject { + + private float x; + + private float count; + + + public float getX() { + return x; + } + + + public void setX(float x) { + this.x = x; + } + + + public float getCount() { + return count; + } + + + public void setCount(float count) { + this.count = count; + } + + + @Override + public String toString() { + return "XObject [x=" + x + " count = " + count + "]"; + } + +} diff --git a/spring-data-mongodb/src/test/resources/groupReduce.js b/spring-data-mongodb/src/test/resources/groupReduce.js new file mode 100644 index 000000000..4c08bc87d --- /dev/null +++ b/spring-data-mongodb/src/test/resources/groupReduce.js @@ -0,0 +1 @@ +function(doc, prev) { prev.count += 1 } \ No newline at end of file diff --git a/spring-data-mongodb/src/test/resources/keyFunction.js b/spring-data-mongodb/src/test/resources/keyFunction.js new file mode 100644 index 000000000..3acc63159 --- /dev/null +++ b/spring-data-mongodb/src/test/resources/keyFunction.js @@ -0,0 +1 @@ +function(doc) { return { x : doc.x }; } \ No newline at end of file diff --git a/src/docbkx/reference/mongodb.xml b/src/docbkx/reference/mongodb.xml index 6298aa4c3..02a702f50 100644 --- a/src/docbkx/reference/mongodb.xml +++ b/src/docbkx/reference/mongodb.xml @@ -1451,8 +1451,8 @@ import static org.springframework.data.mongodb.core.query.Query.query; - Criteria andOperator - (Criteria... + Criteria andOperator (Criteria... criteria)Creates an and query using the $and operator for all of the provided criteria (requires MongoDB 2.0 or later) @@ -1535,8 +1535,8 @@ import static org.springframework.data.mongodb.core.query.Query.query; - Criteria norOperator - (Criteria... + Criteria norOperator (Criteria... criteria)Creates an nor query using the $nor operator for all of the provided criteria @@ -1550,8 +1550,8 @@ import static org.springframework.data.mongodb.core.query.Query.query; - Criteria orOperator - (Criteria... + Criteria orOperator (Criteria... criteria)Creates an or query using the $or operator for all of the provided criteria @@ -1847,22 +1847,24 @@ GeoResults<Restaurant> = operations.geoNear(query, Restaurant.class);
- Map-Reduce + Map-Reduce Operations You can query MongoDB using Map-Reduce which is useful for batch processing, data aggregation, and for when the query language doesn't - fulfill your needs. Spring provides integration with MongoDB's map reduce - by providing methods on MongoOperations to simplify the creation and - execution of Map-Reduce operations. It also integrates with Spring's - + + Spring provides integration with MongoDB's map reduce by providing + methods on MongoOperations to simplify the creation and execution of + Map-Reduce operations. It can convert the results of a Map-Reduce + operation to a POJO also integrates with Spring's Resource abstraction abstraction. This will let you place your JavaScript files on the file system, classpath, http server or any other Spring Resource implementation and then reference the JavaScript resources via an easy URI style syntax, e.g. 'classpath:reduce.js;. Externalizing - JavaScript code in files is preferable to embedding them as Java strings - in your code. You can still pass JavaScript code as Java strings if you - prefer. + JavaScript code in files is often preferable to embedding them as Java + strings in your code. Note that you can still pass JavaScript code as Java + strings if you prefer.
Example Usage @@ -1967,6 +1969,138 @@ MapReduceResults<ValueObject> results = mongoOperations.mapReduce(query, "
+
+ Group Operations + + As an alternative to usiing Map-Reduce to perform data aggregation, + you can use the group + operation which feels similar to using SQL's group by query style, + so it may feel more approachable vs. using Map-Reduce. Using the group + operations does have some limitations, for example it is not supported in + a shareded environment and it returns the full result set in a single BSON + object, so the result should be small, less than 10,000 keys. + + Spring provides integration with MongoDB's group operation by + providing methods on MongoOperations to simplify the creation and + execution of group operations. It can convert the results of the group + operation to a POJO and also integrates with Spring's Resource + abstraction abstraction. This will let you place your JavaScript + files on the file system, classpath, http server or any other Spring + Resource implementation and then reference the JavaScript resources via an + easy URI style syntax, e.g. 'classpath:reduce.js;. Externalizing + JavaScript code in files if often preferable to embedding them as Java + strings in your code. Note that you can still pass JavaScript code as Java + strings if you prefer. + +
+ Example Usage + + In order to understand how group operations work the following + example is used, which is somewhat artifical. For a more realistic + example consult the book 'MongoDB - The definitive guide'. A collection + named "group_test_collection" created with the following rows. + + { "_id" : ObjectId("4ec1d25d41421e2015da64f1"), "x" : 1 } +{ "_id" : ObjectId("4ec1d25d41421e2015da64f2"), "x" : 1 } +{ "_id" : ObjectId("4ec1d25d41421e2015da64f3"), "x" : 2 } +{ "_id" : ObjectId("4ec1d25d41421e2015da64f4"), "x" : 3 } +{ "_id" : ObjectId("4ec1d25d41421e2015da64f5"), "x" : 3 } +{ "_id" : ObjectId("4ec1d25d41421e2015da64f6"), "x" : 3 } + + We would like to group by the only field in each row, the 'x' + field and aggregate the number of times each specific value of 'x' + occurs. To do this we need to create an initial document that contains + our count variable and also a reduce function which will increment it + each time it is encountered. The Java code to execute the group + operation is shown below + + GroupByResults<XObject> results = mongoTemplate.group("group_test_collection", + GroupBy.key("x").initialDocument("{ count: 0 }").reduceFunction("function(doc, prev) { prev.count += 1 }"), + XObject.class); + + The first argument is the name of the collection to run the group + operation over, the second is a fluent API that specifies properties of + the group operation via a GroupBy class. In this + example we are using just the intialDocument + and reduceFunction methods. You can also + specify a key-function, as well as a finalizer as part of the fluent + API. If you have multiple keys to group by, you can pass in a comma + separated list of keys. + + The raw results of the group operation is a JSON document that + looks like this + + { + "retval" : [ { "x" : 1.0 , "count" : 2.0} , + { "x" : 2.0 , "count" : 1.0} , + { "x" : 3.0 , "count" : 3.0} ] , + "count" : 6.0 , + "keys" : 3 , + "ok" : 1.0 +} + + The document under the "retval" field is mapped onto the third + argument in the group method, in this case XObject which is shown + below. + + public class XObject { + + private float x; + + private float count; + + + public float getX() { + return x; + } + + + public void setX(float x) { + this.x = x; + } + + + public float getCount() { + return count; + } + + + public void setCount(float count) { + this.count = count; + } + + + @Override + public String toString() { + return "XObject [x=" + x + " count = " + count + "]"; + } + +} + + You can also obtain tha raw result as a + DbObject by calling the method + getRawResults on the + GroupByResults class. + + There is an additional method overload of the group method on + MongoOperations which lets you specify a + Criteria object for selecting a subset of the + rows. An example which uses a Criteria object, + with some syntax sugar using static imports, as well as referencing a + key-function and reduce function javascript files via a Spring Resource + string is shown below. + + import static org.springframework.data.mongodb.core.mapreduce.GroupBy.keyFunction; +import static org.springframework.data.mongodb.core.query.Criteria.where; + +GroupByResults<XObject> results = mongoTemplate.group(where("x").gt(0), + "group_test_collection", + keyFunction("classpath:keyFunction.js").initialDocument("{ count: 0 }").reduceFunction("classpath:groupReduce.js"), XObject.class); +
+
+
Overriding default mapping with custom converters @@ -2356,4 +2490,4 @@ mongoTemplate.dropCollection("MyNewCollection"); } });
- \ No newline at end of file +