文档章节

act-morphia 1.7.2 带来不一样的数据聚合体验

罗格林
 罗格林
发布于 06/18 05:52
字数 2487
阅读 1185
收藏 9

1. 概述

Mongodb 2.2 开始就提供了数据Aggregation Pipeline (聚合管道)用于简单数据分析统计,包括计数(count),求和(sum),均值(average),标准差(stddev) 等. 这个特性相较以前的 Map Reduce 方式提升了很多. 遗憾的是在服务端代码上使用 Aggregation Pipeline 还是需要使用比较繁复的 API, 包括 Spring Data 和 Morphia 提供的 API. 这大多是因为 Aggregation Pipeline 需要兼顾各种情况, 比如嵌入数组的 rewind, 还有对第一次聚合数据进行再聚合等.

在很多常用情况下, 应用只需要简单的分组聚合, 最多对聚合结果数据进行过滤和排序. 这时候我们希望能通过更简单的方式来获得结果. Act-morphia 插件在随 act-starter-1.8.23.2 最新发布的 1.7.2 版本中提供了一组简单易用的 API 来实现常用聚合逻辑.

2. API 简介

Act-Morphia 依托与 Morphia 库 实现了 Act-DB 框架. 下面是 Act-Morphia 的简单介绍. 已经了解 Act-Morphia 的同学可以直接跳到后面的数据聚合章节.

2.1 Entity (实体类)

任何被 org.mongodb.morphia.annotations.Entity 注解的类. 我们推荐应用实体类继承 MorphiaModel 或者 MorphiaAdaptiveRecord.

示例:

@Entity("order")
public class Order extends MorphiaModel<Order> {
    public int price;

    @Property("reg")
    public String region;

    @Property("dep")
    public String department;
}

(注: 因为本文重点在于数据聚合, 所以 Order 的一些必要属性, 比如产品编号之类的信息在该模型中省却了)

2.2 Dao (数据库访问组件)

Act 定义了通用 Dao 接口, 在不同插件实现下提供对 SQL 和 MongoDB 的访问. Act-Morphia 是基于 Morphia 库的实现 MorphiaDao, 为应用提供 MongoDB 数据访问. 使用 Dao 的方式是直接在服务类中注入相关实现. 假设我们有一个访问 Order 数据的 RESTful 服务, 可以这样来是用 Dao:

@UrlContext("orders")
public class OrderService {
	@Inject // 这里注入 MorphiaDao, 其类型参数必须设置为 `Order`
	private MorphiaDao<Order> dao;
	
	@GetAction
	public Iterable<Order> listAll() {
		return dao.q(); // dao.q() 返回一个新建的 MorphiaQuery 实例, 其实现了 Iterable<Order> 接口
	}
	
	@GetAction("{order}")
	public Order fetch(@DbBind Order order) {
		// ActFramework 自动从 URL 路径中拿到 order 的 ID, 并从数据库中加载为 order 实例
		return order;
	}
	
	@PostAction
	public Order create(Order order) {
		// ActFramework 自动从 JSON 或者 Form 数据中
		return dao.save(order);
	}
	
	@PutAction("{order}/price")
	public void updatePrice(@DbBind @NotNull Order order, int newPrice) {
		order.price = newPrice;
		dao.save(order);
	}

	@DeleteAction("{order}")
	public void delete(@DbBind @NotNull Order order) {
		dao.delete(order);
	}
}

2.3 简单数据聚合 API

2.3.1 SimpleAggregation

提供了简单的 Aggregation Pipeline 封装, 用于帮助应用构建并发送 Aggregation Pipeline 到 MongoDB 以获取聚合结果. 应用通常不会直接创建 SimpleAggregation 实例. 基本上都通过一下方式获得 SimpleAggregation:

  1. MorphiaDao.aggregation() 或其简写 MorphiaDao.a(): 获得一个没有 Where 条件的 SimpleAggregation.
  2. MorphiqQuery.aggregation(): 获得一个带 Where 条件的 SimpleAggregation, 条件由 Query 定义

拿到 SimpleAggregation 实例之后可以链式调用下面的 API:

  • groupCount(String key1, String ... otherKeys) - 分组计数
  • groupSum(String key1, String ... otherKeys) - 分组求和
  • groupAverage(String key1, String ... otherKeys) - 分组求均值
  • groupStdDev(String key1, String ... otherKeys) - 分组求标准差
  • groupSampleStdDev(String key1, String ... otherKeys) - 分组求抽样标准差
  • atLeast(Number) - 过滤聚合数据, 要求必须大于或等于给定参考值
  • atMost(Number) - 过滤聚合数据, 要求必须小于或等于给定参考值
  • greaterThan(Number) - 过滤聚合数据, 要求必须大于给定参考值
  • lessThan(Number) - 过滤聚合数据, 要求必须小于给定参考值
  • between(minInclusive, maxExclusive) - 过滤聚合数据, 要求必须大于或等于第一个参数, 小于第二个参数
  • sorted() - 将聚合数据按数值大小升序排列
  • sorted(true) - 将聚合数据按数值大小降序排列
  • get() - 返回一个 AggregationResult<Double> 的对象, 数值类型为 Double
  • getAsInt() - 返回一个 AggregationResult<Integer> 的对象, 数值类型为 Integer
  • getAsLong() - 返回一个 AggregationResult<Long> 的对象, 数值类型为 Long
  • getAsMap() - 返回一个 Map<Object, Double> 类型的聚合结果, key 为分组数据, val 为数值
  • getAsIntMap() - 返回一个 Map<Object, Integer> 类型的聚合结果, key 为分组数据, val 为数值
  • getAsLongMap() - 返回一个 Map<Object, Long> 类型的聚合结果, key 为分组数据, val 为数值

2.3.2 AggregationResult

封装 MongoDB 聚合返回的结果. 因为 MongoDB 返回的聚合结果是一个 Cursor(游标), 通过 Cursor 取回的数据是一个 DBObject 的列表, 并不是非常方便应用使用. AggregationResult 类提供了一套简单的 API 供应用访问聚合结果

  • val() 返回聚合结果 - 用于 sum, average, stdDev 不分组的情况访问聚合结果数据
  • val(Object groupValue, Object... groupValues) 按照分组数据返回聚合结果数据, 分组数据的给出顺序应该和 SimpleAggregation.groupXxx 方法中给出的分组顺序一致.
  • val(Map<String, Object> groupValues) - 按照分组数据返回聚合结果, 分组数据和分组名字对应.
  • Map<Object, T> asMap() - 返回所有分组的聚合结果并依照分组数据索引.

3. 实例分析

下面我们将使用上节中的简单例子来介绍 Act-morphia 的简单聚合 API 如何满足常用的数据聚合需求.

@UrlContext("order/aggregations")
public class OrderAggregationService {

	@Inject
	private MorphiaDao<Order> dao;

	/**
	 * 统计一共有多少订单.
	 * 
	 * 这个可以直接用 Dao 上的 count() 方法, 无需使用聚合管道
	 */
	@GetAction("count")
	public long count() {
		return dao.count();
	}
	
	/**
	 * 按照地区统计订单数量
	 * 
	 * 直接构造 MorphiaQuery 对象并使用其 count() 方法
	 * 这个方法也无需聚合管道
	 */ 
	 @GetAction("/regions/{region}/count")
	public long countByRegion(String region) {
		// 使用字段名构造查询
		return dao.q("region", region).count();
		// 也可以使用 MongoDB column 名字来构造查询:
		// return dao.q("reg", region).count();
	}
	
	/**
	 * 按照部门统计订单数量
	 * 
	 * 直接构造 MorphiaQuery 对象并使用其 count() 方法
	 * 这个方法也无需聚合管道
	 */ 
	 @GetAction("/departments/{department}/count")
	public long countByDepartment(String department) {
		// 使用字段名构造查询
		return dao.q("department", department).count();
		// 也可以使用 MongoDB column 名字来构造查询:
		// return dao.q("dep", department).count();
	}
	
	/**
	 * 按照地区以及部门统计订单数量
	 *
	 * 直接构造 MorphiaQuery 对象并使用其 count() 方法
	 * 这个方法也无需聚合管道
	 */
	 @GetAction("/region={region},department={department}/count")
	public long countByRegionAndDepartment(String region, String department) {
		// 使用字段名构造查询
		return dao.q("region,department", region, department).count();
		// 也可以使用 MongoDB column 名字来构造查询:
		// return dao.q("reg,dep", region, department).count();
	}
	
	/**
	 * 一次性获得按地区分组统计订单数量的聚合结果
	 *
	 * 返回的 Map key 为地区, value 为该地区的订单数量
	 */
	 @GetAction("/regions/~group-count~")
	public Map<Object, Long> groupCountByRegion() {
		return dao.a().groupCount("region").getAsLongMap()
	}
	
	/**
	 * 一次性获得按部门分组统计订单数量的聚合结果
	 *
	 * 返回的 Map key 为部门, value 为该部门的订单数量
	 */
	 @GetAction("/departments/~group-count~")
	public Map<Object, Long> groupCountByDepartment() {
		return dao.a().groupCount("department").getAsLongMap()
	}
	
	/**
	 * 一次性获得按部门分组统计订单数量的聚合结果
	 *
	 * 返回的 Map key 为地区与部门列表, value 为该地区,部门对应的订单数量
	 */
	 @GetAction("/~region-department~/~group-count~")
	public Map<Object, Long> groupCountByRegionAndDepartment() {
		return dao.a().groupCount("region,department").getAsLongMap();
	}

	/**
	 * 一次性获得部门分组统计小订单数量的聚合结果
	 *
	 * @param priceTarget - 订单价格, 小于该价格的订单为小订单
	 * @return 返回的 Map key 为部门, value 为该部门的订单数量
	 */
	 @GetAction("/departments/~group-count~/~small-orders~/")
	public Map<Object, Long> groupCountByDepartmentForSmallOrders(int priceTarget) {
		return dao.q("price <", priceTarget).groupBy("department").count().getAsLongMap();
	}
	
	/**
	 * 返回所有订单的价格汇总
	 */
	 @GetAction("sum")
	public Long sum() {
		return dao.q().longSum("price");
		// 或者这样:
		// return dao.a().groupSum("price", null).getAsLong().val();
	}
	
	/**
	 * 返回所有小订单的价格汇总
	 *
	 * @param priceTarget - 订单价格, 小于该价格的订单为小订单
	 */
	@GetAction("sum/~small-orders~")
	public Long sumSmallOrders(@NotNull Integer priceTarget) {
		return dao.q("price <", priceTarget).longSum("price");
	}
	
	/**
	 * 返回部门订单价格汇总
	 *
	 * @param department - 指定部门
	 */
	 @GetAction("/departments/{department}/sum")
	public Long sumByDepartment(String department) {
		return dao.q("department", department).longSum("price");
	}

	
	/**
	 * 返回地区及部门订单价格汇总
	 *
	 * @param region - 指定地区
	 * @param department - 指定部门
	 */
	@GetAction("region={region},department={department}/sum")
	public Long sumByDepartment(String region, String department) {
		return dao.q("region,department", region, department).longSum("price");
	}
	
	/**
	 * 返回按地区分组价格汇总
	 *
	 * @return Map 的 key 为地区, value 为该地区价格汇总
	 */
	@GetAction("regions/~group-sum~")
	public Map<Object, Long> groupSumByRegion() {
		return dao.a().groupSum("price", "region").getAsLongMap();
		// 或者这样
		// return dao.q().groupBy("region").sum("price").getAsLongMap();
	}
	
	/**
	 * 返回按地区分组价格汇总, 过滤掉业绩达标的记录.
	 *
	 * 注意, 我们这次不能直接在查询中构造过滤条件, 而是使用 SimpleAggregation 的 lessThan 方法, 
	 * 这是因为查询条件是过滤数据库原始数据的, 而该方法需要过滤汇总后的数据. 简单的说, 查询条件
	 * 相当于 SQL 中的 where 子句, 而我们需要的 SQL 的 having 子句.
	 * 
	 * @param priceTarget - 达标业绩
	 * @return Map 的 key 为地区, value 为该地区价格汇总
	 */
	@GetAction("departments/~group-sum~/~bad-performance~")
	public Map<Object, Long> groupSumByRegionBadPerformance(int priceTarget) {
		return dao.q().groupBy("region").sum("price").lessThan(priceTarget).getAsLongMap();
	}
	
	/**
	 * 返回按地区分组平均价格
	 *
	 * @return Map 的 key 为地区, value 为该地区平均价格
	 */
	@GetAction("regions/~group-average~")
	public Map<Object, Double> groupAverageByRegion() {
		return dao.q().groupBy("region").average("price").getAsMap();
	}
	
	/**
	 * 返回按地区分组价格标准差
	 *
	 * @return Map 的 key 为地区, value 为该地区标准差
	 */
	@GetAction("regions/~group-std-dev~")
	public Map<Object, Double> groupStdDevByRegion() {
		return dao.q().groupBy("region").stdDev("price").getAsMap();
	}

	/**
	 * 返回按地区分组价格抽样标准差.
	 *
	 * 当数据量特别大的时候可以使用抽样统计标准差
	 *
	 * @param samples 样本数量
	 * @return Map 的 key 为地区, value 为该地区抽样标准差
	 */
	@GetAction("regions/~group-sample-std-dev~")
	public Map<Object, Double> groupStdDevByRegion(int samples) {
		return dao.q().groupBy("region").sampleStdDev("price", samples).getAsMap();
	}

}

4. 总结

本文简要介绍了 act-morphia 1.7.2 版本带来的新的聚合 API 以及使用方式, 希望能帮助到使用 act 操作 mongodb 数据库的同学. 如果大家对此有意见或者建议, 请在下面提出您宝贵的意见

© 著作权归作者所有

罗格林

罗格林

粉丝 309
博文 46
码字总数 49389
作品 4
其他
架构师
私信 提问
加载中

评论(7)

梧桐宇宇
梧桐宇宇
用mongo最痛苦的点解决了,非常好
zyz140
zyz140
围观
Artlongs
Artlongs
很好,很强大.
罗格林
罗格林 博主

引用来自“闲大赋”的评论

厉害!ACT我觉得从功能上比Spring差不了多少
让应用使用比 Spring 更简单的表达方式来表达同样的功能是 Act 的核心优势
闲大赋
闲大赋
厉害!ACT我觉得从功能上比Spring差不了多少
一笑阑珊5516
一笑阑珊5516
支持大佬
NickLaus2014
NickLaus2014
羡慕大神,想到什么立马就能实现。加油!!!
ActFramework 1.8.23 发布,非轻量级 Java 框架

1. 核心框架变化 ActFramework 1.8.23 发布, 带来的变化有: #1143 CLI 列表显示 - 交替反显, 更加便于阅读数据: #1142 增加 @Label 注解, Excel 和 CSV 输出的时候显示更加友好的名字: CLI 列...

罗格林
06/18
0
0
act-starters 发布,让 #actframework 应用开发更简便

act-starters 是一系列简化 #actframework 应用依赖管理和其他 maven 构造处理的工具. 现在已经发布了 12 种 starters: act-starter-beetl - 使用 beetl 为默认模板引擎 act-starter-ebean2...

罗格林
2018/01/03
493
11
ActFramework 1.8.16 发布,高性能 Java Web 框架

ActFramework 是一款高效能(对程序员)和高性能(对运行系统)的 Java Web 应用和服务框架. 本次更新的主要内容: 改进对 XML 输入输出的处理 请求处理函数参数类型有循环引用时导致堆栈溢出 异步...

罗格林
2018/12/10
0
0
ActFramework 1.7.0 - 1.8.1,高性能 Java Web 框架

没有及时发布新闻,真是不好意思。今天打个新闻批发,把 Act 1.7.0 到 1.8.1 中值得注意的更新给大家汇报一下。 Act-1.8.1 - 2018-03-11 支持 Request Forward forward 和 redirect 的区别:...

罗格林
2018/03/12
2.5K
20
Act-starter 1.8.8.0 强势发布 - 带来端到端自动化测试支持

Act-starter 1.8.8.0 是以 act-1.8.8-RC4 为核心的多个 starters 项目集合. 这个版本带来超过 40 项修改或增强: [act-e2e] 新的插件带来端到端自动化测试支持 [act-ebean]注册全局映射过滤器...

罗格林
2018/05/14
644
2

没有更多内容

加载失败,请刷新页面

加载更多

php 遇到 No input file specified的解决方法

(一)IIS Noinput file specified 方法一:改PHP.ini中的doc_root行,打开ini文件注释掉此行,然后重启IIS 方法二: 请修改php.ini 找到 ; cgi.force_redirect = 1 去掉前面分号,把后面的1...

chenhongjiang
今天
5
0
MySQL 基础

一、常用命令 在命令行中,配置好环境变量后,通过cmd可以直接进入mysql命令行模式,同时列举几种常用命令 # 进入mysql数据库,密码可以先不写,打完-p后再输入,防止被别人看到mysql -u账...

华山猛男
今天
6
0
简单的博客系统(四)Django请求HTML页面视图信息--基于函数的视图

1. 编写用于查询数据的功能函数 应用目录 下的 views.py 文件通常用于保存响应各种请求的函数或类 from django.shortcuts import renderfrom .models import BlogArticles# Create your ...

ZeroBit
今天
5
0
用脚本将本地照片库批量导入到Day One中

因为目前iCloud 空间已经不足,其中95%都是照片,之前入手了DayOne,且空间没有限制,订阅费一年也不少,再加上DayOne作为一款日记App 也比较有名,功能方面最大的就是地理视图与照片视图,尤...

在山的那边
昨天
19
0
jupyter部署安装

python373 -m ipykernel install --name python373 ipython kernelspec list sc create myjupyterservice binpath="D:\apply\Python373\Scripts\jupyter-notebook --config=V:/my_work/jupyt......

mbzhong
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部