在 MongoDB 中,我们可以将分组操作用作 Map-Reduce 进行数据聚合的替代方法。分组操作类似于使用 SQL 语句的按查询方式分组,所以与使用 Map-Reduce 相比,分组操作可能感觉更容上手。
注意:使用分组操作有一些限制,例如:在共享环境中不支持,而且它在一个 BSON 对象中返回全部结果集,所以结果应该要很小,少于 10000 个键。
Spring Data MongoDB 通过在 MongoOperations 上提供方法来简化组操作的创建和运行,从而提供了与 MongoDB 的组操作的集成。它可以将组操作的结果转换为 POJO,还可以与 Spring 的资源抽象集成。这将让你把你的 JavaScript 文件放在文件系统、classpath、http 服务器或任何其他 Spring 资源实现上,然后通过简单的 URI 风格的语法引用 JavaScript 资源,例如:classpath:reduce.js。
提示:在文件中外部化 JavaScript 代码通常比在代码中将 JavaScript 作为 Java 字符串嵌入要好,也推荐这样做。当然,你还是可以将 JavaScript 代码作为 Java 字符串嵌入,只是不推荐这样做而已。
为了理解组操作是如何工作的,我们使用了下面的例子。创建了一个名为 group_test_collection 的集合,集合有以下几条记录:
{ "_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 }
我们想按每行中唯一的字段(即 x 字段)进行分组,并汇总 x 的每个特定值出现的次数。要做到这一点,我们需要创建一个初始文件,其中包含我们的 count 变量和一个 reduce 函数,该函数将在每次遇到它时增加它。运行分组操作的 Java 代码如下所示:
GroupByResults<XObject> results = mongoTemplate.group("group_test_collection", GroupBy.key("x").initialDocument("{ count: 0 }") .reduceFunction("function(doc, prev) { prev.count += 1 }"), XObject.class);
代码说明:
第一个参数是要运行分组操作的集合的名称,这里是:group_test_collection
第二个参数是一个流式 API,通过 GroupBy 类指定分组操作的属性。在这个例子中,我们只使用 intialDocument() 和 reduceFunction() 方法。你也可以指定一个 key-function,以及一个 finalizer 作为 流式 API 的一部分。如果你有多个键要分组,你可以传入一个逗号分隔的键列表。
MongoDB 组操作的原始结果是一个 JSON 文档,看起来如下:
{ "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 }
注意,retval 字段下的文件将被映射到组方法的第三个参数上,这个例子是 XObject,XObject 的代码如下:
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 + "]"; } }
你也可以通过调用 GroupByResults 类上的 getRawResults() 方法来获得原始结果作为一个文档。
MongoOperations 上的 group() 方法有一个额外的方法重载,可以让你指定一个 Criteria 对象来选择一个行的子集。下面是一个使用 Criteria 对象的例子,其中使用了一些静态导入的语法,并通过 Spring 资源字符串引用了一个 keyFunction.js 和 groupReduce.js 文件。如下:
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);
关于怎样配置 MongoTemplate 这里不在赘述,下面直接给出完整代码。
(1)keyFunction.js 代码
function keyFunction(prev) { return { "x": prev.x }; }
(2)groupReduce.js 代码
function groupReduce(doc, prev) { prev.count += 1 }
(3)XObject.java 代码
package com.hxstrive.springdata.mongodb.entity; 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 + "]"; } }
(4)GroupDemo.java 代码
package com.hxstrive.springdata.mongodb; import com.hxstrive.springdata.mongodb.entity.XObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.mapreduce.GroupBy; import org.springframework.data.mongodb.core.mapreduce.GroupByResults; import java.util.function.Consumer; import static org.springframework.data.mongodb.core.mapreduce.GroupBy.keyFunction; import static org.springframework.data.mongodb.core.query.Criteria.where; /** * 分组操作 * @author hxstrive.com */ @SpringBootTest class GroupDemo { @Autowired private MongoTemplate mongoTemplate; @BeforeEach public void init() { mongoTemplate.dropCollection("group_test_collection"); // 准备数据 String[] datas = { "{ \"_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 }" }; for(String data : datas) { mongoTemplate.save(data, "group_test_collection"); } } @Test public void group1() { GroupByResults<XObject> results = mongoTemplate.group("group_test_collection", GroupBy.key("x").initialDocument("{ count: 0 }") .reduceFunction("function(doc, prev) { prev.count += 1 }"), XObject.class); results.forEach(new Consumer() { @Override public void accept(Object o) { System.out.println(o); } }); // 结果: // XObject [x=1.0 count = 2.0] // XObject [x=2.0 count = 1.0] // XObject [x=3.0 count = 3.0] // 注意:笔者运行的 MongoDB 版本为 3.4.15,在 5.0.14 版本运行抛出如下错误: // com.mongodb.MongoCommandException: Command failed with error 59 (CommandNotFound): 'no such command: 'group'' // on server localhost:27017. The full response is {"ok": 0.0, "errmsg": "no such command: 'group'", "code": 59, // "codeName": "CommandNotFound"} } @Test public void group2() { 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); results.forEach(new Consumer() { @Override public void accept(Object o) { System.out.println(o); } }); // 结果: // XObject [x=1.0 count = 2.0] // XObject [x=2.0 count = 1.0] // XObject [x=3.0 count = 3.0] } }