diff --git a/docs/uniCloud/cf-database.md b/docs/uniCloud/cf-database.md index 09f117b4e58e708e7fd5001e2be56193d7f9d875..1f0caec27f68f65c4f3ea85b55a2e7f8bfffebef 100644 --- a/docs/uniCloud/cf-database.md +++ b/docs/uniCloud/cf-database.md @@ -1,6 +1,9937 @@ ## 云数据库简介 `uniCloud`提供了一个 JSON 格式的文档型数据库,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。 +## 云数据库简介 + +`uniCloud`提供了一个 JSON 格式的文档型数据库,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。 + +关系型数据库和 JSON 文档型数据库的概念对应关系如下表: + +|关系型 |JSON 文档型 | +|:- |:- | +|数据库 database|数据库 database | +|表 table |集合 collection | +|行 row |记录 record / doc| +|列 column |字段 field | + +云数据库支持通过云函数访问,也支持在客户端访问云数据库。 + +客户端访问云数据库,称为[clientDB](https://uniapp.dcloud.net.cn/uniCloud/database)。使用[clientDB](https://uniapp.dcloud.net.cn/uniCloud/database)推荐HBuilderX 2.9.5以上版本。2.9.5以下的版本需单独下载插件,并且不支持`jql`,不再推荐使用。 + +[clientDB](https://uniapp.dcloud.net.cn/uniCloud/database)可极大的提升开发效率,是uniCloud开发者的必备插件。 + +不管在云函数里操作数据库,还是使用[clientDB](https://uniapp.dcloud.net.cn/uniCloud/database)在前端操作数据库,均支持传统的MongoDB查询语法。 + +**本文仅介绍MongoDB的传统语法,[clientDB](https://uniapp.dcloud.net.cn/uniCloud/database)还支持了更简单易用的`jql`语法,推荐开发者使用和学习更简单的方式,[另见jql](https://uniapp.dcloud.net.cn/uniCloud/database?id=jsquery)** + +阿里云使用的mongoDB数据库版本为4.0,腾讯云使用的腾讯云自研的文档型数据库(兼容mongoDB 4.0版本)。请注意API的兼容性说明 + +如果同时还想连接其他数据库,如mysql/redis,用法可以参考如下插件,但非uniCloud环境的数据库连接速度,没有uniCloud的云数据库快: + +- [云函数连接Mysql数据库示例](https://ext.dcloud.net.cn/plugin?id=1925) +- [云函数连接Redis数据库示例](https://ext.dcloud.net.cn/plugin?id=1846) + +## 获取数据库的引用 + +不管云函数还是前端,获取数据库连接都是如下写法。前端写法需2.9.5起支持。 + +```js +const db = uniCloud.database(); +``` + +**DBOptions参数说明** + +|字段 |类型 |必填 |描述 |平台差异说明 | +|:-: |:-: |:-: |:-: |:-: | +|spaceId|String |否 |同一账号下的,服务空间ID |仅腾讯云支持 | + +## 新增集合/数据表 + +如果集合已存在,则报错。 + +**平台差异说明** + +|阿里云 |腾讯云 | +|---- |---- | +|× |√ | + +``` +const db = uniCloud.database(); +db.createCollection(collectionName) +``` + +阿里云新建集合,有2种方式, +1. 在web控制台创建。 +2. 调用add方法,给某集合新增数据记录时,如果该表不存在,会自动创建该表 + +腾讯云可以在web控制台创建表,也可以通过本API创建表,但腾讯云add方法不会自动创建表。 + +## 使用db\_init.json初始化项目数据库@db-init + +自`HBuilderX 2.5.11`起`uniCloud`提供了`db_init.json`来方便开发者快速进行数据库的初始化操作,即在HBuilderX工具中,将本地数据直接同步到云数据库中。 + +这个功能尤其适合插件作者,可以快速初始化集合和数据。 + +**使用db_init.json导入数据库** + +在HBuilderX中项目下对cloudfunctions目录下的`db_init.json`点右键,即可选择`初始化云数据库`。将`db_init.json`里的内容导入云端。 + +**生成db_init.json的方式** +1. 在uniCloud web控制台的数据库界面,左侧导航点击生成`db_init.json`,会将选择的表的内容、索引、表结构导出为`db_init.json`文件 +2. 也可以手动编写`db_init.json`,在HBuilderX中项目下`cloudfucntions`目录右键即可创建`db_init.json`, + +**注意事项** +- 目前`db_init.json`为同步导入形式,无法导入大量数据,后续会实现异步导入方案。 + +db_init.json包含三部分:数据内容(data)、数据表索引(index)、数据表结构(schema),形式如下 + +``` +{ + "collection_test": { // 集合(表名) + "data": [ // 数据 + { + "_id": "da51bd8c5e37ac14099ea43a2505a1a5", + "name": "tom" + } + ], + "index": [{ // 索引 + "IndexName": "index_a", // 索引名称 + "MgoKeySchema": { // 索引规则 + "MgoIndexKeys": [{ + "Name": "index", // 索引字段 + "Direction": "1" // 索引方向,1:ASC-升序,-1:DESC-降序,2dsphere:地理位置 + }], + "MgoIsUnique": false // 索引是否唯一 + } + }], + "schema": { + "bsonType": "object", + "permission": { + ".read": true, + ".create": false, + ".update": false, + ".delete": false + }, + "required": [ + "image_url" + ], + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "image_url": { + "bsonType": "string", + "description": "可以是在线地址,也支持本地地址", + "label": "图片url" + } + } + } + } +} +``` + + +## 数据库回档@backup + +**此功能暂时只有腾讯云支持** + +uniCloud会在每天备份一次数据库,最多保留7天。 + +**操作说明** + +1. 登录[uniCloud后台](https://unicloud-dev.dcloud.net.cn/) +2. 点击左侧菜单`云数据库 --> 数据库回档`,点击`新建回档` +3. 选择可回档时间 +4. 选择需要回档的集合(注意:回档后集合不能与现有集合重名,如需对集合重命名可以在集合列表处操作) + +![数据库回档](https://img.cdn.aliyun.dcloud.net.cn/uni-app/uniCloud/unicloud-db-backup.jpg) + +## 数据导出@export + +**此功能暂时只有阿里云支持** + +此功能主要用于导出整个集合的数据 + +**用法** + +1. 进入[uniCloud web控制台](https://unicloud.dcloud.net.cn/home),选择服务空间,或者直接在HBuilderX云函数目录`cloudfunctions`上右键打开uniCloud web控制台 +2. 进入云数据库选择希望导入数据的集合 +3. 点击导出按钮 +4. 选择导出格式,如果选择csv格式还需要选择导出字段 +5. 点击确定按钮等待下载开始即可 + +**注意** + +- 导出的json文件并非一般情况下的json,而是每行一条json数据的文本文件 +- 导出为csv时必须填写字段选项。字段之间使用英文逗号隔开。例如:`_id, name, age, gender` +- 数据量较大时可能需要等待一段时间才可以开始下载 + +## 数据导入@import + +**此功能暂时只有阿里云支持** + +uniCloud提供的`db_init.json`主要是为了对数据库进行初始化,并不适合导入大量数据。与`db_init.json`不同,数据导入功能可以导入大量数据,目前支持导入 CSV、JSON 格式(关于json格式看下面注意事项)的文件数据。 + +**用法** + +1. 进入[uniCloud web控制台](https://unicloud.dcloud.net.cn/home),选择服务空间,或者直接在HBuilderX云函数目录`cloudfunctions`上右键打开uniCloud web控制台 +2. 进入云数据库选择希望导入数据的集合 +3. 点击导入,选择json文件或csv文件 +4. 选择处理冲突模式(关于处理冲突模式请看下方注意事项) +5. 点击确定按钮等待导入完成即可 + +**注意** + +- 目前导入文件最大限制为50MB +- 导入csv时数据类型会丢失,即所有字段均会作为字符串导入 +- 冲突处理模式为设定记录_id冲突时的处理方式,`insert`表示冲突时依旧导入记录但是是新插入一条,`upsert`表示冲突时更新已存在的记录 +- 这里说的json文件并不是标准的json格式,而是形如下面这样每行一个json格式的数据库记录的文件 + ```json + {"a":1} + {"a":2} + ``` + +## 获取集合的引用 + +```js +const db = uniCloud.database(); +// 获取 `user` 集合的引用 +const collection = db.collection('user'); +``` + +### 集合 Collection + +通过 `db.collection(name)` 可以获取指定集合的引用,在集合上可以进行以下操作 + +| 类型 | 接口 | 说明 | +| -------- | ------- | ---------------------------------------------------------------------------------- | +| 写 | add | 新增记录(触发请求) | +| 计数 | count | 获取符合条件的记录条数 | +| 读 | get | 获取集合中的记录,如果有使用 where 语句定义查询条件,则会返回匹配结果集 (触发请求) | +| 引用 | doc | 获取对该集合中指定 id 的记录的引用 | +| 查询条件 | where | 通过指定条件筛选出匹配的记录,可搭配查询指令(eq, gt, in, ...)使用 | +| | skip | 跳过指定数量的文档,常用于分页,传入 offset | +| | orderBy | 排序方式 | +| | limit | 返回的结果集(文档数量)的限制,有默认值和上限值 | +| | field | 指定需要返回的字段 | + + +查询及更新指令用于在 `where` 中指定字段需满足的条件,指令可通过 `db.command` 对象取得。 + + +### 记录 Record / Document + +通过 `db.collection(collectionName).doc(docId)` 可以获取指定集合上指定 id 的记录的引用,在记录上可以进行以下操作 + +| 接口| 说明 | | +| ----| ------|---- | +| 写 | set | 覆写记录 | +| | update| 局部更新记录(触发请求)| +| | remove| 删除记录(触发请求) | +| 读 | get | 获取记录(触发请求) | + + +### 查询筛选指令 Query Command + +以下指令挂载在 `db.command` 下 + +| 类型 | 接口 | 说明 | +| -------- | ---- | ---------------------------------- | +| 比较运算 | eq | 字段等于 == | +| | neq | 字段不等于 != | +| | gt | 字段大于 > | +| | gte | 字段大于等于 >= | +| | lt | 字段小于 < | +| | lte | 字段小于等于 <= | +| | in | 字段值在数组里 | +| | nin | 字段值不在数组里 | +| 逻辑运算 | and | 表示需同时满足指定的所有条件 | +| | or | 表示需同时满足指定条件中的至少一个 | + +如果你熟悉SQL,可查询[mongodb与sql语句对照表](https://blog.csdn.net/xinghebuluo/article/details/7012788/)进行学习。 + +### 字段更新指令 Update Command + +以下指令挂载在 `db.command` 下 + +| 类型 | 接口 | 说明 | +| ---- | ------- | -------------------------------- | +| 字段 | set | 设置字段值 | +| | remove | 删除字段 | +| | inc | 加一个数值,原子自增 | +| | mul | 乘一个数值,原子自乘 | +| | push | 数组类型字段追加尾元素,支持数组 | +| | pop | 数组类型字段删除尾元素,支持数组 | +| | shift | 数组类型字段删除头元素,支持数组 | +| | unshift | 数组类型字段追加头元素,支持数组 | + + +## 支持的数据类型 + +数据库提供以下几种数据类型: +* String:字符串 +* Number:数字 +* Object:对象 +* Array:数组 +* Bool:布尔值 +* GeoPoint:地理位置点 +* GeoLineStringL: 地理路径 +* GeoPolygon: 地理多边形 +* GeoMultiPoint: 多个地理位置点 +* GeoMultiLineString: 多个地理路径 +* GeoMultiPolygon: 多个地理多边形 +* Date:时间 +* Null + +以下对几个特殊的数据类型做个补充说明 + +### 时间 Date + +**使用阿里云时请存储日期字符串或者时间戳,比如`new Date().toISOString()`。数据库存储Date类型数据仅腾讯云支持** + + + + Date 类型用于表示时间,精确到毫秒,可以用 JavaScript 内置 Date 对象创建。需要特别注意的是,用此方法创建的时间是客户端时间,不是服务端时间。如果需要使用服务端时间,应该用 API 中提供的 serverDate 对象来创建一个服务端当前时间的标记,当使用了 serverDate 对象的请求抵达服务端处理时,该字段会被转换成服务端当前的时间,更棒的是,我们在构造 serverDate 对象时还可通过传入一个有 offset 字段的对象来标记一个与当前服务端时间偏移 offset 毫秒的时间,这样我们就可以达到比如如下效果:指定一个字段为服务端时间往后一个小时。 + + + + ```js + //服务端当前时间 + new db.serverDate() + ``` + + ```js + //服务端当前时间加1S + new db.serverDate({ + offset: 1000 + }) + ``` + +如果需要对日期进行比较操作,可以使用聚合操作符将日期进行转化,比如以下示例查询所有time字段在`2020-02-02`以后的记录 + +```js +'use strict'; +const db = uniCloud.database() +exports.main = async (event, context) => { + const dbCmd = db.command + const $ = dbCmd.aggregate + let res = await db.collection('unicloud-test').where(dbCmd.expr( + $.gte(['$time',$.dateFromString({ + dateString: new Date('2020-02-02').toISOString() + })]) + )).get() + return res +}; +``` + +### 地理位置 + +参考:[GEO地理位置](#GEO地理位置) + +### Null + + Null 相当于一个占位符,表示一个字段存在但是值为空。 + +## 新增文档@add + +方法1: collection.add(data) + +参数说明 + +| 参数 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ---------------------------------------- | +| data | object | array | 是 | {_id: '10001', 'name': 'Ben'} _id 非必填| + +响应参数 + +单条插入时 + + +| 参数| 类型 | 说明 | +| ----| ------| ---------------------------------------- | +|id | String|插入记录的id | + +批量插入时 + +| 参数 | 类型 | 说明 | +| ---- | ------| ---------------------------------------- | +| inserted| Number| 插入成功条数 | +|ids | Array |批量插入所有记录的id | + +示例: + +```js +// 单条插入数据 +let res = await collection.add({ + name: 'Ben' +}) +// 批量插入数据 +let res = await collection.add([{ + name: 'Alex' +},{ + name: 'Ben' +},{ + name: 'John' +}]) +``` + +**Tips** + +- 云服务商为阿里云时,若集合不存在,调用add方法会自动创建集合 + +方法2: collection.doc().set(data) + +也可通过 `set` 方法新增一个文档,需先取得文档引用再调用 `set` 方法。 +如果文档不存在,`set` 方法会创建一个新文档。 + + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ---------------------------------------- | +| data | object | 是 | 更新字段的Object,{'name': 'Ben'} _id 非必填| + +**响应参数** + +| 参数 | 类型 | 说明 | +| ---- | ------| ---------------------------------------- | +|updated | Number| 更新成功条数,数据更新前后没变化时也会返回1 | +|upsertedId | String| 创建的文档id | + + +```js +let res = await collection.doc('doc-id').set({ + name: "Hey" +}); +``` + + +## 查询文档 + +支持 `where()`、`limit()`、`skip()`、`orderBy()`、`get()`、`field()`、`count()` 等操作。 + +只有当调用`get()`时才会真正发送查询请求。 + +注:默认取前100条数据,最大取前100条数据。 + +**get响应参数** + +| 参数| 类型 | 说明 | +| ----| ------| ---------------------------------------- | +|data | Array | 查询结果数组 | + +### 添加查询条件 + +collection.where() + +**在聚合操作中请使用match** + +设置过滤条件,where 可接收对象作为参数,表示筛选出拥有和传入对象相同的 key-value 的文档。比如筛选出所有类型为计算机的、内存为 8g 的商品: + +```js +let res = await db.collection('goods').where({ + category: 'computer', + type: { + memory: 8, + } +}).get() +``` + +如果要表达更复杂的查询,可使用高级查询指令,比如筛选出所有内存大于 8g 的计算机商品: +```js +const dbCmd = db.command // 取指令 +db.collection('goods').where({ + category: 'computer', + type: { + memory: dbCmd.gt(8), // 表示大于 8 + } +}) +``` + +在SQL里使用字符串表达式操作。但在NOSQL中使用json操作。这使得 等于 的表达,从 `=` 变成了 `:`;而大于的表达,从 `>` 变成了 `dbCmd.gt()` + +所有的比较符,详见[表格](https://uniapp.dcloud.io/uniCloud/cf-database?id=%e6%9f%a5%e8%af%a2%e7%ad%9b%e9%80%89%e6%8c%87%e4%bb%a4-query-command) + + +`where` 还可以使用正则表达式来查询文档,比如一下示例查询所有`name`字段以ABC开头的用户 +```js +db.collection('user').where({ + name: new RegExp('^ABC') +}) +``` + +### 获取查询数量 + +collection.count() + +```js +let res = await db.collection('goods').where({ + category: 'computer', + type: { + memory: 8, + } +}).count() +``` + +**注意** + +使用阿里云时,count必须搭配where使用,此问题阿里云正在修复。如果要count所有记录可以使用一个必然满足的条件,比如下面这样: + +```js +const dbCmd = db.command +let res = await db.collection('goods').where({ + _id: dbCmd.exists(true) +}).count() +``` + +响应参数 + +| 字段 | 类型 | 必填 | 说明 | +| --------- | ------- | ---- | ------------------------ | +| total | Integer | 否 | 计数结果 | + + + +### 设置记录数量 + +collection.limit() + +参数说明 + +| 参数 | 类型 | 必填 | 说明 | +| ----- | ------- | ---- | -------------- | +| value | Integer | 是 | 返回的数据条数 | + +使用示例 + +```js +let res = await collection.limit(1).get() // 只返回第一条记录 +``` + +### 设置起始位置 + +collection.skip(value) + +参数说明 + +| 参数 | 类型 | 必填 | 说明 | +| ----- | ------- | ---- | -------------- | +| value | Integer | 是 | 跳过指定的位置,从位置之后返回数据 | + +使用示例 + +```js +let res = await collection.skip(4).get() +``` + +### 对结果排序 + +collection.orderBy(field, orderType) + +参数说明 + +| 参数 | 类型 | 必填 | 说明 | +| --------- | ------ | ---- | ----------------------------------- | +| field | string | 是 | 排序的字段 | +| orderType | string | 是 | 排序的顺序,升序(asc) 或 降序(desc) | + +如果需要对嵌套字段排序,需要用 "点表示法" 连接嵌套字段,比如 style.color 表示字段 style 里的嵌套字段 color。 + +同时也支持按多个字段排序,多次调用 orderBy 即可,多字段排序时的顺序会按照 orderBy 调用顺序先后对多个字段排序 + + +使用示例 + +```js +let res = await collection.orderBy("name", "asc").get() +``` + +### 指定返回字段 + +collection.field() + +从查询结果中,过滤掉不需要的字段,或者指定要返回的字段。 + +参数说明 + +| 参数 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | --------------------------------------- | +| - | object | 是 | 过滤字段对象,包含字段名和策略,不返回传false,返回传true | + +使用示例 + +```js +collection.field({ 'age': true }) //只返回age字段,其他字段不返回 +``` +备注:只能指定要返回的字段或者不要返回的字段。即{'a': true, 'b': false}是一种错误的参数格式 + +### 查询指令 + +查询指令以dbCmd.开头,包括等于、不等于、大于、大于等于、小于、小于等于、in、nin、and、or。 + + +下面的查询指令以以下数据集为例: + +```json +// goods表 + +[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 +},{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 +},{ + "type": { + "brand": "S", + "name": "S-01", + "author": "S-01-A" + }, + "category": "book", + "quarter": "2020 Q3", + "price": 20 +}] + +``` + +#### eq 等于@dbcmd-eq + +表示字段等于某个值。`eq` 指令接受一个字面量 (literal),可以是 `number`, `boolean`, `string`, `object`, `array`。 + +事实上在uniCloud的数据库中,`等于`有两种写法。 + +比如筛选出所有2020 Q2季度的商品, + +写法1:使用`:`来比较 + +```js +const myOpenID = "xxx" +let res = await db.collection('articles').where({ + quarter: '2020 Q2' +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + }] +} +``` + +写法2:使用指令`dbcmd.eq()` + +```js +const dbCmd = db.command +const myOpenID = "xxx" +let res = await db.collection('articles').where({ + quarter: dbCmd.eq('2020 Q2') +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + }] +} +``` + +注意 `eq` 指令有更大的灵活性,可以用于表示字段等于某个对象的情况,比如: + +```js +// 这种写法表示匹配 type.brand == 'S' 且 type.name == 'S-01' +let res = await db.collection('articles').where({ + type: { + brand: 'S', + name: 'S-01' + } +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "S", + "name": "S-01", + "author": "S-01-A" + }, + "category": "book", + "quarter": "2020 Q3", + "price": 20 + }] +} + +// 这种写法表示 stat 对象等于 { brand: 'S', name: 'S-01' } +// 对象中还有其他字段时无法匹配,例如:{ brand: 'S', name: 'S-01', author: 'S-01-A' } +// 对象中字段顺序不一致也不能匹配,例如:{ name: 'S-01', brand: 'S' } +const dbCmd = db.command +let res = await db.collection('articles').where({ + stat: dbCmd.eq({ + brand: 'S', + name: 'S-01' + }) +}).get() + +// 查询返回值 +{ + "data":[] +} +``` + +#### neq 不等于@dbcmd-neq + +字段不等于。`neq` 指令接受一个字面量 (literal),可以是 `number`, `boolean`, `string`, `object`, `array`。 + +如筛选出品牌不为 X 的计算机: + +```js +const dbCmd = db.command +let res = await db.collection('goods').where({ + category: 'computer', + type: { + brand: dbCmd.neq('X') + }, +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + }] +} +``` + +#### gt 大于@dbcmd-gt + +字段大于指定值。 + +如筛选出价格大于 3000 的计算机: + +```js +const dbCmd = db.command +let res = await db.collection('goods').where({ + category: 'computer', + price: dbCmd.gt(3000) +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 + }] +} +``` + +#### gte 大于等于@dbcmd-gte + +字段大于或等于指定值。 + +#### lt 小于@dbcmd-lt + +字段小于指定值。 + +#### lte 小于等于@dbcmd-lte + +字段小于或等于指定值。 + +#### in 在数组中@dbcmd-in + +字段值在给定的数组中。 + +筛选出内存为 8g 或 16g 的计算机商品: + +```js +const dbCmd = db.command +let res = await db.collection('goods').where({ + category: 'computer', + type: { + memory: dbCmd.in([8, 16]) + } +}).get() + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + },{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 + }] +} +``` + +#### nin 不在数组中@dbcmd-nin + +字段值不在给定的数组中。 + +筛选出内存不是 8g 或 16g 的计算机商品: + +```js +const dbCmd = db.command +db.collection('goods').where({ + category: 'computer', + type: { + memory: dbCmd.nin([8, 16]) + } +}) + +// 查询返回值 +{ + "data":[] +} +``` + +#### and 且@dbcmd-and + +表示需同时满足指定的两个或以上的条件。 + +如筛选出内存大于 4g 小于 32g 的计算机商品: + +流式写法: +```js +const dbCmd = db.command +db.collection('goods').where({ + category: 'computer', + type: { + memory: dbCmd.gt(4).and(dbCmd.lt(32)) + } +}) + + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + },{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 + }] +} +``` + +前置写法: +```js +const dbCmd = db.command +db.collection('goods').where({ + category: 'computer', + type: { + memory: dbCmd.and(dbCmd.gt(4), dbCmd.lt(32)) + } +}) +``` + +#### or 或@dbcmd-or + +表示需满足所有指定条件中的至少一个。如筛选出价格小于 4000 或在 6000-8000 之间的计算机: + +流式写法: +```js +const dbCmd = db.command +db.collection('goods').where({ + category: 'computer', + type: { + price:dbCmd.lt(4000).or(dbCmd.gt(6000).and(dbCmd.lt(8000))) + } +}) + + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + },{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 + }] +} +``` + +前置写法: +```js +const dbCmd = db.command +db.collection('goods').where({ + category: 'computer', + type: { + price: dbCmd.or(dbCmd.lt(4000), dbCmd.and(dbCmd.gt(6000), dbCmd.lt(8000))) + } +}) +``` + +如果要跨字段 “或” 操作:(如筛选出内存 8g 或 cpu 3.2 ghz 的计算机) + +```js +const dbCmd = db.command +db.collection('goods').where(dbCmd.or( + { + type: { + memory: dbCmd.gt(8) + } + }, + { + type: { + cpu: 3.2 + } + } +)) + +// 查询返回值 +{ + "data":[{ + "type": { + "brand": "A", + "name": "A-01", + "memory": 16, + "cpu": 3.2 + }, + "category": "computer", + "quarter": "2020 Q2", + "price": 2500 + },{ + "type": { + "brand": "X", + "name": "X-01", + "memory": 8, + "cpu": 4.0 + }, + "category": "computer", + "quarter": "2020 Q3", + "price": 6500 + }] +} +``` + +### 正则表达式查询@regexp + +#### db.RegExp + +根据正则表达式进行筛选 + +例如下面可以筛选出 `version` 字段开头是 "数字+s" 的记录,并且忽略大小写: +```js +// 可以直接使用正则表达式 +db.collection('articles').where({ + version: /^\ds/i +}) + +// 也可以使用new RegExp +db.collection('user').where({ + name: new RegExp('^\\ds', 'i') +}) + +// 或者使用new db.RegExp,这种方式阿里云不支持 +db.collection('articles').where({ + version: new db.RegExp({ + regex: '^\\ds', // 正则表达式为 /^\ds/,转义后变成 '^\\ds' + options: 'i' // i表示忽略大小写 + }) +}) +``` + +## 删除文档 + +**方式1 通过指定文档ID删除** + +collection.doc(_id).remove() + +```js +// 清理全部数据 +let res = await collection.get() +res.data.map(async(document) => { + return await collection.doc(document.id).remove(); +}); +``` + +**方式2 条件查找文档然后直接批量删除** + +collection.where().remove() + +```js +// 删除字段a的值大于2的文档 +const dbCmd = db.command +let res = await collection.where({ + a: dbCmd.gt(2) +}).remove() + +// 清理全部数据 +const dbCmd = db.command +let res = await collection.where({ + _id: dbCmd.exists(true) +}).remove() +``` + +响应参数 + +| 字段 | 类型 | 必填 | 说明 | +| --------- | ------- | ---- | ------------------------ | +| deleted | Integer | 否 | 删除的记录数量 | + +示例:判断删除成功或失败,打印删除的记录数量 + +```js +const db = uniCloud.database(); +db.collection("table1").doc("5f79fdb337d16d0001899566").remove() + .then((res) => { + console.log("删除成功,删除条数为: ",res.deleted); + }) + .catch((err) => { + console.log( err.message ) + }) + .finally(() => { + + }) +``` + +## 更新文档 + +### 更新指定文档 + +collection.doc().update(Object data) + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ---------------------------------------- | +| data | object | 是 | 更新字段的Object,{'name': 'Ben'} _id 非必填| + +**响应参数** + +| 参数 | 类型 | 说明 | +| ---- | ------| ---------------------------------------- | +|updated| Number| 更新成功条数,数据更新前后没变化时会返回0 | + + +```js +let res = await collection.doc('doc-id').update({ + name: "Hey", + count: { + fav: 1 + } +}); +``` + +```json +// 更新前 +{ + "_id": "doc-id", + "name": "Hello", + "count": { + "fav": 0, + "follow": 0 + } +} + +// 更新后 +{ + "_id": "doc-id", + "name": "Hey", + "count": { + "fav": 1, + "follow": 0 + } +} +``` + +更新数组时,已数组下标作为key即可,比如以下示例将数组arr内下标为1的值修改为 uniCloud + +```js +let res = await collection.doc('doc-id').update({ + arr: { + 1: "uniCloud" + } +}) +``` + +```json +// 更新前 +{ + "_id": "doc-id", + "arr": ["hello", "world"] +} +// 更新后 +{ + "_id": "doc-id", + "arr": ["hello", "uniCloud"] +} +``` + +### 更新文档,如果不存在则创建 + +collection.doc().set() + +**注意:** + +- 此方法会覆写已有字段,需注意与`update`表现不同,比如以下示例执行`set`之后`follow`字段会被删除 + +```js +let res = await collection.doc('doc-id').set({ + name: "Hey", + count: { + fav: 1 + } +}) +``` + +```json +// 更新前 +{ + "_id": "doc-id", + "name": "Hello", + "count": { + "fav": 0, + "follow": 0 + } +} + +// 更新后 +{ + "_id": "doc-id", + "name": "Hey", + "count": { + "fav": 1 + } +} +``` + +### 批量更新文档 +collection.update() + +```js +const dbCmd = db.command +let res = await collection.where({name: dbCmd.eq('hey')}).update({ + age: 18, +}) +``` + +### 更新数组内指定下标的元素 + +```js +const res = await db.collection('query').doc('1').update({ + // 更新students[1] + ['students.' + 1]: { + name: 'wang' + } +}) +``` + +```js +// 更新前 +{ + "_id": "1", + "students": [ + { + "name": "zhang" + }, + { + "name": "li" + } + ] +} + +// 更新后 +{ + "_id": "1", + "students": [ + { + "name": "zhang" + }, + { + "name": "wang" + } + ] +} +``` + +### 更新数组内匹配条件的元素 + +**注意:只可确定数组内只会被匹配到一个的时候使用** + +```js +const res = await db.collection('query').where({ + 'students.id': '001' +}).update({ + // 将students内id为001的name改为li + 'students.$.name': 'li' +}) +``` + + +```js +// 更新前 +{ + "_id": "1", + "students": [ + { + "id": "001", + "name": "zhang" + }, + { + "id": "002", + "name": "wang" + } + ] +} + +// 更新后 +{ + "_id": "1", + "students": [ + { + "id": "001", + "name": "li" + }, + { + "id": "002", + "name": "wang" + } + ] +} +``` + +### 更新操作符 + +更多数据库操作符请查看[数据库操作符](#dbcmd) + +#### set + +更新指令。用于设定字段等于指定值。这种方法相比传入纯 JS 对象的好处是能够指定字段等于一个对象: + +```js +const dbCmd = db.command +let res = await db.collection('photo').doc('doc-id').update({ + count: dbCmd.set({ + fav: 1, + follow: 1 + }) +}) +``` + +```json +// 更新前 +{ + "_id": "doc-id", + "name": "Hello", + "count": { + "fav": 0, + "follow": 0 + } +} + +// 更新后 +{ + "_id": "doc-id", + "name": "Hello", + "count": { + "fav": 1, + "follow": 1 + } +} +``` + +#### inc + +更新指令。用于指示字段自增某个值,这是个原子操作,使用这个操作指令而不是先读数据、再加、再写回的好处是: + +1. 原子性:多个用户同时写,对数据库来说都是将字段加一,不会有后来者覆写前者的情况 +2. 减少一次请求:不需先读再写 + +之后的 mul 指令同理。 + +在文章阅读数+1、收藏+1等很多场景会用到它。如给收藏的商品数量加一: + +```js +const dbCmd = db.command + +let res = await db.collection('user').where({ + _id: 'my-doc-id' +}).update({ + count: { + fav: dbCmd.inc(1) + } +}) +``` + +```json +// 更新前 +{ + "_id": "my-doc-id", + "name": "Hello", + "count": { + "fav": 0, + "follow": 0 + } +} + +// 更新后 +{ + "_id": "my-doc-id", + "name": "Hello", + "count": { + "fav": 1, + "follow": 0 + } +} +``` + +请注意并没有直接提供减法操作符,如果要实现减法,仍通过inc实现。比如上述字段减1, + +```js +const dbCmd = db.command + +let res = await db.collection('user').where({ + _id: 'my-doc-id' +}).update({ + count: { + fav: dbCmd.inc(-1) + } +}) +``` + +#### mul + +更新指令。用于指示字段自乘某个值。 + +以下示例将count内的fav字段乘10 + +```js +const dbCmd = db.command + +let res = await db.collection('user').where({ + _id: 'my-doc-id' +}).update({ + count: { + fav: dbCmd.mul(10) + } +}) +``` + +```json +// 更新前 +{ + "_id": "my-doc-id", + "name": "Hello", + "count": { + "fav": 2, + "follow": 0 + } +} + +// 更新后 +{ + "_id": "my-doc-id", + "name": "Hello", + "count": { + "fav": 20, + "follow": 0 + } +} +``` + +请注意没有直接提供除法操作符,如果要实现除法,仍通过mul实现。比如上述字段除以10, + +```js +const dbCmd = db.command + +let res = await db.collection('user').where({ + _id: 'my-doc-id' +}).update({ + count: { + fav: dbCmd.mul(0.1) + } +}) +``` + +#### remove + +更新指令。用于表示删除某个字段。如某人删除了自己一条商品评价中的评分: + +```js +const dbCmd = db.command +let res = await db.collection('comments').doc('comment-id').update({ + rating: dbCmd.remove() +}) +``` + +```json +// 更新前 +{ + "_id": "comment-id", + "rating": 5, + "comment": "xxx" +} + +// 更新后 +{ + "_id": "comment-id", + "comment": "xxx" +} +``` + +#### push +向数组尾部追加元素,支持传入单个元素或数组 + +```js +const dbCmd = db.command + +let res = await db.collection('comments').doc('comment-id').update({ + // users: dbCmd.push('aaa') + users: dbCmd.push(['c', 'd']) +}) +``` + +```json +// 更新前 +{ + "_id": "comment-id", + "users": ["a","b"] +} + +// 更新后 +{ + "_id": "comment-id", + "users": ["a","b","c","d"] +} +``` + +#### pop +删除数组尾部元素 + +```js +const dbCmd = db.command + +let res = await db.collection('comments').doc('comment-id').update({ + users: dbCmd.pop() +}) +``` + +```json +// 更新前 +{ + "_id": "comment-id", + "users": ["a","b"] +} + +// 更新后 +{ + "_id": "comment-id", + "users": ["a"] +} +``` + +#### unshift +向数组头部添加元素,支持传入单个元素或数组。使用同push + +```js +const dbCmd = db.command + +let res = await db.collection('comments').doc('comment-id').update({ + // users: dbCmd.push('aaa') + users: dbCmd.unshift(['c', 'd']) +}) +``` + +```json +// 更新前 +{ + "_id": "comment-id", + "users": ["a","b"] +} + +// 更新后 +{ + "_id": "comment-id", + "users": ["c","d","a","b"] +} +``` + +#### shift +删除数组头部元素。使用同pop + +```js +const dbCmd = db.command + +let res = await db.collection('comments').doc('comment-id').update({ + users: dbCmd.shift() +}) +``` + +```json +// 更新前 +{ + "_id": "comment-id", + "users": ["a","b"] +} + +// 更新后 +{ + "_id": "comment-id", + "users": ["b"] +} +``` + +## GEO地理位置 + +注意:**如果需要对类型为地理位置的字段进行搜索,一定要建立地理位置索引**。 + +### GEO数据类型 + +#### Point + +用于表示地理位置点,用经纬度唯一标记一个点,这是一个特殊的数据存储类型。 + +签名:`Point(longitude: number, latitude: number)` + +示例: +```js +new db.Geo.Point(longitude, latitude) +``` + +#### LineString + +用于表示地理路径,是由两个或者更多的 `Point` 组成的线段。 + +签名:`LineString(points: Point[])` + +示例: + +```js +new db.Geo.LineString([ + new db.Geo.Point(lngA, latA), + new db.Geo.Point(lngB, latB), + // ... +]) +``` + +#### Polygon + +用于表示地理上的一个多边形(有洞或无洞均可),它是由一个或多个**闭环** `LineString` 组成的几何图形。 + +由一个环组成的 `Polygon` 是没有洞的多边形,由多个环组成的是有洞的多边形。对由多个环(`LineString`)组成的多边形(`Polygon`),第一个环是外环,所有其他环是内环(洞)。 + +签名:`Polygon(lines: LineString[])` + +示例: + +```js +new db.Geo.Polygon([ + new db.Geo.LineString(...), + new db.Geo.LineString(...), + // ... +]) +``` + +#### MultiPoint + +用于表示多个点 `Point` 的集合。 + +签名:`MultiPoint(points: Point[])` + +示例: + +```js +new db.Geo.MultiPoint([ + new db.Geo.Point(lngA, latA), + new db.Geo.Point(lngB, latB), + // ... +]) +``` + +#### MultiLineString + +用于表示多个地理路径 `LineString` 的集合。 + +签名:`MultiLineString(lines: LineString[])` + +示例: + +```js +new db.Geo.MultiLineString([ + new db.Geo.LineString(...), + new db.Geo.LineString(...), + // ... +]) +``` + + +#### MultiPolygon + +用于表示多个地理多边形 `Polygon` 的集合。 + +签名:`MultiPolygon(polygons: Polygon[])` + +示例: + +```js +new db.Geo.MultiPolygon([ + new db.Geo.Polygon(...), + new db.Geo.Polygon(...), + // ... +]) +``` + +### GEO操作符 + +#### geoNear + +按从近到远的顺序,找出字段值在给定点的附近的记录。 + +签名: +```js +db.command.geoNear(options: IOptions) + +interface IOptions { + geometry: Point // 点的地理位置 + maxDistance?: number // 选填,最大距离,米为单位 + minDistance?: number // 选填,最小距离,米为单位 +} +``` + + +示例: + +```js +let res = await db.collection('user').where({ + location: db.command.geoNear({ + geometry: new db.Geo.Point(lngA, latA), + maxDistance: 1000, + minDistance: 0 + }) +}).get() +``` + +#### geoWithin + +找出字段值在指定 Polygon / MultiPolygon 内的记录,无排序 + +签名: + +```js +db.command.geoWithin(IOptions) + +interface IOptions { + geometry: Polygon | MultiPolygon // 地理位置 +} +``` + +示例: + +```js +// 一个闭合的区域 +const area = new Polygon([ + new LineString([ + new Point(lngA, latA), + new Point(lngB, latB), + new Point(lngC, latC), + new Point(lngA, latA) + ]), +]) + +// 搜索 location 字段在这个区域中的 user +let res = await db.collection('user').where({ + location: db.command.geoWithin({ + geometry: area + }) +}).get() +``` + +#### geoIntersects + +找出字段值和给定的地理位置图形相交的记录 + +签名: + +```js +db.command.geoIntersects(IOptions) + +interface IOptions { + geometry: Point | LineString | MultiPoint | MultiLineString | Polygon | MultiPolygon // 地理位置 +} +``` + +示例: + +```js +// 一条路径 +const line = new LineString([ + new Point(lngA, latA), + new Point(lngB, latB) +]) + +// 搜索 location 与这条路径相交的 user +let res = await db.collection('user').where({ + location: db.command.geoIntersects({ + geometry: line + }) +}).get() +``` + + + +## 事务 + +**目前仅腾讯云支持事务,阿里云暂不支持** + +事务通常用来在某个数据库操作失败之后进行回滚。 + +### runTransaction + +发起事务。与`startTransaction`作用类似,接收参数类型不同 + +**`runTransaction` 的形式如下:** + +```javascript +db.runTransaction(callback: function, times: number) +``` + +**参数** + +|参数 |类型 |说明 | +|--- |--- |--- | +|callback |Function |事务执行函数,需为 async 异步函数或返回 Promise 的函数 | +|times |Number |事务执行最多次数,默认 3 次,成功后不重复执行,只有事务冲突时会重试,其他异常时不会重试| + +**返回值** + +`runTransaction`返回一个`Promise`,此`Promise.resolve`的结果为`callback`事务执行函数的返回值,`reject` 的结果为事务执行过程中抛出的异常或者是 `transaction.rollback` 传入的值 + +**callback 事务执行函数的说明** + +事务执行函数由开发者传入,函数接收一个参数 transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取数据库集合记录引用进行操作,rollback 方法用于在不想继续执行事务时终止并回滚事务。 + +事务执行函数必须为 `async` 异步函数或返回 `Promise` 的函数,当事务执行函数返回时,uniCloud 会认为用户逻辑已完成,自动提交(`commit`)事务,因此务必确保用户事务逻辑完成后才在 `async` 异步函数中返回或 `resolve Promise`。 + +事务执行函数可能会被执行多次,在内部发现事务冲突时会自动重复执行,如果超过设定的执行次数上限,会报错退出。 + +在事务执行函数中发生的错误,都会认为事务执行失败而抛错。 + +事务执行函数返回的值会作为 `runTransaction` 返回的 `Promise resolve` 的值,在函数中抛出的异常会作为 `runTransaction` 返回的 `Promise reject` 的值,如果事务执行函数中调用了 `transaction.rollback`,则传入 `rollback` 函数的值会作为 `runTransaction` 返回的 `Promise reject` 的值。 + +**限制** + +事务操作时为保障效率和并发性,只允许进行单记录操作,不允许进行批量操作,但可以在一个事务进行多次数据库操作。 + +**注意事项** + +开发者提供的事务执行函数正常返回时,uniCloud 会自动提交(`commit`)事务,请勿在事务执行函数内调用 `transaction.commit` 方法,该方法仅在通过 `db.startTransaction` 进行事务操作时使用 + +**示例代码** + +两个账户之间进行转账的简易示例 + +```javascript +const db = uniCloud.database() +const dbCmd = db.command +exports.main = async (event) => { + try { + const result = await db.runTransaction(async transaction => { + const aaaRes = await transaction.collection('account').doc('aaa').get() + const bbbRes = await transaction.collection('account').doc('bbb').get() + + if (aaaRes.data && bbbRes.data) { + try { + const updateAAARes = await transaction.collection('account').doc('aaa').update({ + amount: dbCmd.inc(-10) + }) + + const updateBBBRes = await transaction.collection('account').doc('bbb').update({ + amount: dbCmd.inc(10) + }) + + console.log(`transaction succeeded`) + + // 会作为 runTransaction resolve 的结果返回 + return { + aaaAccount: aaaRes.data.amount - 10, + } + } catch(e) { + // 会作为 runTransaction reject 的结果出去 + await transaction.rollback(-100) + } + } else { + // 会作为 runTransaction reject 的结果出去 + await transaction.rollback(-100) + } + }) + + return { + success: true, + aaaAccount: result.aaaAccount, + } + } catch (e) { + console.error(`transaction error`, e) + + return { + success: false, + error: e + } + } +} +``` + +### startTransaction + +发起事务。与`runTransaction`作用类似,接收参数类型不同 + +**`startTransaction` 形式如下** + +```javascript +// 与runTransaction不同,startTransaction不接收参数 +db.startTransaction() +``` + +**返回值** + +返回一个`Promise`,此`Promise resolve`的结果为事务操作对象(**注意这里与runTransaction的区别**),其上可通过 `collection API` 操作数据库,通过 `commit`(**使用`startTransaction`需要主动`commit`**) 或 `rollback` 来结束或终止事务。 + +**限制** + +事务操作时为保障效率和并发性,只允许进行单记录操作,不允许进行批量操作,但可以在一个事务进行多次数据库操作。 + +**示例代码** + +两个账户之间进行转账的简易示例 + +```javascript +const db = uniCloud.database() +const dbCmd = db.command + +exports.main = async (event) => { + const transaction = await db.startTransaction() + try { + + const aaaRes = await transaction.collection('account').doc('aaa').get() + const bbbRes = await transaction.collection('account').doc('bbb').get() + + if (aaaRes.data && bbbRes.data) { + const updateAAARes = await transaction.collection('account').doc('aaa').update({ + amount: dbCmd.inc(-10) + }) + + const updateBBBRes = await transaction.collection('account').doc('bbb').update({ + amount: dbCmd.inc(10) + }) + + await transaction.commit() + + console.log(`transaction succeeded`) + + return { + success: true, + aaaAccount: aaaRes.data.amount - 10, + } + } else { + + return { + success: false, + error: `rollback`, + rollbackCode: -100, + } + } + } catch (e) { + await transaction.rollback() + console.error(`transaction error`, e) + + return { + success: false, + error: e + } + } +} +``` + +## 聚合操作 + +有时候我们需要对数据进行分析操作,比如一些统计操作、联表查询等,这个时候简单的查询操作就搞不定这些需求,因此就需要使用聚合操作来完成。 + +获取数据库集合的聚合操作实例 + +```js +db.collection('scores').aggregate() +``` + +### 聚合表达式 + +表达式可以是字段路径、常量、或聚合操作符。表达式可以嵌套表达式。 + +**字段路径** + +表达式用字段路径表示法来指定记录中的字段。字段路径的表示由一个 `$` 符号加上字段名或嵌套字段名。嵌套字段名用点将嵌套的各级字段连接起来。如 `$profile` 就表示 `profile` 的字段路径,`$profile.name` 就表示 `profile.name` 的字段路径(`profile` 字段中嵌套的 `name` 字段)。 + +例如:现有以下数据 + +```json +[{ + "profile": { + "name": "Chloe" + }, + "status": 0 +}] +``` + +```js +// 执行以下操作 +let res = await db.collection('scores').aggregate() + .addFields({ + name: '$profile.name' + }) + .end() + +// 返回值为 +{ + "data": [{ + "profile": { + "name": "Chloe" + }, + "status": 0, + "name": "Chloe" + }] +} +``` + + +**常量** + +常量可以是任意类型。默认情况下 $ 开头的字符串都会被当做字段路径处理,如果想要避免这种行为,使用 `db.command.aggregate.literal` 声明为常量。 + +**聚合操作符** + +参考[聚合操作符](#aggregate-operator) + +### addFields + +聚合阶段。添加新字段到输出的记录。经过 `addFields` 聚合阶段,输出的所有记录中除了输入时带有的字段外,还将带有 `addFields` 指定的字段。 + + +**API 说明** + +`addFields` 等同于同时指定了所有已有字段和新增字段的 `project` 阶段。 + +**`addFields` 的形式如下:** +```js +addFields({ + <新字段>: <表达式> +}) +``` +`addFields` 可指定多个新字段,每个新字段的值由使用的表达式决定。 + +如果指定的新字段与原有字段重名,则新字段的值会覆盖原有字段的值。注意 `addFields` 不能用来给数组字段添加元素。 + +**示例 1:连续两次 addFields** + +假设集合 scores 有如下记录: +```js +{ + _id: 1, + student: "Maya", + homework: [ 10, 5, 10 ], + quiz: [ 10, 8 ], + extraCredit: 0 +} +{ + _id: 2, + student: "Ryan", + homework: [ 5, 6, 5 ], + quiz: [ 8, 8 ], + extraCredit: 8 +} +``` +应用两次 `addFields`,第一次增加两个字段分别为 `homework` 和 `quiz` 的和值,第二次增加一个字段再基于上两个和值求一次和值。 +```js +const $ = db.command.aggregate +let res = await db.collection('scores').aggregate() + .addFields({ + totalHomework: $.sum('$homework'), + totalQuiz: $.sum('$quiz') + }) + .addFields({ + totalScore: $.add(['$totalHomework', '$totalQuiz', '$extraCredit']) + }) + .end() +``` + +返回结果如下: +```js +{ + "_id" : 1, + "student" : "Maya", + "homework" : [ 10, 5, 10 ], + "quiz" : [ 10, 8 ], + "extraCredit" : 0, + "totalHomework" : 25, + "totalQuiz" : 18, + "totalScore" : 43 +} +{ + "_id" : 2, + "student" : "Ryan", + "homework" : [ 5, 6, 5 ], + "quiz" : [ 8, 8 ], + "extraCredit" : 8, + "totalHomework" : 16, + "totalQuiz" : 16, + "totalScore" : 40 +} +``` + +**示例 2:在嵌套记录里增加字段** + +可以用点表示法在嵌套记录里增加字段。假设 vehicles 集合含有如下记录: +```js +{ _id: 1, type: "car", specs: { doors: 4, wheels: 4 } } +{ _id: 2, type: "motorcycle", specs: { doors: 0, wheels: 2 } } +{ _id: 3, type: "jet ski" } +``` +可以用如下操作在 `specs` 字段下增加一个新的字段 `fuel_type`,值都设为固定字符串 `unleaded`: +```js +let res = await db.collection('vehicles').aggregate() + .addFields({ + 'spec.fuel_type': 'unleaded' + }) + .end() +``` + +返回结果如下: +```js +{ _id: 1, type: "car", + specs: { doors: 4, wheels: 4, fuel_type: "unleaded" } } +{ _id: 2, type: "motorcycle", + specs: { doors: 0, wheels: 2, fuel_type: "unleaded" } } +{ _id: 3, type: "jet ski", + specs: { fuel_type: "unleaded" } } +``` + +**示例 3:设置字段值为另一个字段** + +可以通过 `$` 加字段名组成的字符串作为值的表达式来设置字段的值为另一个字段的值。 + +同样用上一个集合示例,可以用如下操作添加一个字段 `vehicle_type`,将其值设置为 `type` 字段的值: +```js +let res = await db.collection('vehicles').aggregate() + .addFields({ + vehicle_type: '$type' + }) + .end() +``` +返回结果如下: +```js +{ _id: 1, type: "car", vehicle_type: "car", + specs: { doors: 4, wheels: 4, fuel_type: "unleaded" } } +{ _id: 2, type: "motorcycle", vehicle_type: "motorcycle", + specs: { doors: 0, wheels: 2, fuel_type: "unleaded" } } +{ _id: 3, type: "jet ski", vehicle_type: "jet ski", + specs: { fuel_type: "unleaded" } } +``` + +### bucket + +聚合阶段。将输入记录根据给定的条件和边界划分成不同的组,每组即一个 `bucket`。 + +**API 说明** + +每组分别作为一个记录输出,包含一个以下界为值的 `_id` 字段和一个以组中记录数为值的 `count` 字段。`count` 在没有指定 `output` 的时候是默认输出的。 + +`bucket` 只会在组内有至少一个记录的时候输出。 + +**bucket 的形式如下:** +```js +bucket({ + groupBy: , + boundaries: [, , ...], + default: , + output: { + : , + ... + : + } +}) +``` +`groupBy` 是一个用以决定分组的表达式,会应用在各个输入记录上。可以用 `$` 前缀加上要用以分组的字段路径来作为表达式。除非用 `default` 指定了默认值,否则每个记录都需要包含指定的字段,且字段值必须在 `boundaries` 指定的范围之内。 + +`boundaries` 是一个数组,每个元素分别是每组的下界。必须至少指定两个边界值。数组值必须是同类型递增的值。 + +`default` 可选,指定之后,没有进入任何分组的记录将都进入一个默认分组,这个分组记录的 `_id` 即由 `default` 决定。`default` 的值必须小于 `boundaries` 中的最小值或大于等于其中的最大值。`default` 的值可以与 `boundaries` 元素值类型不同。 + +`output` 可选,用以决定输出记录除了 `_id` 外还要包含哪些字段,各个字段的值必须用累加器表达式指定。当 `output` 指定时,默认的 `count` 是不会被默认输出的,必须手动指定: +```js +output: { + count: $.sum(1), + ... + : +} +``` +使用 bucket 需要满足以下至少一个条件,否则会抛出错误: + +每一个输入记录应用 groupBy 表达式获取的值都必须是一个在 boundaries 内的值 + +指定一个 default 值,该值在 boundaries 以外,或与 boundaries 元素的值不同的类型。 + +**示例** + +假设集合 items 有如下记录: +```js +{ + _id: "1", + price: 10 +} +{ + _id: "2", + price: 50 +} +{ + _id: "3", + price: 20 +} +{ + _id: "4", + price: 80 +} +{ + _id: "5", + price: 200 +} +``` + +对上述记录进行分组,将 [0, 50) 分为一组,[50, 100) 分为一组,其他分为一组: + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .bucket({ + groupBy: '$price', + boundaries: [0, 50, 100], + default: 'other', + output: { + count: $.sum(), + ids: $.push('$_id') + } + }) + .end() +``` + +返回结果如下: + +```js +[ + { + "_id": 0, + "count": 2, + "ids": [ + "1", + "3" + ] + }, + { + "_id": 50, + "count": 2, + "ids": [ + "2", + "4" + ] + }, + { + "_id": "other", + "count": 22, + "ids": [ + "5" + ] + } +] +``` + +### bucketAuto + +聚合阶段。将输入记录根据给定的条件划分成不同的组,每组即一个 `bucket`。与 `bucket` 的其中一个不同之处在于无需指定 `boundaries`,`bucketAuto` 会自动尝试将记录尽可能平均的分散到每组中。 + +**API 说明** +每组分别作为一个记录输出,包含一个以包含组中最大值和最小值两个字段的对象为值的 _id 字段和一个以组中记录数为值的 count 字段。count 在没有指定 output 的时候是默认输出的。 + +**bucketAuto 的形式如下:** +```js +bucketAuto({ + groupBy: , + buckets: , + granularity: , + output: { + : , + ... + : + } +}) +``` +`groupBy` 是一个用以决定分组的表达式,会应用在各个输入记录上。可以用 $ 前缀加上要用以分组的字段路径来作为表达式。除非用 `default` 指定了默认值,否则每个记录都需要包含指定的字段,且字段值必须在 `boundaries` 指定的范围之内。 + +`buckets` 是一个用于指定划分组数的正整数。 + +`granularity` 是可选枚举值字符串,用于保证自动计算出的边界符合给定的规则。这个字段仅可在所有 `groupBy` 值都是数字并且没有 `NaN` 的情况下使用。枚举值包括:`R5、R10、R20、R40、R80、1-2-5、E6、E12、E24、E48、E96、E192、POWERSOF2`。 + +`output` 可选,用以决定输出记录除了 `_id` 外还要包含哪些字段,各个字段的值必须用累加器表达式指定。当 `output` 指定时,默认的 `count` 是不会被默认输出的,必须手动指定: +```js +output: { + count: $.sum(1), + ... + : +} +``` +在以下情况中,输出的分组可能会小于给定的组数: + +输入记录数少于分组数 +- `groupBy` 计算得到的唯一值少于分组数 +- `granularity` 的间距少于分组数 +- `granularity` 不够精细以至于不能平均分配到各组 + +**granularity 详细说明** + +`granularity` 用于保证边界值属于一个给定的数字序列。 + +**Renard 序列** + +Renard 序列是以 10 的 5 / 10 / 20 / 40 / 80 次方根来推导的、在 1.0 到 10.0 (如果是 R80 则是 10.3) 之间的数字序列。 + +设置 granularity 为 R5 / R10 / R20 / R40 / R80 就把边界值限定在序列内。如果 groupBy 的值不在 1.0 到 10.0 (如果是 R80 则是 10.3) 内,则序列数字会自动乘以 10。 + +**E 序列** + +E 序列是以 10 的 6 / 12 / 24 / 48 / 96 / 192 次方跟来推导的、带有一个特定误差的、在 1.0 到 10.0 之间的数字序列。 + +**1-2-5 序列** + +1-2-5 序列 表现与三值 Renard 序列一样。 + +**2的次方序列** + +由 2 的各次方组成的序列数字。 + +**示例** + +假设集合 items 有如下记录: +```js +{ + _id: "1", + price: 10.5 +} +{ + _id: "2", + price: 50.3 +} +{ + _id: "3", + price: 20.8 +} +{ + _id: "4", + price: 80.2 +} +{ + _id: "5", + price: 200.3 +} +``` +对上述记录进行自动分组,分成三组: +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .bucketAuto({ + groupBy: '$price', + buckets: 3, + }) + .end() +``` +返回结果如下: +```js +{ + "_id": { + "min": 10.5, + "max": 50.3 + }, + "count": 2 +} +{ + "_id": { + "min": 50.3, + "max": 200.3 + }, + "count": 2 +} +{ + "_id": { + "min": 200.3, + "max": 200.3 + }, + "count": 1 +} +``` + +### count + +聚合阶段。计算上一聚合阶段输入到本阶段的记录数,输出一个记录,其中指定字段的值为记录数。 + +**API 说明** + +**count 的形式如下:** +```js +count() +``` + 是输出记录数的字段的名字,不能是空字符串,不能以 $ 开头,不能包含 . 字符。 + +count 阶段等同于 group + project 的操作: +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .group({ + _id: null, + count: $.sum(1), + }) + .project({ + _id: 0, + }) + .end() +``` +上述操作会输出一个包含 count 字段的记录。 + +**示例** + +假设集合 items 有如下记录: +```js +{ + _id: "1", + price: 10.5 +} +{ + _id: "2", + price: 50.3 +} +{ + _id: "3", + price: 20.8 +} +{ + _id: "4", + price: 80.2 +} +{ + _id: "5", + price: 200.3 +} +``` +找出价格大于 50 的记录数: +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .match({ + price: $.gt(50) + }) + .count('expensiveCount') + .end() +``` +返回结果如下: +```js +{ + "expensiveCount": 3 +} +``` + +### geoNear + +聚合阶段。将记录按照离给定点从近到远输出。 + +|属性 |类型 |默认值 |必填 |说明 | +|---- |---- |---- |---- |---- | +|near |GeoPoint | |是 |GeoJSON Point,用于判断距离的点 | +|spherical |true | |是 |必填,值为 true | +|limit |number | |否 |限制返回记录数 | +|maxDistance |number | |否 |距离最大值 | +|minDistance |number | |否 |距离最小值 | +|query |Object | |否 |要求记录必须同时满足该条件(语法同 where) | +|distanceMultiplier |number | |否 |返回时在距离上乘以该数字 | +|distanceField |string | |是 |存放距离的输出字段名,可以用点表示法表示一个嵌套字段 | +|includeLocs |string | |否 |列出要用于距离计算的字段,如果记录中有多个字段都是地理位置时有用 | +|key |string | |否 |选择要用的地理位置索引。如果集合由多个地理位置索引,则必须指定一个,指定的方式是指定对应的字段 | + +**API 说明** + +- `geoNear` 必须为第一个聚合阶段 +- 必须有地理位置索引。如果有多个,必须用 `key` 参数指定要使用的索引。 + +**示例** + +假设集合 attractions 有如下记录: +```js +{ + "_id": "geoNear.0", + "city": "Guangzhou", + "docType": "geoNear", + "location": { + "type": "Point", + "coordinates": [ + 113.30593, + 23.1361155 + ] + }, + "name": "Canton Tower" +}, +{ + "_id": "geoNear.1", + "city": "Guangzhou", + "docType": "geoNear", + "location": { + "type": "Point", + "coordinates": [ + 113.306789, + 23.1564721 + ] + }, + "name": "Baiyun Mountain" +}, +{ + "_id": "geoNear.2", + "city": "Beijing", + "docType": "geoNear", + "location": { + "type": "Point", + "coordinates": [ + 116.3949659, + 39.9163447 + ] + }, + "name": "The Palace Museum" +}, +{ + "_id": "geoNear.3", + "city": "Beijing", + "docType": "geoNear", + "location": { + "type": "Point", + "coordinates": [ + 116.2328567, + 40.242373 + ] + }, + "name": "Great Wall" +} +``` +```js +const $ = db.command.aggregate +let res = await db.collection('attractions').aggregate() + .geoNear({ + distanceField: 'distance', // 输出的每个记录中 distance 即是与给定点的距离 + spherical: true, + near: new db.Geo.Point(113.3089506, 23.0968251), + query: { + docType: 'geoNear', + }, + key: 'location', // 若只有 location 一个地理位置索引的字段,则不需填 + includeLocs: 'location', // 若只有 location 一个是地理位置,则不需填 + }) + .end() +``` + +返回结果如下: +```js +{ + "_id": "geoNear.0", + "location": { + "type": "Point", + "coordinates": [ + 113.30593, + 23.1361155 + ] + }, + "docType": "geoNear", + "name": "Canton Tower", + "city": "Guangzhou", + "distance": 4384.68131486958 +}, +{ + "_id": "geoNear.1", + "city": "Guangzhou", + "location": { + "type": "Point", + "coordinates": [ + 113.306789, + 23.1564721 + ] + }, + "docType": "geoNear", + "name": "Baiyun Mountain", + "distance": 6643.521654040738 +}, +{ + "_id": "geoNear.2", + "docType": "geoNear", + "name": "The Palace Museum", + "city": "Beijing", + "location": { + "coordinates": [ + 116.3949659, + 39.9163447 + ], + "type": "Point" + }, + "distance": 1894750.4414538583 +}, +{ + "_id": "geoNear.3", + "docType": "geoNear", + "name": "Great Wall", + "city": "Beijing", + "location": { + "type": "Point", + "coordinates": [ + 116.2328567, + 40.242373 + ] + }, + "distance": 1928300.3308822548 +} +``` + +### group + +聚合阶段。将输入记录按给定表达式分组,输出时每个记录代表一个分组,每个记录的 _id 是区分不同组的 key。输出记录中也可以包括累计值,将输出字段设为累计值即会从该分组中计算累计值。 + +使用group可以很方便的实现类似SQL的distinct功能 + +**API 说明** + +**group 的形式如下:** +```js +group({ + _id: , + : , + ... + : +}) +``` + +`_id` 参数是必填的,如果填常量则只有一组。其他字段是可选的,都是累计值,用 `$.sum` 等累计器(`const $ = db.command.aggregate`),但也可以使用其他表达式。 + +累计器必须是以下操作符之一: + +详细使用方法见[累计器操作符](#累计器操作符) + +|操作符 |说明 | +|---- |---- | +|addToSet |向数组中添加值,如果数组中已存在该值,不执行任何操作 | +|avg |返回一组集合中,指定字段对应数据的平均值 | +|sum |计算并且返回一组字段所有数值的总和 | +|first |返回指定字段在一组集合的第一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 | +|last |返回指定字段在一组集合的最后一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 | +|max |返回一组数值的最大值 | +|min |返回一组数值的最小值 | +|push |在 group 阶段,返回一组中表达式指定列与对应的值,一起组成的数组 | +|stdDevPop |返回一组字段对应值的标准差 | +|stdDevSamp |计算输入值的样本标准偏差。如果输入值代表数据总体,或者不概括更多的数据,请改用 db.command.aggregate.stdDevPop| +|mergeObjects |将多个文档合并为单个文档 | + +**内存限制** + +该阶段有 100M 内存使用限制。 + +**示例 1:按字段值分组** + +假设集合 avatar 有如下记录: +```js +{ + _id: "1", + alias: "john", + region: "asia", + scores: [40, 20, 80], + coins: 100 +} +{ + _id: "2", + alias: "arthur", + region: "europe", + scores: [60, 90], + coins: 20 +} +{ + _id: "3", + alias: "george", + region: "europe", + scores: [50, 70, 90], + coins: 50 +} +{ + _id: "4", + alias: "john", + region: "asia", + scores: [30, 60, 100, 90], + coins: 40 +} +{ + _id: "5", + alias: "george", + region: "europe", + scores: [20], + coins: 60 +} +{ + _id: "6", + alias: "john", + region: "asia", + scores: [40, 80, 70], + coins: 120 +} +``` +```js +const $ = db.command.aggregate +let res = await db.collection('avatar').aggregate() + .group({ + _id: '$alias', + num: $.sum(1) + }) + .end() +``` +返回结果如下: +```js +{ + "_id": "john", + "num": 3 +} +{ + "_id": "authur", + "num": 1 +} +{ + "_id": "george", + "num": 2 +} +``` + +**示例 2:按多个值分组** + +可以给 _id 传入记录的方式按多个值分组。还是沿用上面的示例数据,按各个区域(region)获得相同最高分(score)的来分组,并求出各组虚拟币(coins)的总量: +```js +const $ = db.command.aggregate +let res = await db.collection('avatar').aggregate() + .group({ + _id: { + region: '$region', + maxScore: $.max('$scores') + }, + totalCoins: $.sum('$coins') + }) + .end() +``` +返回结果如下: +```js +{ + "_id": { + "region": "asia", + "maxScore": 80 + }, + "totalCoins": 220 +} +{ + "_id": { + "region": "asia", + "maxScore": 100 + }, + "totalCoins": 100 +} +{ + "_id": { + "region": "europe", + "maxScore": 90 + }, + "totalCoins": 70 +} +{ + "_id": { + "region": "europe", + "maxScore": 20 + }, + "totalCoins": 60 +} +``` + +### limit + +聚合阶段。限制输出到下一阶段的记录数。 + +**示例** + +假设集合 items 有如下记录: +```js +{ + _id: "1", + price: 10 +} +{ + _id: "2", + price: 50 +} +{ + _id: "3", + price: 20 +} +{ + _id: "4", + price: 80 +} +{ + _id: "5", + price: 200 +} +``` +返回价格大于 20 的记录的最小的两个记录: +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .match({ + price: $.gt(20) + }) + .sort({ + price: 1, + }) + .limit(2) + .end() +``` +返回结果如下: +```js +{ + "_id": "3", + "price": 20 +} +{ + "_id": "4", + "price": 80 +} +``` + +### lookup + +聚合阶段。联表查询。与同个数据库下的一个指定的集合做 left outer join(左外连接)。对该阶段的每一个输入记录,lookup 会在该记录中增加一个数组字段,该数组是被联表中满足匹配条件的记录列表。lookup 会将连接后的结果输出给下个阶段。 + +**API 说明** + +`lookup` 有两种使用方式 + +#### 相等匹配 +将输入记录的一个字段和被连接集合的一个字段进行相等匹配时,采用以下定义: +```js +lookup({ + from: <要连接的集合名>, + localField: <输入记录的要进行相等匹配的字段>, + foreignField: <被连接集合的要进行相等匹配的字段>, + as: <输出的数组字段名> +}) +``` + +**参数详细说明** + +|参数字段 |说明 | +|---- |---- | +|from |要进行连接的另外一个集合的名字 | +|localField |当前流水线的输入记录的字段名,该字段将被用于与 from 指定的集合的 foreignField 进行相等匹配。如果输入记录中没有该字段,则该字段的值在匹配时会被视作 null| +|foreignField |被连接集合的字段名,该字段会被用于与 localField 进行相等匹配。如果被连接集合的记录中没有该字段,该字段的值将在匹配时被视作 null | +|as |指定连接匹配出的记录列表要存放的字段名,这个数组包含的是匹配出的来自 from 集合的记录。如果输入记录中本来就已有该字段,则该字段会被覆写 | + +这个操作等价于以下伪 SQL 操作: + +``` +SELECT *, +FROM collection +WHERE IN (SELECT * + FROM + WHERE = ); +``` + +**例子:** + +- 指定一个相等匹配条件 +- 对数组字段应用相等匹配 +- 组合 mergeObjects 应用相等匹配 + +#### 自定义连接条件、拼接子查询 + +如果需要指定除相等匹配之外的连接条件,或指定多个相等匹配条件,或需要拼接被连接集合的子查询结果,那可以使用如下定义: +```js +lookup({ + from: <要连接的集合名>, + let: { <变量1>: <表达式1>, ..., <变量n>: <表达式n> }, + pipeline: [ <在要连接的集合上进行的流水线操作> ], + as: <输出的数组字段名> +}) +``` + +**参数详细说明** + +|参数字段 |说明 | +|---- |---- | +|from |要进行连接的另外一个集合的名字 | +|let |可选。指定在 pipeline 中可以使用的变量,变量的值可以引用输入记录的字段,比如 let: { userName: '$name' } 就代表将输入记录的 name 字段作为变量 userName 的值。在 pipeline 中无法直接访问输入记录的字段,必须通过 let 定义之后才能访问,访问的方式是在 expr 操作符中用 $$变量名 的方式访问,比如 $$userName。 | +|pipeline |指定要在被连接集合中运行的聚合操作。如果要返回整个集合,则该字段取值空数组 []。在 pipeline 中无法直接访问输入记录的字段,必须通过 let 定义之后才能访问,访问的方式是在 expr 操作符中用 $$变量名 的方式访问,比如 $$userName。 | +|as |指定连接匹配出的记录列表要存放的字段名,这个数组包含的是匹配出的来自 from 集合的记录。如果输入记录中本来就已有该字段,则该字段会被覆写 | + +该操作等价于以下伪 SQL 语句: +``` +SELECT *, +FROM collection +WHERE IN (SELECT + FROM + WHERE ); +``` + +**例子** + +- 指定多个连接条件 +- 拼接被连接集合的子查询 + +**示例** + +**指定一个相等匹配条件** + +假设 orders 集合有以下记录: +```js +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1}, + {"_id":6} +] +``` +books 集合有以下记录: +```js +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"}, + {"_id":"book2","author":"author 2","category":"novel","stock":20,"title":"novel 2"}, + {"_id":"book5","author":"author 4","category":"science","stock":50,"title":null}, + {"_id":"book6","author":"author 5","category":"novel","stock":"60"} +] +``` +以下聚合操作可以通过一个相等匹配条件连接 `orders` 和 `books` 集合,匹配的字段是 `orders` 集合的 `book` 字段和 `books` 集合的 title 字段: +```js +const db = uniCloud.database() +let res = await db.collection('orders').aggregate() + .lookup({ + from: 'books', + localField: 'book', + foreignField: 'title', + as: 'bookList', + }) + .end() +``` +结果: +```js +[ + { + "_id": 4, + "book": "novel 1", + "price": 30, + "quantity": 2, + "bookList": [ + { + "_id": "book1", + "title": "novel 1", + "author": "author 1", + "category": "novel", + "stock": 10 + } + ] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "author": "author 3", + "stock": 30 + } + ] + }, + { + "_id": 6, + "bookList": [ + { + "_id": "book5", + "category": "science", + "author": "author 4", + "stock": 50, + "title": null + }, + { + "_id": "book6", + "author": "author 5", + "stock": "60", + "category": "novel" + } + ] + } +] +``` +对数组字段应用相等匹配 +假设 authors 集合有以下记录: +```js +[ + {"_id": 1, "name": "author 1", "intro": "Two-time best-selling sci-fiction novelist"}, + {"_id": 3, "name": "author 3", "intro": "UCB assistant professor"}, + {"_id": 4, "name": "author 4", "intro": "major in CS"} +] +``` +books 集合有以下记录: +```js +[ + {"_id":"book1","authors":["author 1"],"category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","authors":["author 3", "author 4"],"category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","authors":["author 3"],"category":"science","stock":40,"title":"science 2"} +] +``` +以下操作获取作者信息及他们分别发表的书籍,使用了 lookup 操作匹配 authors 集合的 name 字段和 books 集合的 authors 数组字段: +```js +const db = cloud.database() +let res = await db.collection('authors').aggregate() + .lookup({ + from: 'books', + localField: 'name', + foreignField: 'authors', + as: 'publishedBooks', + }) + .end() +``` +结果 +```js +[ + { + "_id": 1, + "intro": "Two-time best-selling sci-fiction novelist", + "name": "author 1", + "publishedBooks": [ + { + "_id": "book1", + "title": "novel 1", + "category": "novel", + "stock": 10, + "authors": [ + "author 1" + ] + } + ] + }, + { + "_id": 3, + "name": "author 3", + "intro": "UCB assistant professor", + "publishedBooks": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "stock": 30, + "authors": [ + "author 3", + "author 4" + ] + }, + { + "_id": "book4", + "title": "science 2", + "category": "science", + "stock": 40, + "authors": [ + "author 3" + ] + } + ] + }, + { + "_id": 4, + "intro": "major in CS", + "name": "author 4", + "publishedBooks": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "stock": 30, + "authors": [ + "author 3", + "author 4" + ] + } + ] + } +] +``` + +**组合 mergeObjects 应用相等匹配** + +假设 `orders` 集合有以下记录: +```js +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1}, + {"_id":6} +] +``` +`books` 集合有以下记录: +```js +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"}, + {"_id":"book2","author":"author 2","category":"novel","stock":20,"title":"novel 2"}, + {"_id":"book5","author":"author 4","category":"science","stock":50,"title":null}, + {"_id":"book6","author":"author 5","category":"novel","stock":"60"} +] +``` +以下操作匹配 orders 的 book 字段和 books 的 title 字段,并将 books 匹配结果直接 merge 到 orders 记录中。 +```js +var db = cloud.database() +var $ = db.command.aggregate +let res = await db.collection('orders').aggregate() + .lookup({ + from: "books", + localField: "book", + foreignField: "title", + as: "bookList" + }) + .replaceRoot({ + newRoot: $.mergeObjects([ $.arrayElemAt(['$bookList', 0]), '$$ROOT' ]) + }) + .project({ + bookList: 0 + }) + .end() +``` +结果 +```js +[ + { + "_id": 4, + "title": "novel 1", + "author": "author 1", + "category": "novel", + "stock": 10, + "book": "novel 1", + "price": 30, + "quantity": 2 + }, + { + "_id": 5, + "category": "science", + "title": "science 1", + "author": "author 3", + "stock": 30, + "book": "science 1", + "price": 20, + "quantity": 1 + }, + { + "_id": 6, + "category": "science", + "author": "author 4", + "stock": 50, + "title": null + } +] +``` + +**指定多个连接条件** + +假设 `orders` 集合有以下记录: +```js +[ + {"_id":4,"book":"novel 1","price":300,"quantity":20}, + {"_id":5,"book":"science 1","price":20,"quantity":1} +] +``` +`books` 集合有以下记录: +```js +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"} +] +``` +以下操作连接 `orders` 和 `books` 集合,要求两个条件: + +- orders 的 book 字段与 books 的 title 字段相等 +- books 的 stock 字段 大于或等于 orders 的 quantityorders 字段 +```js +const db = cloud.database() +const dbCmd = db.command +const $ = dbCmd.aggregate +let res = await db.collection('orders').aggregate() + .lookup({ + from: 'books', + let: { + order_book: '$book', + order_quantity: '$quantity' + }, + pipeline: $.pipeline() + .match(dbCmd.expr($.and([ + $.eq(['$title', '$$order_book']), + $.gte(['$stock', '$$order_quantity']) + ]))) + .project({ + _id: 0, + title: 1, + author: 1, + stock: 1 + }) + .done(), + as: 'bookList', + }) + .end() +``` +结果: +```js +[ + { + "_id": 4, + "book": "novel 1", + "price": 300, + "quantity": 20, + "bookList": [] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + } + ] + } +] +``` + +**拼接被连接集合的子查询** + +假设 `orders` 集合有以下记录: +```js +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1} +] +``` +`books` 集合有以下记录: +```js +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"} +] +``` +在每条输出记录上加上一个数组字段,该数组字段的值是对 books 集合的一个查询语句的结果: +```js +const db = cloud.database() +const $ = db.command.aggregate +let res = await db.collection('orders').aggregate() + .lookup({ + from: 'books', + let: { + order_book: '$book', + order_quantity: '$quantity' + }, + pipeline: $.pipeline() + .match({ + author: 'author 3' + }) + .project({ + _id: 0, + title: 1, + author: 1, + stock: 1 + }) + .done(), + as: 'bookList', + }) + .end() +``` +结果 +```js +[ + { + "_id": 4, + "book": "novel 1", + "price": 30, + "quantity": 20, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + }, + { + "title": "science 2", + "author": "author 3", + "stock": 40 + } + ] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + }, + { + "title": "science 2", + "author": "author 3", + "stock": 40 + } + ] + } +] +``` + +### match + +聚合阶段。根据条件过滤文档,并且把符合条件的文档传递给下一个流水线阶段。 + +**API 说明** + +**match 的形式如下:** +```js +match(<查询条件>) +``` + +查询条件与普通查询一致,可以用普通查询操作符,注意 match 阶段和其他聚合阶段不同,不可使用聚合操作符,只能使用查询操作符。 +```js +// 直接使用字符串 +match({ + name: 'Tony Stark' +}) +``` +```js +// 使用操作符 +const dbCmd = db.command +match({ + age: dbCmd.gt(18) +}) +``` + +**示例** + +假设集合 articles 有如下记录: +```js +{ "_id" : "1", "author" : "stark", "score" : 80 } +{ "_id" : "2", "author" : "stark", "score" : 85 } +{ "_id" : "3", "author" : "bob", "score" : 60 } +{ "_id" : "4", "author" : "li", "score" : 55 } +{ "_id" : "5", "author" : "jimmy", "score" : 60 } +{ "_id" : "6", "author" : "li", "score" : 94 } +{ "_id" : "7", "author" : "justan", "score" : 95 } +``` + +**匹配** + +下面是一个直接匹配的例子: +```js +let res = await db.collection('articles') + .aggregate() + .match({ + author: 'stark' + }) + .end() +``` +这里的代码尝试找到所有 author 字段是 stark 的文章,那么匹配如下: +```js +{ "_id" : "1", "author" : "stark", "score" : 80 } +{ "_id" : "2", "author" : "stark", "score" : 85 } +``` + +**计数** + +match 过滤出文档后,还可以与其他流水线阶段配合使用。 + +比如下面这个例子,我们使用 group 进行搭配,计算 score 字段大于 80 的文档数量: +```js +const dbCmd = db.command +const $ = dbCmd.aggregate +let res = await db.collection('articles') + .aggregate() + .match({ + score: dbCmd.gt(80) + }) + .group({ + _id: null, + count: $.sum(1) + }) + .end() +``` +返回值如下: +```js +{ "_id" : null, "count" : 3 } +``` + +### project + +聚合阶段。把指定的字段传递给下一个流水线,指定的字段可以是某个已经存在的字段,也可以是计算出来的新字段。 + +**API 说明** + +**project 的形式如下:** +```js +project({ + <表达式> +}) +``` + +表达式可以有以下格式: + +|格式 |说明 | +|---- |---- | +|<字段>: <1 或 true> |指定包含某个已有字段 | +|_id: <0 或 false> |舍弃 _id 字段 | +|<字段>: <表达式> |加入一个新字段,或者重置某个已有字段 | +|<字段>: <0 或 false> |舍弃某个字段(如果你指定舍弃了某个非 _id 字段,那么在此次 project 中,你不能再使用其它表达式) | + +**指定包含字段** + +_id 字段是默认包含在输出中的,除此之外其他任何字段,如果想要在输出中体现的话,必须在 project 中指定; 如果指定包含一个尚不存在的字段,那么 project 会忽略这个字段,不会加入到输出的文档中; + +**指定排除字段** + +如果你在 project 中指定排除某个字段,那么其它字段都会体现在输出中; 如果指定排除的是非 _id 字段,那么在本次 project 中,不能再使用其它表达式; + +**加入新的字段或重置某个已有字段** + +你可以使用一些特殊的表达式加入新的字段,或重置某个已有字段。 + +**多层嵌套的字段** + +有时有些字段处于多层嵌套的底层,我们可以使用点记法: +```js +"contact.phone.number": <1 or 0 or 表达式> +``` +也可以直接使用嵌套的格式: +```js +contact: { phone: { number: <1 or 0 or 表达式> } } +``` + +**示例** + +假设我们有一个 articles 集合,其中含有以下文档: +```js +{ + "_id": 666, + "title": "This is title", + "author": "Nobody", + "isbn": "123456789", + "introduction": "......" +} +``` +**指定包含某些字段** + +下面的代码使用 project,让输出只包含 _id、title 和 author 字段: +```js +let res = await db.collection('articles') + .aggregate() + .project({ + title: 1, + author: 1 + }) + .end() +``` + +输出如下: +```js +{ "_id" : 666, "title" : "This is title", "author" : "Nobody" } +``` + +**去除输出中的 _id 字段** + +_id 是默认包含在输出中的,如果不想要它,可以指定去除它: +```js +let res = await db.collection('articles') + .aggregate() + .project({ + _id: 0, // 指定去除 _id 字段 + title: 1, + author: 1 + }) + .end() +``` +输出如下: +```js +{ "title" : "This is title", "author" : "Nobody" } +``` + +**去除某个非 _id 字段** + +我们还可以指定在输出中去掉某个非 _id 字段,这样其它字段都会被输出: +```js +let res = await db.collection('articles') + .aggregate() + .project({ + isbn: 0, // 指定去除 isbn 字段 + }) + .end() +``` +输出如下,相比输入,没有了 isbn 字段: +```js +{ + "_id" : 666, + "title" : "This is title", + "author" : "Nobody", + "introduction": "......" +} +``` + +**加入计算出的新字段** + +假设我们有一个 students 集合,其中包含以下文档: +```js +{ + "_id": 1, + "name": "小明", + "scores": { + "chinese": 80, + "math": 90, + "english": 70 + } +} +``` +下面的代码,我们使用 project,在输出中加入了一个新的字段 totalScore: +```js +const { sum } = db.command.aggregate +let res = await db.collection('students') + .aggregate() + .project({ + _id: 0, + name: 1, + totalScore: sum([ + "$scores.chinese", + "$scores.math", + "$scores.english" + ]) + }) + .end() +``` +输出为: +```js +{ "name": "小明", "totalScore": 240 } +``` + +**加入新的数组字段** + +假设我们有一个 points 集合,包含以下文档: +```js +{ "_id": 1, "x": 1, "y": 1 } +{ "_id": 2, "x": 2, "y": 2 } +{ "_id": 3, "x": 3, "y": 3 } +``` + +下面的代码,我们使用 project,把 x 和 y 字段,放入到一个新的数组字段 coordinate 中: +```js +let res = await db.collection('points') + .aggregate() + .project({ + coordinate: ["$x", "$y"] + }) + .end() +``` +输出如下: +```js +{ "_id": 1, "coordinate": [1, 1] } +{ "_id": 2, "coordinate": [2, 2] } +{ "_id": 3, "coordinate": [3, 3] } +``` + +### replaceRoot + +聚合阶段。指定一个已有字段作为输出的根节点,也可以指定一个计算出的新字段作为根节点。 + +**API 说明** + +**replaceRoot 使用形式如下:** +```js +replaceRoot({ + newRoot: <表达式> +}) +``` +表达式格式如下: + +|格式 |说明 | +|---- |---- | +|<字段名> |指定一个已有字段作为输出的根节点(如果字段不存在则报错) | +|<对象> |计算一个新字段,并且把这个新字段作为根节点 | + +**示例** + +**使用已有字段作为根节点** + +假设我们有一个 schools 集合,内容如下: +```js +{ + "_id": 1, + "name": "SFLS", + "teachers": { + "chinese": 22, + "math": 18, + "english": 21, + "other": 123 + } +} +``` +下面的代码使用 replaceRoot,把 teachers 字段作为根节点输出: +```js +let res = await db.collection('schools') + .aggregate() + .replaceRoot({ + newRoot: '$teachers' + }) + .end() +``` +输出如下: +```js +{ + "chinese": 22, + "math": 18, + "english": 21, + "other": 123 +} +``` +**使用计算出的新字段作为根节点** + +假设我们有一个 roles 集合,内容如下: +```js +{ "_id": 1, "first_name": "四郎", "last_name": "黄" } +{ "_id": 2, "first_name": "邦德", "last_name": "马" } +{ "_id": 3, "first_name": "牧之", "last_name": "张" } +``` +下面的代码使用 replaceRoot,把 first_name 和 last_name 拼在一起: +```js +const { concat } = db.command.aggregate +let res = await db.collection('roles') + .aggregate() + .replaceRoot({ + newRoot: { + full_name: concat(['$last_name', '$first_name']) + } + }) + .end() +``` +输出如下: +```js +{ "full_name": "黄四郎" } +{ "full_name": "马邦德" } +{ "full_name": "张牧之" } +``` + +### sample + +聚合阶段。随机从文档中选取指定数量的记录。 + +**API 说明** + +**sample 的形式如下:** +```js +sample({ + size: <正整数> +}) +``` +请注意:size 是正整数,否则会出错。 + +**示例** + +假设文档 users 有以下记录: +```js +{ "name": "a" } +{ "name": "b" } +``` + +**随机选取** + +如果现在进行抽奖活动,需要选出一名幸运用户。那么 sample 的调用方式如下: +```js +let res = await db.collection('users') + .aggregate() + .sample({ + size: 1 + }) + .end() +``` + +返回了随机选中的一个用户对应的记录,结果如下: + +```js +{ "_id": "696529e4-7e82-4e7f-812e-5144714edff6", "name": "b" } +``` + +### skip + +聚合阶段。指定一个正整数,跳过对应数量的文档,输出剩下的文档。 + +**示例** +```js +let res = await db.collection('users') + .aggregate() + .skip(5) + .end() +``` + +这段代码会跳过查找到的前 5 个文档,并且把剩余的文档输出。 + +### sort + +聚合阶段。根据指定的字段,对输入的文档进行排序。 + +**API 说明** + +**形式如下:** +```js +sort({ + <字段名1>: <排序规则>, + <字段名2>: <排序规则>, +}) +``` + +<排序规则>可以是以下取值: + +- 1 代表升序排列(从小到大); +- -1 代表降序排列(从大到小); + +**示例** + +升序/降序排列 + +假设我们有集合 articles,其中包含数据如下: +```js +{ "_id": "1", "author": "stark", "score": 80, "age": 18 } +{ "_id": "2", "author": "bob", "score": 60, "age": 18 } +{ "_id": "3", "author": "li", "score": 55, "age": 19 } +{ "_id": "4", "author": "jimmy", "score": 60, "age": 22 } +{ "_id": "5", "author": "justan", "score": 95, "age": 33 } +``` +```js +let res = await db.collection('articles') + .aggregate() + .sort({ + age: -1, + score: -1 + }) + .end() +``` +上面的代码在 students 集合中进行聚合搜索,并且将结果排序,首先根据 age 字段降序排列,然后再根据 score 字段进行降序排列。 + +输出结果如下: +```js +{ "_id": "5", "author": "justan", "score": 95, "age": 33 } +{ "_id": "4", "author": "jimmy", "score": 60, "age": 22 } +{ "_id": "3", "author": "li", "score": 55, "age": 19 } +{ "_id": "1", "author": "stark", "score": 80, "age": 18 } +{ "_id": "2", "author": "bob", "score": 60, "age": 18 } +``` + +### sortByCount + +聚合阶段。根据传入的表达式,将传入的集合进行分组(group)。然后计算不同组的数量,并且将这些组按照它们的数量进行排序,返回排序后的结果。 + +**API 说明** + +**sortByCount 的调用方式如下:** +```js +sortByCount(<表达式>) +``` + +表达式的形式是:$ + 指定字段。请注意:不要漏写 $ 符号。 + +**示例** + +**统计基础类型** + +假设集合 passages 的记录如下: +```js +{ "category": "Web" } +{ "category": "Web" } +{ "category": "Life" } +``` +下面的代码就可以统计文章的分类信息,并且计算每个分类的数量。即对 category 字段执行 sortByCount 聚合操作。 +```js +let res = await db.collection('passages') + .aggregate() + .sortByCount('$category') + .end() +``` + +返回的结果如下所示:Web 分类下有2篇文章,Life 分类下有1篇文章。 +```js +{ "_id": "Web", "count": 2 } +{ "_id": "Life", "count": 1 } +``` +**解构数组类型** + +假设集合 passages 的记录如下:tags 字段对应的值是数组类型。 +```js +{ "tags": [ "JavaScript", "C#" ] } +{ "tags": [ "Go", "C#" ] } +{ "tags": [ "Go", "Python", "JavaScript" ] } +``` +如何统计文章的标签信息,并且计算每个标签的数量?因为 tags 字段对应的数组,所以需要借助 unwind 操作解构 tags 字段,然后再调用 sortByCount。 + +下面的代码实现了这个功能: +```js +let res = await db.collection('passages') + .aggregate() + .unwind(`$tags`) + .sortByCount(`$tags`) + .end() +``` +返回的结果如下所示: +```js +{ "_id": "Go", "count": 2 } +{ "_id": "C#", "count": 2 } +{ "_id": "JavaScript", "count": 2 } +{ "_id": "Python", "count": 1 } +``` + +### unwind + +聚合阶段。使用指定的数组字段中的每个元素,对文档进行拆分。拆分后,文档会从一个变为一个或多个,分别对应数组的每个元素。 + +**API 说明** + +使用指定的数组字段中的每个元素,对文档进行拆分。拆分后,文档会从一个变为一个或多个,分别对应数组的每个元素。 + +**unwind 有两种使用形式:** + +**参数是一个字段名** +```js +unwind(<字段名>) +``` +**参数是一个对象** +```js +unwind({ + path: <字段名>, + includeArrayIndex: , + preserveNullAndEmptyArrays: +}) +``` + +|字段 |类型 |说明 | +|---- |---- |---- | +|path |string |想要拆分的数组的字段名,需要以 $ 开头。 | +|includeArrayIndex |string |可选项,传入一个新的字段名,数组索引会保存在这个新的字段上。新的字段名不能以 $ 开头。 | +|preserveNullAndEmptyArrays |boolean|如果为 true,那么在 path 对应的字段为 null、空数组或者这个字段不存在时,依然会输出这个文档;如果为 false,unwind 将不会输出这些文档。默认为 false。| + +**示例** + +**拆分数组** + +假设我们有一个 products 集合,包含数据如下: +```js +{ "_id": "1", "product": "tshirt", "size": ["S", "M", "L"] } +{ "_id": "2", "product": "pants", "size": [] } +{ "_id": "3", "product": "socks", "size": null } +{ "_id": "4", "product": "trousers", "size": ["S"] } +{ "_id": "5", "product": "sweater", "size": ["M", "L"] } +``` + +我们根据 size 字段对这些文档进行拆分 +```js +db.collection('products') + .aggregate() + .unwind('$size') + .end() +``` + +输出如下: +```js +{ "_id": "1", "product": "tshirt", "size": "S" } +{ "_id": "1", "product": "tshirt", "size": "M" } +{ "_id": "1", "product": "tshirt", "size": "L" } +{ "_id": "4", "product": "trousers", "size": "S" } +{ "_id": "5", "product": "sweater", "size": "M" } +{ "_id": "5", "product": "sweater", "size": "L" } +``` + +**拆分后,保留原数组的索引** + +我们根据 size 字段对文档进行拆分后,想要保留原数组索引在新的 index 字段中。 +```js +let res = await db.collection('products') + .aggregate() + .unwind({ + path: '$size', + includeArrayIndex: 'index' + }) + .end() +``` +输出如下: +```js +{ "_id": "1", "product": "tshirt", "size": "S", "index": 0 } +{ "_id": "1", "product": "tshirt", "size": "M", "index": 1 } +{ "_id": "1", "product": "tshirt", "size": "L", "index": 2 } +{ "_id": "4", "product": "trousers", "size": "S", "index": 0 } +{ "_id": "5", "product": "sweater", "size": "M", "index": 0 } +{ "_id": "5", "product": "sweater", "size": "L", "index": 1 } +``` + +**保留字段为空的文档** + +注意到我们的集合中有两行特殊的空值数据: +```js +... +{ "_id": "2", "product": "pants", "size": [] } +{ "_id": "3", "product": "socks", "size": null } +... +``` +如果想要在输出中保留 size 为空数组、null,或者 size 字段不存在的文档,可以使用 preserveNullAndEmptyArrays 参数 +```js +let res = await db.collection('products') + .aggregate() + .unwind({ + path: '$size', + preserveNullAndEmptyArrays: true + }) + .end() +``` +输出如下: +```js +{ "_id": "1", "product": "tshirt", "size": "S" } +{ "_id": "1", "product": "tshirt", "size": "M" } +{ "_id": "1", "product": "tshirt", "size": "L" } +{ "_id": "2", "product": "pants", "size": null } +{ "_id": "3", "product": "socks", "size": null } +{ "_id": "4", "product": "trousers", "size": "S" } +{ "_id": "5", "product": "sweater", "size": "M" } +{ "_id": "5", "product": "sweater", "size": "L" } +``` + +### end + +标志聚合操作定义完成,发起实际聚合操作 + +**返回值** + +Promise.<Object> + +|属性 |类型 |说明 | +|--- |--- |--- | +|list |Array.<any>|聚合结果列表 | + +**示例代码** +```js +const $ = db.command.aggregate +let res = await db.collection('books').aggregate() + .group({ + // 按 category 字段分组 + _id: '$category', + // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值 + avgSales: $.avg('$sales') + }) + .end() +``` + +## 数据库操作符@dbcmd + +### 查询·逻辑操作符 + +#### and + +查询操作符,用于表示逻辑 "与" 的关系,表示需同时满足多个查询筛选条件 + + +##### 使用说明 + `and` 有两种使用情况: + + + +**1. 用在根查询条件** + + 此时需传入多个查询条件,表示需同时满足提供的多个完整查询条件 + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where(dbCmd.and([ + { + progress: dbCmd.gt(50) + }, + { + tags: 'cloud' + } +])).get() +``` +但以上用 `and` 组成的查询条件是不必要的,因为传入的对象的各字段隐式组成了 “与” 的关系,上述条件等价于下方更简洁的写法: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.gt(50), + tags: 'cloud' +}).get() +``` +通常需要显示使用 `and` 是用在有跨字段或操作的时候,如以下表示 “progress 字段大于 50 或 tags 字段等于 cloud 或 tags 数组字段(如果 tags 是数组)中含有 cloud”: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where(dbCmd.and([ + dbCmd.or({ + progress: dbCmd.gt(50) + }), + dbCmd.or({ + tags: 'cloud' + }) +])).get() +``` + + +**2. 用在字段查询条件** + + 需传入多个查询操作符或常量,表示字段需满足或匹配给定的条件。 + + 如以下用前置写法的方式表示 "progress 字段值大于 50 且小于 100" + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.and(dbCmd.gt(50), dbCmd.lt(100)) +}).get() +``` +还可以用后置写法的方式表示同样的条件: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.gt(50).and(dbCmd.lt(100)) +}).get() +``` +注意 `Command` 默认也可以直接链式调用其他 `Command`,默认表示多个 `Command` 的与操作,因此上述代码还可以精简为: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.gt(50).lt(100) +}).get() +``` + +##### 调用风格 + 方法接收两种传参方式,一是传入一个数组参数,二是传入多个参数,效果一样。 + + +```js +// 传入数组 +function and(expressions: Expression[]): Command +// 传入多参数 +function and(...expressions: Expression[]): Command +``` + +#### or + +查询操作符,用于表示逻辑 "或" 的关系,表示需同时满足多个查询筛选条件。或指令有两种用法,一是可以进行字段值的 “或” 操作,二是也可以进行跨字段的 “或” 操作。 + + +##### 字段值的或操作 + 字段值的 “或” 操作指的是指定一个字段值为多个值之一即可。 + + 如筛选出进度大于 80 或小于 20 的 todo: + + 流式写法: + + +```js +let res = await const dbCmd = db.command +db.collection('todo').where({ + progress: dbCmd.gt(80).or(dbCmd.lt(20)) +}).get() +``` +前置写法: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.or(dbCmd.gt(80), dbCmd.lt(20)) +}).get() +``` +前置写法也可接收一个数组: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.or([dbCmd.gt(80), dbCmd.lt(20)]) +}).get() +``` + +##### 跨字段的或操作 + 跨字段的 “或” 操作指条件 “或”,相当于可以传入多个 where 语句,满足其中一个即可。 + + 如筛选出进度大于 80 或已标为已完成的 todo: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where(dbCmd.or([ + { + progress: dbCmd.gt(80) + }, + { + done: true + } +])).get() +``` + +##### 调用风格 + 方法接收两种传参方式,一是传入一个数组参数,二是传入多个参数,效果一样。 + + +```js +// 传入数组 +function or(expressions: Expression[]): Command +// 传入多参数 +function or(...expressions: Expression[]): Command +``` + +#### not + +查询操作符,用于表示逻辑 "非" 的关系,表示需不满足指定的条件。 + + +##### 示例 + 如筛选出进度不等于100的 todo: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.not(dbCmd.eq(100)) +}).get() +``` +`not` 也可搭配其他逻辑指令使用,包括 `and`, `or`, `nor`, `not`,如 `or`: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.not(dbCmd.or([dbCmd.lt(50), dbCmd.eq(100)])) +}).get() +``` + +#### nor + +查询操作符,用于表示逻辑 "都不" 的关系,表示需不满足指定的所有条件。如果记录中没有对应的字段,则默认满足条件。 + + +##### 示例 1 + 筛选出进度既不小于20又不大于80的 todo : + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.nor([dbCmd.lt(20), dbCmd.gt(80)]) +}).get() +``` +以上同时会筛选出不存在 `progress` 字段的记录,如果要要求 `progress` 字段存在,可以用 `exists` 指令: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where({ + progress: dbCmd.exists().nor([dbCmd.lt(20), dbCmd.gt(80)]) + // 等价于以下非链式调用的写法: + // progress: dbCmd.exists().and(dbCmd.nor([dbCmd.lt(20), dbCmd.gt(80)])) +}).get() +``` + +##### 示例 2 + 筛选出 `progress` 不小于 20 且 `tags` 数组不包含 `miniprogram` 字符串的记录: + + +```js +const dbCmd = db.command +db.collection('todo').where(dbCmd.nor([{ + progress: dbCmd.lt(20), +}, { + tags: 'miniprogram', +}])).get() +``` +以上会筛选出满足以下条件之一的记录: + + +1. `progress` 不小于 20 且 `tags` 数组不包含 `miniprogram` 字符串 3. `progress` 不小于 20 且 `tags` 字段不存在 5. `progress` 字段不存在 且 `tags` 数组不包含 `miniprogram` 字符串 7. `progress` 不小于 20 且 `tags` 字段不存在 + 如果要求 `progress` 和 `tags` 字段存在,可以用 `exists` 指令: + + +```js +const dbCmd = db.command +let res = await db.collection('todo').where( + dbCmd.nor([{ + progress: dbCmd.lt(20), + }, { + tags: 'miniprogram', + }]) + .and({ + progress: dbCmd.exists(true), + tags: dbCmd.exists(true), + }) +).get() +``` + +##### 调用风格 + 方法接收两种传参方式,一是传入一个数组参数,二是传入多个参数,效果一样。 + + +```js +// 传入数组 +function nor(expressions: Expression[]): Command +// 传入多参数 +function nor(...expressions: Expression[]): Command +``` + +### 查询·比较操作符 + +#### eq + +查询筛选条件,表示字段等于某个值。`eq` 指令接受一个字面量 (literal),可以是 `number`, `boolean`, `string`, `object`, `array`, `Date`。 + + +##### 使用说明 + 比如筛选出所有自己发表的文章,除了用传对象的方式: + + +```js +const openID = 'xxx' +let res = await db.collection('articles').where({ + _openid: openID +}).get() +``` +还可以用指令: + + +```js +const dbCmd = db.command +const openID = 'xxx' +let res = await db.collection('articles').where({ + _openid: dbCmd.eq(openid) +}).get() +``` +注意 `eq` 指令比对象的方式有更大的灵活性,可以用于表示字段等于某个对象的情况,比如: + + +```js +// 这种写法表示匹配 stat.publishYear == 2018 且 stat.language == 'zh-CN' +let res = await db.collection('articles').where({ + stat: { + publishYear: 2018, + language: 'zh-CN' + } +}).get() +// 这种写法表示 stat 对象等于 { publishYear: 2018, language: 'zh-CN' } +const dbCmd = db.command +let res = await db.collection('articles').where({ + stat: dbCmd.eq({ + publishYear: 2018, + language: 'zh-CN' + }) +}).get() +``` + +#### neq + +查询筛选条件,表示字段不等于某个值。`eq` 指令接受一个字面量 (literal),可以是 `number`, `boolean`, `string`, `object`, `array`, `Date`。 + + +##### 使用说明 + 表示字段不等于某个值,和 [eq](Command.eq.html) 相反 + +#### lt + +查询筛选操作符,表示需小于指定值。可以传入 `Date` 对象用于进行日期比较。 + + +##### 示例代码 + 找出进度小于 50 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.lt(50) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +#### lte + +查询筛选操作符,表示需小于或等于指定值。可以传入 `Date` 对象用于进行日期比较。 + + +##### 示例代码 + 找出进度小于或等于 50 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.lte(50) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +#### gt + +查询筛选操作符,表示需大于指定值。可以传入 `Date` 对象用于进行日期比较。 + + +##### 示例代码 + 找出进度大于 50 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.gt(50) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +#### gte + +查询筛选操作符,表示需大于或等于指定值。可以传入 `Date` 对象用于进行日期比较。 + + +##### 示例代码 + 找出进度大于或等于 50 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.gte(50) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +#### in + +查询筛选操作符,表示要求值在给定的数组内。 + + +##### 示例代码 + 找出进度为 0 或 100 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.in([0, 100]) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +#### nin + +查询筛选操作符,表示要求值不在给定的数组内。 + + +##### 示例代码 + 找出进度不是 0 或 100 的 todo + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.nin([0, 100]) +}) +.get({ + success: console.log, + fail: console.error +}) +``` + +### 查询·字段操作符 + +#### exists + +判断字段是否存在 + + +##### 示例代码 + 找出存在 tags 字段的记录 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + tags: dbCmd.exists(true) +}) +.get() +``` + +#### mod + +查询筛选操作符,给定除数 divisor 和余数 remainder,要求字段作为被除数时 value % divisor = remainder。 + + +##### 示例代码 + 找出进度为 10 的倍数的字段的记录 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + progress: dbCmd.mod(10, 0) +}) +.get() +``` + +### 查询·数组操作符 + +#### all + +数组查询操作符。用于数组字段的查询筛选条件,要求数组字段中包含给定数组的所有元素。 + + +##### 示例代码 1:普通数组 + 找出 tags 数组字段同时包含 cloud 和 database 的记录 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + tags: dbCmd.all(['cloud', 'database']) +}) +.get() +``` + +##### 示例代码 2:对象数组 + 如果数组元素是对象,则可以用 `dbCmd.elemMatch` 匹配对象的部分字段 + + 假设有字段 `places` 定义如下: + + +```js +{ + "type": string + "area": number + "age": number +} +``` +找出数组字段中至少同时包含一个满足 “area 大于 100 且 age 小于 2” 的元素和一个满足 “type 为 mall 且 age 大于 5” 的元素 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + places: dbCmd.all([ + dbCmd.elemMatch({ + area: dbCmd.gt(100), + age: dbCmd.lt(2), + }), + dbCmd.elemMatch({ + type: 'mall', + age: dbCmd.gt(5), + }), + ]), +}) +.get() +``` + +#### elemMatch + +用于数组字段的查询筛选条件,要求数组中包含至少一个满足 `elemMatch` 给定的所有条件的元素 + + +##### 示例代码:数组是对象数组的情况 + 假设集合示例数据如下: + + +```js +{ + "_id": "a0", + "city": "x0", + "places": [{ + "type": "garden", + "area": 300, + "age": 1 + }, { + "type": "theatre", + "area": 50, + "age": 15 + }] +} +``` +找出 `places` 数组字段中至少同时包含一个满足 “area 大于 100 且 age 小于 2” 的元素 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + places: dbCmd.elemMatch({ + area: dbCmd.gt(100), + age: dbCmd.lt(2), + }) +}) +.get() +``` +*注意**:如果不使用 `elemMatch` 而直接如下指定条件,则表示的是 `places` 数组字段中至少有一个元素的 `area` 字段大于 100 且 `places` 数组字段中至少有一个元素的 `age` 字段小于 2: + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + places: { + area: dbCmd.gt(100), + age: dbCmd.lt(2), + } +}) +.get() +``` + +##### 示例代码:数组元素都是普通数据类型的情况 + 假设集合示例数据如下: + + +```js +{ + "_id": "a0", + "scores": [60, 80, 90] +} +``` +找出 `scores` 数组字段中至少同时包含一个满足 “大于 80 且小于 100” 的元素 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + scores: dbCmd.elemMatch(dbCmd.gt(80).lt(100)) +}) +.get() +``` + +#### size + +更新操作符,用于数组字段的查询筛选条件,要求数组长度为给定值 + + +##### 示例 + 找出 tags 数组字段长度为 2 的所有记录 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').where({ + places: dbCmd.size(2) +}) +.get() +``` + +### 查询·地理位置操作符 + +#### geoNear + +按从近到远的顺序,找出字段值在给定点的附近的记录。 + + +##### 索引要求 + 需对查询字段建立地理位置索引 + + +##### 示例代码 + 找出离给定位置 1 公里到 5 公里范围内的记录 + + +```js +const dbCmd = db.command +let res = await db.collection('restaurants').where({ + location: dbCmd.geoNear({ + geometry: new db.Geo.Point(113.323809, 23.097732), + minDistance: 1000, + maxDistance: 5000, + }) +}).get() +``` + +#### geoWithin + +找出字段值在指定区域内的记录,无排序。指定的区域必须是多边形(Polygon)或多边形集合(MultiPolygon)。 + + +##### 索引要求 + 需对查询字段建立地理位置索引 + + +##### 示例代码 1:给定多边形 + +```js +const dbCmd = db.command +const { Point, LineString, Polygon } = db.Geo +let res = await .collection('restaurants').where({ + location: dbCmd.geoWithin({ + geometry: new Polygon([ + new LineString([ + new Point(0, 0), + new Point(3, 2), + new Point(2, 3), + new Point(0, 0) + ]) + ]), + }) +}).get() +``` + +##### 示例代码 2:给定圆形 + 可以不用 `geometry` 而用 `centerSphere` 构建一个圆形。 + + `centerSphere` 对应的值的定义是:`[ [经度, 纬度], 半径 ]` + + 半径需以弧度计,比如需要 10km 的半径,则用距离除以地球半径 6378.1km 得出的数字。 + + +```js +const dbCmd = db.command +let res = await db.collection('restaurants').where({ + location: dbCmd.geoWithin({ + centerSphere: [ + [-88, 30], + 10 / 6378.1, + ] + }) +}).get() +``` + +#### geoIntersects + +找出给定的地理位置图形相交的记录 + + +##### 索引要求 + 需对查询字段建立地理位置索引 + + +##### 示例代码:找出和一个多边形相交的记录 + +```js +const dbCmd = db.command +const { Point, LineString, Polygon } = db.Geo +let res = await db.collection('restaurants').where({ + location: dbCmd.geoIntersects({ + geometry: new Polygon([ + new LineString([ + new Point(0, 0), + new Point(3, 2), + new Point(2, 3), + new Point(0, 0) + ]) + ]), + }) +}).get() +``` + +### 查询·表达式操作符 + +#### expr + +查询操作符,用于在查询语句中使用聚合表达式,方法接收一个参数,该参数必须为聚合表达式 + +##### 使用说明 + +1. `expr` 可用于在聚合 [`match`](../aggregate/Aggregate.match.html) 流水线阶段中引入聚合表达式 3. 如果聚合 [`match`](../aggregate/Aggregate.match.html) 阶段是在 [`lookup`](../aggregate/Aggregate.lookup.html) 阶段内,此时的 `expr` 表达式内可使用 `lookup` 中使用 `let` 参数定义的变量,具体示例可见 [`lookup`](../aggregate/Aggregate.lookup.html) 的 `指定多个连接条件` 例子 5. `expr` 可用在普通查询语句(`where`)中引入聚合表达式 + +##### 示例代码 1:比较同一个记录中的两个字段 + 假设 `items` 集合的数据结构如下: + + +```js +{ + "_id": string, + "inStock": number, // 库存量 + "ordered": number // 被订量 +} +``` +找出被订量大于库存量的记录: + + +```js +const dbCmd = db.command +const $ = dbCmd.aggregate +let res = await db.collection('items').where(dbCmd.expr($.gt('$ordered', '$inStock'))).get() +``` + +##### 示例代码 2:与条件语句组合使用 + 假设 `items` 集合的数据结构如下: + + +```json +{ + "_id": string, + "price": number +} +``` +假设加个小于等于 10 的打 8 折,大于 10 的打 5 折,让数据库查询返回打折后价格小于等于 8 的记录: + + +```js +const dbCmd = db.command +const $ = dbCmd.aggregate +let res = await db.collection('items').where(dbCmd.expr( + $.lt( + $.cond({ + if: $.gte('$price', 10), + then: $.multiply(['$price', '0.5']), + else: $.multiply(['$price', '0.8']), + }) + , + 8 + ) +).get() +``` + +### 更新·字段操作符 + +#### set + +更新操作符,用于设定字段等于指定值。 + + +##### 使用说明 + 这种方法相比传入纯 JS 对象的好处是能够指定字段等于一个对象 + + +##### 示例 + +```js +// 以下方法只会更新 style.color 为 red,而不是将 style 更新为 { color: 'red' },即不影响 style 中的其他字段 +let res = await db.collection('todos').doc('doc-id').update({ + style: { + color: 'red' + } +}) + +// 以下方法更新 style 为 { color: 'red', size: 'large' } +let res = await db.collection('todos').doc('doc-id').update({ + style: dbCmd.set({ + color: 'red', + size: 'large' + }) +}) +``` + +#### remove + +更新操作符,用于表示删除某个字段。 + + +##### 示例代码 + 删除 style 字段: + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('todo-id').update({ + style: dbCmd.remove() +}) +``` + +#### inc + +更新操作符,原子操作,用于指示字段自增 + + +##### 原子自增 + 多个用户同时写,对数据库来说都是将字段自增,不会有后来者覆写前者的情况 + + +##### 示例代码 + 将一个 todo 的进度自增 10: + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('todo-id').update({ + progress: dbCmd.inc(10) +}) +``` + +#### mul + +更新操作符,原子操作,用于指示字段自乘某个值 + + +##### 原子自乘 + 多个用户同时写,对数据库来说都是将字段自乘,不会有后来者覆写前者的情况 + + +##### 示例代码 + 将一个 todo 的进度自乘 10: + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('todo-id').update({ + progress: dbCmd.mul(10) +}) +``` + +#### min + +更新操作符,给定一个值,只有该值小于字段当前值才进行更新。 + + +##### 示例代码 + 如果字段 progress > 50,则更新到 50 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: dbCmd.min(50) +}) +``` + +#### max + +更新操作符,给定一个值,只有该值大于字段当前值才进行更新。 + + +##### 示例代码 + 如果字段 progress < 50,则更新到 50 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: dbCmd.max(50) +}) +``` + +#### rename + +更新操作符,字段重命名。如果需要对嵌套深层的字段做重命名,需要用点路径表示法。不能对嵌套在数组里的对象的字段进行重命名。 + + +##### 示例 1:重命名顶层字段 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: dbCmd.rename('totalProgress') +}) +``` + +##### 示例 2:重命名嵌套字段 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + someObject: { + someField: dbCmd.rename('someObject.renamedField') + } +}) +``` + +或: + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + 'someObject.someField': dbCmd.rename('someObject.renamedField') +}) +``` + +### 更新·数组操作符 + +#### push + +数组更新操作符。对一个值为数组的字段,往数组添加一个或多个值。或字段原为空,则创建该字段并设数组为传入值。 + + +##### 参数说明 + + +**position 说明** + + 要求必须同时有 `each` 参数存在。 + + 非负数代表从数组开始位置数的位置,从 0 开始计。如果数值大于等于数组长度,则视为在尾部添加。负数代表从数组尾部倒数的位置,比如 -1 就代表倒数第二个元素的位置。如果负数数值的绝对值大于等于数组长度,则视为从数组头部添加。 + + + +**sort 说明** + + 要求必须同时有 `each` 参数存在。给定 1 代表升序,-1 代表降序。 + + 如果数组元素是记录,则用 `{ <字段>: 1 | -1 }` 的格式表示根据记录中的什么字段做升降序排序。 + + + +**slice** 说明** + + 要求必须同时有 `each` 参数存在 + +|值 |说明 | +|:-: |:-: | +|0 |将字段更新为空数组 | +|正数 |数组只保留前 n 个元素| +|负数 |数组只保留后 n 个元素| + + +##### 示例 1:尾部添加元素 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push(['mini-program', 'cloud']) +}) +``` + +##### 示例 2:从第二个位置开始插入 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: ['mini-program', 'cloud'], + position: 1, + }) +}) +``` + +##### 示例 3:排序 + +插入后对整个数组做排序 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: ['mini-program', 'cloud'], + sort: 1, + }) +}) +``` + +不插入,只对数组做排序 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: [], + sort: 1, + }) +}) +``` +如果字段是对象数组,可以如下根据元素对象里的字段进行排序: + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: [ + { name: 'miniprogram', weight: 8 }, + { name: 'cloud', weight: 6 }, + ], + sort: { + weight: 1, + }, + }) +}) +``` + +##### 示例 4:截断保留 + 插入后只保留后 2 个元素 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: ['mini-program', 'cloud'], + slice: -2, + }) +}) +``` + +##### 示例 5:在指定位置插入、然后排序、最后只保留前 2 个元素 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.push({ + each: ['mini-program', 'cloud'], + position: 1, + slice: 2, + sort: 1, + }) +}) +``` + +#### pop + +数组更新操作符,对一个值为数组的字段,将数组尾部元素删除,仅可以删除末尾一个 + +##### 示例代码 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.pop() +}) +``` + +#### unshift + +数组更新操作符,对一个值为数组的字段,往数组头部添加一个或多个值。或字段原为空,则创建该字段并设数组为传入值。 + + +##### 示例代码 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.unshift(['mini-program', 'cloud']) +}) +``` + +#### shift + +数组更新操作符,对一个值为数组的字段,将数组头部元素删除。 + + +##### 示例代码 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.shift() +}) +``` + +#### pull + +数组更新操作符。给定一个值或一个查询条件,将数组中所有匹配给定值或查询条件的元素都移除掉。 + +##### 示例代码 1:根据常量匹配移除 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.pull('database') +}) +``` + +##### 示例代码 2:根据查询条件匹配移除 + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.pull(dbCmd.in(['database', 'cloud'])) +}) +``` + +##### 示例代码 3:对象数组时,根据查询条件匹配移除 + 假设有字段 `places` 数组中的元素结构如下 + + +```json +{ + "type": string + "area": number + "age": number +} +``` + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + places: dbCmd.pull({ + area: dbCmd.gt(100), + age: dbCmd.lt(2), + }) +}) +``` + +##### 示例代码 4:有嵌套对象的对象数组时,根据查询条件匹配移除 + 假设有字段 `cities` 数组中的元素结构如下 + + +```json +{ + "name": string + "places": Place[] +} +``` +`Place` 结构如下: + + +```json +{ + "type": string + "area": number + "age": number +} +``` +可用 `elemMatch` 匹配嵌套在对象数组里面的对象数组字段 places + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + cities: dbCmd.pull({ + places: dbCmd.elemMatch({ + area: dbCmd.gt(100), + age: dbCmd.lt(2), + }) + }) +}) +``` + +#### pullAll + +数组更新操作符。给定一个值或一个查询条件,将数组中所有匹配给定值的元素都移除掉。跟 `pull` 的差别在于只能指定常量值、传入的是数组。 + + +##### 示例代码:根据常量匹配移除 + 从 tags 中移除所有 database 和 cloud 字符串 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.pullAll(['database', 'cloud']) +}) +``` + +#### addToSet + +数组更新操作符。原子操作。给定一个或多个元素,除非数组中已存在该元素,否则添加进数组。 + + +##### 示例代码 1:添加一个元素 + 如果 tags 数组中不包含 database,添加进去 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.addToSet('database') +}) +``` + +##### 示例代码 2:添加多个元素 + +需传入一个对象,其中有一个字段 `each`,其值为数组,每个元素就是要添加的元素 + + +```js +const dbCmd = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: dbCmd.addToSet({ + each: ['database', 'cloud'] + }) +}) +``` + +## 聚合操作符@aggregate-operator + +### 算术操作符 + +#### abs + +聚合操作符。返回一个数字的绝对值。 + +##### API 说明 + +语法如下: + + +```js +db.command.aggregate.abs() +``` +`abs` 传入的值除了数字常量外,也可以是任何最终解析成一个数字的表达式。 + + 如果表达式解析为 `null` 或者指向一个不存在的字段,则 `abs` 的结果是 `null`。如果值解析为 `NaN`,则结果是 `NaN`。 + + +##### 示例代码 + 假设集合 `ratings` 有如下记录: + + +```json +{ _id: 1, start: 5, end: 8 } +{ _id: 2, start: 4, end: 4 } +{ _id: 3, start: 9, end: 7 } +{ _id: 4, start: 6, end: 7 } +``` +··· +可以用如下方式求得各个记录的 `start` 和 `end` 之间的绝对差异大小: + + +```js +const $ = db.command.aggregate +let res = await db.collection('ratings').aggregate() + .project({ + delta: $.abs($.subtract(['$start', '$end'])) + }) + .end() +``` +返回结果如下: + + +```json +{ "_id" : 1, "delta" : 3 } +{ "_id" : 2, "delta" : 0 } +{ "_id" : 3, "delta" : 2 } +{ "_id" : 4, "delta" : 1 } +``` + +#### add + +聚合操作符。将数字相加或将数字加在日期上。如果数组中的其中一个值是日期,那么其他值将被视为毫秒数加在该日期上。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.add([<表达式1>, <表达式2>, ...]) +``` +表达式可以是形如 `$ + 指定字段`,也可以是普通字符串。只要能够被解析成字符串即可。 + + +##### 示例代码 + 假设集合 `staff` 有如下记录: + + +```json +{ _id: 1, department: "x", sales: 5, engineer: 10, lastUpdate: ISODate("2019-05-01T00:00:00Z") } +{ _id: 2, department: "y", sales: 10, engineer: 20, lastUpdate: ISODate("2019-05-01T02:00:00Z") } +{ _id: 3, department: "z", sales: 20, engineer: 5, lastUpdate: ISODate("2019-05-02T03:00:00Z") } +``` + + +**数字求和** + + 可以用如下方式求得各个记录人数总数: + + +```js +const $ = db.command.aggregate +let res = await db.collection('staff').aggregate() + .project({ + department: 1, + total: $.add(['$sales', '$engineer']) + }) + .end() +``` +返回结果如下: + + +```json +{ _id: 1, department: "x", total: 15 } +{ _id: 2, department: "y", total: 30 } +{ _id: 3, department: "z", total: 25 } +``` + + +**增加日期值** + + 如下操作可以获取各个记录的 `lastUpdate` 加一个小时之后的值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('staff').aggregate() + .project({ + department: 1, + lastUpdate: $.add(['$lastUpdate', 60*60*1000]) + }) + .end() +``` +返回结果如下: + + +```json +{ _id: 1, department: "x", lastUpdate: ISODate("2019-05-01T01:00:00Z") } +{ _id: 2, department: "y", lastUpdate: ISODate("2019-05-01T03:00:00Z") } +{ _id: 3, department: "z", lastUpdate: ISODate("2019-05-02T04:00:00Z") } +``` + +#### ceil + +聚合操作符。向上取整,返回大于或等于给定数字的最小整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.ceil() +``` +`` 可以是任意解析为数字的表达式。如果表达式解析为 `null` 或指向一个不存在的字段,则返回 `null`,如果解析为 `NaN`,则返回 `NaN`。 + + +##### 示例代码 + 假设集合 `sales` 有如下记录: + + +```json +{ _id: 1, sales: 5.2 } +{ _id: 2, sales: 1.32 } +{ _id: 3, sales: -3.2 } +``` +可以用如下方式取各个数字的向上取整值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('sales').aggregate() + .project({ + sales: $.ceil('$sales') + }) + .end() +``` +返回结果如下: + + +```json +{ _id: 1, sales: 6 } +{ _id: 2, sales: 2 } +{ _id: 3, sales: -3 } +``` + +#### divide + +聚合操作符。传入被除数和除数,求商。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.divide([<被除数表达式>, <除数表达式>]) +``` +表达式可以是任意解析为数字的表达式。 + + +##### 示例代码 + 假设集合 `railroads` 有如下记录: + + +```js +{ _id: 1, meters: 5300 } +{ _id: 2, meters: 64000 } +{ _id: 3, meters: 130 } +``` +可以用如下方式取各个数字转换为千米之后的值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('railroads').aggregate() + .project({ + km: $.divide(['$meters', 1000]) + }) + .end() +``` +返回结果如下: + + +```js +{ _id: 1, km: 5.3 } +{ _id: 2, km: 64 } +{ _id: 3, km: 0.13 } +``` + +#### exp + +聚合操作符。取 e(自然对数的底数,欧拉数) 的 n 次方。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.exp() +``` +`` 可以是任意解析为数字的表达式。如果表达式解析为 `null` 或指向一个不存在的字段,则返回 `null`,如果解析为 `NaN`,则返回 `NaN`。 + + +##### 示例代码 + 假设集合 `math` 有如下记录: + + +```js +{ _id: 1, exp: 0 } +{ _id: 2, exp: 1 } +{ _id: 3, exp: 2 } +``` + +```js +const $ = db.command.aggregate +let res = await db.collection('math').aggregate() + .project({ + result: $.exp('$exp') + }) + .end() +``` +返回结果如下: + + +```js +{ _id: 1, result: 1 } +{ _id: 2, result: 2.71828182845905 } +{ _id: 3, result: 7.38905609893065 } +``` + +#### floor + +聚合操作符。向下取整,返回大于或等于给定数字的最小整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.floor() +``` +`` 可以是任意解析为数字的表达式。如果表达式解析为 `null` 或指向一个不存在的字段,则返回 `null`,如果解析为 `NaN`,则返回 `NaN`。 + + +##### 示例代码 + 假设集合 `sales` 有如下记录: + + +```js +{ _id: 1, sales: 5.2 } +{ _id: 2, sales: 1.32 } +{ _id: 3, sales: -3.2 } +``` +可以用如下方式取各个数字的向下取整值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('sales').aggregate() + .project({ + sales: $.floor('$sales') + }) + .end() +``` +返回结果如下: + + +```js +{ _id: 1, sales: 5 } +{ _id: 2, sales: 1 } +{ _id: 3, sales: -6 } +``` + +#### ln + +聚合操作符。计算给定数字在自然对数值。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.ln() +``` +`` 可以是任意解析为非负数字的表达式。 + + `ln` 等价于 `log([, Math.E])`,其中 `Math.E` 是 `JavaScript` 获取 `e` 的值的方法。 + + +##### 示例代码 + +##### db.command.aggregate.ln + 聚合操作符。计算给定数字在自然对数值。 + + 语法如下: + + +```js +db.command.aggregate.ln() +``` +`` 可以是任意解析为非负数字的表达式。 + + `ln` 等价于 `log([, Math.E])`,其中 `Math.E` 是 `JavaScript` 获取 `e` 的值的方法。 + +#### log + +聚合操作符。计算给定数字在给定对数底下的 log 值。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.log([, ]) +``` +`` 可以是任意解析为非负数字的表达式。`` 可以是任意解析为大于 1 的数字的表达式。 + + 如果任一参数解析为 `null` 或指向任意一个不存在的字段,`log` 返回 `null`。如果任一参数解析为 `NaN`,`log` 返回 `NaN`。 + + +##### 示例代码 + 假设集合 `curve` 有如下记录: + + +```js +{ _id: 1, x: 1 } +{ _id: 2, x: 2 } +{ _id: 3, x: 3 } +``` +计算 `log2(x)` 的值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('curve').aggregate() + .project({ + log: $.log(['$x', 2]) + }) + .end() +``` +返回结果如下: + + +```js +{ _id: 1, log: 0 } +{ _id: 2, log: 1 } +{ _id: 3, log: 1.58496250072 } +``` + +#### log10 + +聚合操作符。计算给定数字在对数底为 10 下的 log 值。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.log() +``` +`` 可以是任意解析为非负数字的表达式。 + + `log10` 等同于 `log` 方法的第二个参数固定为 10。 + + +##### 示例代码 + +##### db.command.aggregate.log10 + 聚合操作符。计算给定数字在对数底为 10 下的 log 值。 + + 语法如下: + + +```js +db.command.aggregate.log() +``` +`` 可以是任意解析为非负数字的表达式。 + + `log10` 等同于 `log` 方法的第二个参数固定为 10。 + +#### mod + +聚合操作符。取模运算,取数字取模后的值。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.mod([, ]) +``` +第一个数字是被除数,第二个数字是除数。参数可以是任意解析为数字的表达式。 + + +##### 示例代码 + 假设集合 `shopping` 有如下记录: + + +```js +{ _id: 1, bags: 3, items: 5 } +{ _id: 2, bags: 2, items: 8 } +{ _id: 3, bags: 5, items: 16 } +``` +各记录取 `items` 除以 `bags` 的余数(`items % bags`): + + +```js +const $ = db.command.aggregate +let res = await db.collection('shopping').aggregate() + .project({ + overflow: $.mod(['$items', '$bags']) + }) + .end() +``` +返回结果如下: + + +```js +{ _id: 1, log: 2 } +{ _id: 2, log: 0 } +{ _id: 3, log: 1 } +``` + +#### multiply + +聚合操作符。取传入的数字参数相乘的结果。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.multiply([, , ...]) +``` +参数可以是任意解析为数字的表达式。 + + +##### 示例代码 + 假设集合 `fruits` 有如下记录: + + +```js +{ "_id": 1, "name": "apple", "price": 10, "quantity": 100 } +{ "_id": 2, "name": "orange", "price": 15, "quantity": 50 } +{ "_id": 3, "name": "lemon", "price": 5, "quantity": 20 } +``` +求各个水果的的总价值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('fruits').aggregate() + .project({ + name: 1, + total: $.multiply(['$price', '$quantity']), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "name": "apple", "total": 1000 } +{ "_id": 2, "name": "orange", "total": 750 } +{ "_id": 3, "name": "lemo", "total": 100 } +``` + +#### pow + +聚合操作符。求给定基数的指数次幂。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.pow([, ]) +``` +参数可以是任意解析为数字的表达式。 + + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ "_id": 1, "x": 2, "y": 3 } +{ "_id": 2, "x": 5, "y": 7 } +{ "_id": 3, "x": 10, "y": 20 } +``` +求 `x` 和 `y` 的平方和: + + +```js +const $ = db.command.aggregate +let res = await db.collection('stats').aggregate() + .project({ + sumOfSquares: $.add([$.pow(['$x', 2]), $.pow(['$y', 2])]), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "sumOfSquares": 13 } +{ "_id": 2, "sumOfSquares": 74 } +{ "_id": 3, "sumOfSquares": 500 } +``` + +#### sqrt + +聚合操作符。求平方根。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.sqrt([]) +``` +参数可以是任意解析为非负数字的表达式。 + + +##### 示例代码 + 假设直角三角形集合 `triangle` 有如下记录: + + +```js +{ "_id": 1, "x": 2, "y": 3 } +{ "_id": 2, "x": 5, "y": 7 } +{ "_id": 3, "x": 10, "y": 20 } +``` +假设 `x` 和 `y` 分别为两直角边,则求斜边长: + + +```js +const $ = db.command.aggregate +let res = await db.collection('triangle').aggregate() + .project({ + len: $.sqrt([$.add([$.pow(['$x', 2]), $.pow(['$y', 2])])]), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "len": 3.605551275463989 } +{ "_id": 2, "len": 8.602325267042627 } +{ "_id": 3, "len": 22.360679774997898 } +``` + +#### subtract + +聚合操作符。将两个数字相减然后返回差值,或将两个日期相减然后返回相差的毫秒数,或将一个日期减去一个数字返回结果的日期。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.subtract([, ]) +``` +参数可以是任意解析为数字或日期的表达式。 + + +##### 示例代码 + 假设集合 `scores` 有如下记录: + + +```js +{ "_id": 1, "max": 10, "min": 1 } +{ "_id": 2, "max": 7, "min": 5 } +{ "_id": 3, "max": 6, "min": 6 } +``` +求各个记录的 `max` 和 `min` 的差值。: + + +```js +const $ = db.command.aggregate +let res = await db.collection('scores').aggregate() + .project({ + diff: $.subtract(['$max', '$min']) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "diff": 9 } +{ "_id": 2, "diff": 2 } +{ "_id": 3, "diff": 0 } +``` + +#### trunc + +聚合操作符。将数字截断为整形。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.trunc() +``` +参数可以是任意解析为数字的表达式。 + + +##### 示例代码 + 假设集合 `scores` 有如下记录: + + +```js +{ "_id": 1, "value": 1.21 } +{ "_id": 2, "value": 3.83 } +{ "_id": 3, "value": -4.94 } +``` + +```js +const $ = db.command.aggregate +let res = await db.collection('scores').aggregate() + .project({ + int: $.trunc('$value') + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "value": 1 } +{ "_id": 2, "value": 3 } +{ "_id": 3, "value": -4 } +``` + +### 数组操作符 + +#### arrayElemAt + +聚合操作符。返回在指定数组下标的元素。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.arrayElemAt([, ]) +``` +`` 可以是任意解析为数字的表达式。 + + `` 可以是任意解析为整形的表达式。如果是正数,`arrayElemAt` 返回在 `index` 位置的元素,如果是负数,`arrayElemAt` 返回从数组尾部算起的 `index` 位置的元素。 + + +##### 示例代码 + 假设集合 `exams` 有如下记录: + + +```js +{ "_id": 1, "scores": [80, 60, 65, 90] } +{ "_id": 2, "scores": [78] } +{ "_id": 3, "scores": [95, 88, 92] } +``` +求各个第一次考试的分数和和最后一次的分数: + + +```js +const $ = db.command.aggregate +let res = await db.collection('exams').aggregate() + .project({ + first: $.arrayElemAt(['$scores', 0]), + last: $.arrayElemAt(['$scores', -1]), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "first": 80, "last": 90 } +{ "_id": 2, "first": 78, "last": 78 } +{ "_id": 3, "first": 95, "last": 92 } +``` + +#### arrayToObject + +聚合操作符。将一个数组转换为对象。 + + +##### API 说明 + 语法可以取两种: + + 第一种:传入一个二维数组,第二维的数组长度必须为 2,其第一个值为字段名,第二个值为字段值 + + +```js +db.command.aggregate.arrayToObject([ + [, ], + [, ], + ... +]) +``` +第二种:传入一个对象数组,各个对象必须包含字段 `k` 和 `v`,分别指定字段名和字段值 + + +```js +db.command.aggregate.arrayToObject([ + { "k": , "v": }, + { "k": , "v": }, + ... +]) +``` +传入 `arrayToObject` 的参数只要可以解析为上述两种表示法之一即可。 + + +##### 示例代码 + 假设集合 `shops` 有如下记录: + + +```js +{ "_id": 1, "sales": [ ["max", 100], ["min", 50] ] } +{ "_id": 2, "sales": [ ["max", 70], ["min", 60] ] } +{ "_id": 3, "sales": [ { "k": "max", "v": 50 }, { "k": "min", "v": 30 } ] } +``` +求各个第一次考试的分数和和最后一次的分数: + + +```js +const $ = db.command.aggregate +let res = await db.collection('shops').aggregate() + .project({ + sales: $.arrayToObject('$sales'), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "sales": { "max": 100, "min": 50 } } +{ "_id": 2, "sales": { "max": 70, "min": 60 } } +{ "_id": 3, "sales": { "max": 50, "min": 30 } } +``` + +#### concatArrays + +聚合操作符。将多个数组拼接成一个数组。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.arrayToObject([ , , ... ]) +``` +参数可以是任意解析为数组的表达式。 + + +##### 示例代码 + 假设集合 `items` 有如下记录: + + +```js +{ "_id": 1, "fruits": [ "apple" ], "vegetables": [ "carrot" ] } +{ "_id": 2, "fruits": [ "orange", "lemon" ], "vegetables": [ "cabbage" ] } +{ "_id": 3, "fruits": [ "strawberry" ], "vegetables": [ "spinach" ] } +``` + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + list: $.concatArrays(['$fruits', '$vegetables']), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "list": [ "apple", "carrot" ] } +{ "_id": 2, "list": [ "orange", "lemon", "cabbage" ] } +{ "_id": 3, "list": [ "strawberry", "spinach" ] } +``` + +#### filter + +聚合操作符。根据给定条件返回满足条件的数组的子集。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.filter({ + input: , + as: , + cond: +}) +``` + +|字段 |说明 | +|---- |---- | +|input|一个可以解析为数组的表达式 | +|as |可选,用于表示数组各个元素的变量,默认为 this | +|cond |一个可以解析为布尔值的表达式,用于判断各个元素是否满足条件,各个元素的名字由 as 参数决定(参数名需加 $$ 前缀,如 $$this)| + +参数可以是任意解析为数组的表达式。 + + +##### 示例代码 + 假设集合 `fruits` 有如下记录: + + +```json +{ + "_id": 1, + "stock": [ + { "name": "apple", "price": 10 }, + { "name": "orange", "price": 20 } + ], +} +{ + "_id": 2, + "stock": [ + { "name": "lemon", "price": 15 }, + ], +} +``` + +```js +const _ = db.command +const $ = db.command.aggregate +let res = await db.collection('fruits').aggregate() + .project({ + stock: $.filter({ + input: '$stock', + as: 'item', + cond: $.gte(['$$item.price', 15]) + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "stock": [ { "name": "orange", "price": 20} ] } +{ "_id": 2, "stock": [ { "name": "lemon", "price": 15 } ] } +``` + +#### in + +聚合操作符。给定一个值和一个数组,如果值在数组中则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.in([, ]) +``` +`` 可以是任意表达式。 + + `` 可以是任意解析为数组的表达式。 + + +##### 示例代码 + 假设集合 `shops` 有如下记录: + + +```js +{ "_id": 1, "topsellers": ["bread", "ice cream", "butter"] } +{ "_id": 2, "topsellers": ["ice cream", "cheese", "yagurt"] } +{ "_id": 3, "topsellers": ["croissant", "cucumber", "coconut"] } +``` +标记销量最高的商品包含 `ice cream` 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + included: $.in(['ice cream', '$topsellers']) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "included": true } +{ "_id": 2, "included": true } +{ "_id": 3, "included": false } +``` + +#### indexOfArray + +聚合操作符。在数组中找出等于给定值的第一个元素的下标,如果找不到则返回 -1。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.indexOfArray([ , , , ]) +``` + +|字段 |类型 |说明 | +|---- |---- |---- | +|- |string |一个可以解析为数组的表达式,如果解析为 null,则 indexOfArray 返回 null | +|- |string |对数据各个元素应用的条件匹配表达式 | +|- |integer|可选,用于指定搜索的开始下标,必须是非负整数 | +|- |integer|可选,用于指定搜索的结束下标,必须是非负整数,指定了 时也应指定 ,否则 默认当做| + +参数可以是任意解析为数组的表达式。 + + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```json +{ + "_id": 1, + "sales": [ 1, 6, 2, 2, 5 ] +} +{ + "_id": 2, + "sales": [ 4, 2, 1, 5, 2 ] +} +{ + "_id": 3, + "sales": [ 2, 5, 3, 3, 1 ] +} +``` + +```js +const $ = db.command.aggregate +let res = await db.collection('stats').aggregate() + .project({ + index: $.indexOfArray(['$sales', 2, 2]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "index": 2 } +{ "_id": 2, "index": 4 } +{ "_id": 3, "index": -1 } +``` + +#### isArray + +聚合操作符。判断给定表达式是否是数组,返回布尔值。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.isArray() +``` +参数可以是任意表达式。 + + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ + "_id": 1, + "base": 10, + "sales": [ 1, 6, 2, 2, 5 ] +} +{ + "_id": 2, + "base": 1, + "sales": 100 +} +``` +计算总销量,如果 `sales` 是数字,则求 `sales * base`,如果 `sales` 是数组,则求数组元素之和与 `base` 的乘积。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('stats').aggregate() + .project({ + sum: $.cond({ + if: $.isArray('$sales'), + then: $.multiply([$.sum(['$sales']), '$base']), + else: $.multiply(['$sales', '$base']), + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "index": 160 } +{ "_id": 2, "index": 100 } +``` + +#### map + +聚合操作符。类似 JavaScript Array 上的 `map` 方法,将给定数组的每个元素按给定转换方法转换后得出新的数组。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.map({ + input: , + as: , + in: +}) +``` + +|字段 |说明 | +|---- |---- | +|input|一个可以解析为数组的表达式 | +|as |可选,用于表示数组各个元素的变量,默认为 this | +|in |一个可以应用在给定数组的各个元素上的表达式,各个元素的名字由 as 参数决定(参数名需加 $$ 前缀,如 $$this)| + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ + "_id": 1, + "sales": [ 1.32, 6.93, 2.48, 2.82, 5.74 ] +} +{ + "_id": 2, + "sales": [ 2.97, 7.13, 1.58, 6.37, 3.69 ] +} +``` +将各个数字截断为整形,然后求和 + + +```js +const $ = db.command.aggregate +let res = await db.collection('stats').aggregate() + .project({ + truncated: $.map({ + input: '$sales', + as: 'num', + in: $.trunc('$$num'), + }) + }) + .project({ + total: $.sum('$truncated') + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "index": 16 } +{ "_id": 2, "index": 19 } +``` + +#### objectToArray + +聚合操作符。将一个对象转换为数组。方法把对象的每个键值对都变成输出数组的一个元素,元素形如 `{ k: , v: }`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.objectToArray() +``` + +##### 示例代码 + 假设集合 `items` 有如下记录: + + +```js +{ "_id": 1, "attributes": { "color": "red", "price": 150 } } +{ "_id": 2, "attributes": { "color": "blue", "price": 50 } } +{ "_id": 3, "attributes": { "color": "yellow", "price": 10 } } +``` + +``` +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + array: $.objectToArray('$attributes') + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "array": [{ "k": "color", "v": "red" }, { "k": "price", "v": 150 }] } +{ "_id": 2, "array": [{ "k": "color", "v": "blue" }, { "k": "price", "v": 50 }] } +{ "_id": 3, "array": [{ "k": "color", "v": "yellow" }, { "k": "price", "v": 10 }] } +``` + +#### range + +聚合操作符。返回一组生成的序列数字。给定开始值、结束值、非零的步长,`range` 会返回从开始值开始逐步增长、步长为给定步长、但不包括结束值的序列。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.range([, , ]) +``` + +|字段 |说明 | +|---- |---- | +|start |开始值,一个可以解析为整形的表达式 | +|end |结束值,一个可以解析为整形的表达式 | +|non-zero step|可选,步长,一个可以解析为非零整形的表达式,默认为 1 | + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ "_id": 1, "max": 52 } +{ "_id": 2, "max": 38 } +``` + +```js +const $ = db.command.aggregate +db.collection('stats').aggregate() + .project({ + points: $.range([0, '$max', 10]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "points": [0, 10, 20, 30, 40, 50] } +{ "_id": 2, "points": [0, 10, 20] } +``` + +#### reduce + +聚合操作符。类似 JavaScript 的 `reduce` 方法,应用一个表达式于数组各个元素然后归一成一个元素。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.reduce({ + input: + initialValue: , + in: +}) +``` + +|字段 |说明 | +|---- |---- | +|input |输入数组,可以是任意解析为数组的表达式 | +|initialValue |初始值 | +|in |用来作用于每个元素的表达式,在 in 中有两个可用变量,value 是表示累计值的变量,this 是表示当前数组元素的变量| + +##### 示例代码 + + +**简易字符串拼接** + + 假设集合 `player` 有如下记录: + + +```js +{ "_id": 1, "fullname": [ "Stephen", "Curry" ] } +{ "_id": 2, "fullname": [ "Klay", "Thompsom" ] } +``` +获取各个球员的全名,并加 `Player:` 前缀: + + +```js +const $ = db.command.aggregate +let res = await db.collection('player').aggregate() + .project({ + info: $.reduce({ + input: '$fullname', + initialValue: 'Player:', + in: $.concat(['$$value', ' ', '$$this']), + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "info": "Player: Stephen Curry" } +{ "_id": 2, "info": "Player: Klay Thompson" } +``` +获取各个球员的全名,不加前缀: + + +```js +const $ = db.command.aggregate +let res = await db.collection('player').aggregate() + .project({ + name: $.reduce({ + input: '$fullname', + initialValue: '', + in: $.concat([ + '$$value', + $.cond({ + if: $.eq(['$$value', '']), + then: '', + else: ' ', + }), + '$$this', + ]), + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "name": "Stephen Curry" } +{ "_id": 2, "name": "Klay Thompson" } +``` + +#### reverseArray + +聚合操作符。返回给定数组的倒序形式。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.reverseArray() +``` +参数可以是任意解析为数组表达式。 + + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ + "_id": 1, + "sales": [ 1, 2, 3, 4, 5 ] +} +``` +取 `sales` 倒序: + + +```js +const $ = db.command.aggregate +let res = await db.collection('stats').aggregate() + .project({ + reversed: $.reverseArray('$sales'), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "reversed": [5, 4, 3, 2, 1] } +``` + +#### size + +聚合操作符。返回数组长度。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.size() +``` +`` 可以是任意解析为数组的表达式。 + + +##### 示例代码 + 假设集合 `shops` 有如下记录: + + +```js +{ "_id": 1, "staff": [ "John", "Middleton", "George" ] } +{ "_id": 2, "staff": [ "Steph", "Jack" ] } +``` +计算各个商店的雇员数量: + + +```js +const $ = db.command.aggregate +let res = await db.collection('shops').aggregate() + .project({ + totalStaff: $.size('$staff') + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "totalStaff": 3 } +{ "_id": 2, "totalStaff": 2 } +``` + +#### slice + +聚合操作符。类似 JavaScritp 的 `slice` 方法。返回给定数组的指定子集。 + + +##### API 说明 + 语法有两种: + + 返回从开头或结尾开始的 `n` 个元素: + + +```js +db.command.aggregate.slice([, ]) +``` +返回从指定位置算作数组开头、再向后或向前的 `n` 个元素: + + +```js +db.command.aggregate.slice([, , ]) +``` +`` 可以是任意解析为数组的表达式。 + + `` 可以是任意解析为整形的表达式。如果是正数,则将数组的第 `` 个元素作为数组开始;如果 `` 比数组长度更长,`slice` 返回空数组。如果是负数,则将数组倒数第 `` 个元素作为数组开始;如果 `` 的绝对值大于数组长度,则开始位置即为数组开始位置。 + + `` 可以是任意解析为整形的表达式。如果 `` 有提供,则 `` 必须为正整数。如果是正数,`slice` 返回前 `n` 个元素。如果是负数,`slice` 返回后 `n` 个元素。 + + +##### 示例代码 + 假设集合 `people` 有如下记录: + + +```js +{ "_id": 1, "hobbies": [ "basketball", "football", "tennis", "badminton" ] } +{ "_id": 2, "hobbies": [ "golf", "handball" ] } +{ "_id": 3, "hobbies": [ "table tennis", "swimming", "rowing" ] } +``` +统一返回前两个爱好: + + +```js +const $ = db.command.aggregate +let res = await db.collection('fruits').aggregate() + .project({ + hobbies: $.slice(['$hobbies', 2]), + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "hobbies": [ "basketball", "football" ] } +{ "_id": 2, "hobbies": [ "golf", "handball" ] } +{ "_id": 3, "hobbies": [ "table tennis", "swimming" ] } +``` + +#### zip + +聚合操作符。把二维数组的第二维数组中的相同序号的元素分别拼装成一个新的数组进而组装成一个新的二维数组。如可将 `[ [ 1, 2, 3 ], [ "a", "b", "c" ] ]` 转换成 `[ [ 1, "a" ], [ 2, "b" ], [ 3, "c" ] ]`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.zip({ + inputs: [, , ...], + useLongestLength: , + defaults: +}) +``` +`inputs` 是一个二维数组(`inputs` 不可以是字段引用),其中每个元素的表达式(这个可以是字段引用)都可以解析为数组。如果其中任意一个表达式返回 `null`,`` 也返回 `null`。如果其中任意一个表达式不是指向一个合法的字段 / 解析为数组 / 解析为 `null`,则返回错误。 + + `useLongestLength` 决定输出数组的长度是否采用输入数组中的最长数组的长度。默认为 `false`,即输入数组中的最短的数组的长度即是输出数组的各个元素的长度。 + + `defaults` 是一个数组,用于指定在输入数组长度不一的情况下时采用的数组各元素默认值。指定这个字段则必须指定 `useLongestLength`,否则返回错误。如果 `useLongestLength` 是 `true` 但是 `defaults` 是空或没有指定,则 `zip` 用 `null` 做数组元素的缺省默认值。指定各元素默认值时 `defaults` 数组的长度必须是输入数组最大的长度。 + + +##### 示例代码 + 假设集合 `stats` 有如下记录: + + +```js +{ "_id": 1, "zip1": [1, 2], "zip2": [3, 4], "zip3": [5, 6] ] } +{ "_id": 2, "zip1": [1, 2], "zip2": [3], "zip3": [4, 5, 6] ] } +{ "_id": 3, "zip1": [1, 2], "zip2": [3] ] } +``` + + +**只传 inputs** + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + zip: $.zip({ + inputs: [ + '$zip1', // 字段引用 + '$zip2', + '$zip3', + ], + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "zip": [ [1, 3, 5], [2, 4, 6] ] } +{ "_id": 2, "zip": [ [1, 3, 4] ] } +{ "_id": 3, "zip": null } +``` + + +**设置 useLongestLength** + + 如果设 `useLongestLength` 为 `true`: + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + zip: $.zip({ + inputs: [ + '$zip1', // 字段引用 + '$zip2', + '$zip3', + ], + useLongestLength: true, + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "zip": [ [1, 3, 5], [2, 4, 6] ] } +{ "_id": 2, "zip": [ [1, 3, 4], [2, null, 5], [null, null, 6] ] } +{ "_id": 3, "zip": null } +``` + + +**设置 defaults** + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + zip: $.zip({ + inputs: [ + '$zip1', // 字段引用 + '$zip2', + '$zip3', + ], + useLongestLength: true, + defaults: [-300, -200, -100], + }) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "zip": [ [1, 3, 5], [2, 4, 6] ] } +{ "_id": 2, "zip": [ [1, 3, 4], [2, -200, 5], [-300, -200, 6] ] } +{ "_id": 3, "zip": null } +``` + +### 布尔操作符 + +#### and + +聚合操作符。给定多个表达式,`and` 仅在所有表达式都返回 `true` 时返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.and([, , ...]) +``` +如果表达式返回 `false`、`null`、`0`、或 `undefined`,表达式会解析为 `false`,否则对其他返回值都认为是 `true`。 + + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "min": 10, "max": 100 } +{ "_id": 2, "min": 60, "max": 80 } +{ "_id": 3, "min": 30, "max": 50 } +``` +求 `min` 大于等于 30 且 `max` 小于等于 80 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + fullfilled: $.and([$.gte(['$min', 30]), $.lte(['$max', 80])]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "fullfilled": false } +{ "_id": 2, "fullfilled": true } +{ "_id": 3, "fullfilled": true } +``` + +#### not + +聚合操作符。给定一个表达式,如果表达式返回 `true`,则 `not` 返回 `false`,否则返回 `true`。注意表达式不能为逻辑表达式(`and`、`or`、`nor`、`not`)。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.not() +``` +如果表达式返回 `false`、`null`、`0`、或 `undefined`,表达式会解析为 `false`,否则对其他返回值都认为是 `true`。 + + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "min": 10, "max": 100 } +{ "_id": 2, "min": 60, "max": 80 } +{ "_id": 3, "min": 30, "max": 50 } +``` +求 `min` 不大于 40 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + fullfilled: $.not($.gt(['$min', 40])) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "fullfilled": true } +{ "_id": 2, "fullfilled": false } +{ "_id": 3, "fullfilled": true } +``` + +#### or + +聚合操作符。给定多个表达式,如果任意一个表达式返回 `true`,则 `or` 返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.or([, , ...]) +``` +如果表达式返回 `false`、`null`、`0`、或 `undefined`,表达式会解析为 `false`,否则对其他返回值都认为是 `true`。 + + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "min": 10, "max": 100 } +{ "_id": 2, "min": 60, "max": 80 } +{ "_id": 3, "min": 30, "max": 50 } +``` +求 `min` 小于 40 且 `max` 大于 60 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + fullfilled: $.or([$.lt(['$min', 30]), $.gt(['$max', 60])]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "fullfilled": true } +{ "_id": 2, "fullfilled": false } +{ "_id": 3, "fullfilled": true } +``` + +### 比较操作符 + +#### cmp + +聚合操作符。给定两个值,返回其比较值: + + +##### API 说明 + 如果第一个值小于第二个值,返回 -1 +如果第一个值大于第二个值,返回 1 +如果两个值相等,返回 0 + + 语法如下: + + +```js +db.command.aggregate.cmp([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "shop1": 10, "shop2": 100 } +{ "_id": 2, "shop1": 80, "shop2": 20 } +{ "_id": 3, "shop1": 50, "shop2": 50 } +``` +求 `shop1` 和 `shop2` 的各个物品的价格对比。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + compare: $.cmp(['$shop1', '$shop2'])) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "compare": -1 } +{ "_id": 2, "compare": 1 } +{ "_id": 3, "compare": 0 } +``` + +#### eq + +聚合操作符。匹配两个值,如果相等则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.eq([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +求 `value` 等于 50 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + matched: $.eq(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": false } +{ "_id": 2, "matched": false } +{ "_id": 3, "matched": true } +``` + +#### gt + +聚合操作符。匹配两个值,如果前者大于后者则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.gt([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +判断 `value` 是否大于 50。 + + +```js +const $ = db.command.aggregate +db.collection('price').aggregate() + .project({ + matched: $.gt(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": false } +{ "_id": 2, "matched": true } +{ "_id": 3, "matched": false } +``` + +#### gte + +聚合操作符。匹配两个值,如果前者大于或等于后者则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.gte([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +判断 `value` 是否大于或等于 50。 + + +```js +const $ = db.command.aggregate +let res = await b.collection('price').aggregate() + .project({ + matched: $.gte(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": false } +{ "_id": 2, "matched": true } +{ "_id": 3, "matched": true } +``` + +#### lt + +聚合操作符。匹配两个值,如果前者小于后者则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.lt([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +``` +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +判断 `value` 是否小于 50。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + matched: $.lt(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": true } +{ "_id": 2, "matched": false } +{ "_id": 3, "matched": false } +``` + +#### lte + +聚合操作符。匹配两个值,如果前者小于或等于后者则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.lte([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +判断 `value` 是否小于 50。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + matched: $.lte(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": true } +{ "_id": 2, "matched": false } +{ "_id": 3, "matched": true } +``` + +#### neq + +聚合操作符。匹配两个值,如果不相等则返回 `true`,否则返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.neq([, ]) +``` + +##### 示例代码 + 假设集合 `price` 有如下记录: + + +```js +{ "_id": 1, "value": 10 } +{ "_id": 2, "value": 80 } +{ "_id": 3, "value": 50 } +``` +求 `value` 不等于 50 的记录。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('price').aggregate() + .project({ + matched: $.neq(['$value', 50]) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "matched": true } +{ "_id": 2, "matched": true } +{ "_id": 3, "matched": false } +``` + +### 条件操作符 + +#### cond + +聚合操作符。计算布尔表达式,返回指定的两个值其中之一。 + + +##### API 说明 + `cond` 的使用形式如下: + + +```js +cond({ if: <布尔表达式>, then: <真值>, else: <假值> }) +``` +或者: + + +```js +cond([ <布尔表达式>, <真值>, <假值> ]) +``` +两种形式中,三个参数(`if`、`then`、`else`)都是必须的。 + + 如果布尔表达式为真,那么 `$cond` 将会返回 `<真值>`,否则会返回 `<假值>` + + +##### 示例代码 + 假设集合 `items` 的记录如下: + + +```js +{ "_id": "0", "name": "item-a", "amount": 100 } +{ "_id": "1", "name": "item-b", "amount": 200 } +{ "_id": "2", "name": "item-c", "amount": 300 } +``` +我们可以使用 `cond`,根据 `amount` 字段,来生成新的字段 `discount`: + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + name: 1, + discount: $.cond({ + if: $.gte(['$amount', 200]), + then: 0.7, + else: 0.9 + }) + }) + .end() +``` +输出如下: + + +```js +{ "_id": "0", "name": "item-a", "discount": 0.9 } +{ "_id": "1", "name": "item-b", "discount": 0.7 } +{ "_id": "2", "name": "item-c", "discount": 0.7 } +``` + +#### ifNull + +聚合操作符。计算给定的表达式,如果表达式结果为 null、undefined 或者不存在,那么返回一个替代值;否则返回原值。 + + +##### API 说明 + `ifNull` 的使用形式如下: + + +```js +ifNull([ <表达式>, <替代值> ]) +``` + +##### 示例代码 + 假设集合 `items` 的记录如下: + + +```js +{ "_id": "0", "name": "A", "description": "这是商品A" } +{ "_id": "1", "name": "B", "description": null } +{ "_id": "2", "name": "C" } +``` +我们可以使用 `ifNull`,对不存在 `desc` 字段的文档,或者 `desc` 字段为 `null` 的文档,补充一个替代值。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + _id: 0, + name: 1, + description: $.ifNull(['$description', '商品描述空缺']) + }) + .end() +``` +输出如下: + + +```js +{ "name": "A", "description": "这是商品A" } +{ "name": "B", "description": "商品描述空缺" } +{ "name": "C", "description": "商品描述空缺" } +``` + +#### switch + +聚合操作符。根据给定的 `switch-case-default` 计算返回值、 + + +##### API 说明 + `switch` 的使用形式如下: + + +```js +switch({ + branches: [ + case: <表达式>, then: <表达式>, + case: <表达式>, then: <表达式>, + ... + ], + default: <表达式> +}) +``` + +##### 示例代码 + 假设集合 `items` 的记录如下: + + +```js +{ "_id": "0", "name": "item-a", "amount": 100 } +{ "_id": "1", "name": "item-b", "amount": 200 } +{ "_id": "2", "name": "item-c", "amount": 300 } +``` +我们可以使用 `switch`,根据 `amount` 字段,来生成新的字段 `discount`: + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + name: 1, + discount: $.switch({ + branches: [ + { case: $.gt(['$amount', 250]), then: 0.8 }, + { case: $.gt(['$amount', 150]), then: 0.9 } + ], + default: 1 + }) + }) + .end() +``` +输出如下: + + +```js +{ "_id": "0", "name": "item-a", "discount": 1 } +{ "_id": "1", "name": "item-b", "discount": 0.9 } +{ "_id": "2", "name": "item-c", "discount": 0.8 } +``` + +### 日期操作符 + +#### dateFromParts + +聚合操作符。给定日期的相关信息,构建并返回一个日期对象。 + +**平台差异说明** + +|阿里云 |腾讯云 | +|---- |---- | +|× |√ | + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.dateFromParts({ + year: , + month: , + day: , + hour: , + minute: , + second: , + millisecond: , + timezone: +}) +``` +你也可以使用 ISO 8601 的标准: + + +```js +db.command.aggregate.dateFromParts({ + isoWeekYear: , + isoWeek: , + isoDayOfWeek: , + hour: , + minute: , + second: , + millisecond: , + timezone: +}) +``` + +**说明** + +- `timezone`字段请参考[Olson Timezone Identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones),形式类似:`Asia/Shanghai` + +##### 示例代码 + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + date: $.dateFromParts({ + year: 2017, + month: 2, + day: 8, + hour: 12, + timezone: 'America/New_York' + }), + }) + .end() +``` +输出如下: + + +```js +{ + "date": ISODate("2017-02-08T17:00:00.000Z") +} +``` + +#### dateFromString + +聚合操作符。将一个日期/时间字符串转换为日期对象 + +**平台差异说明** + +|阿里云 |腾讯云 | +|---- |---- | +|× |√ | + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.dateFromString({ + dateString: , + timezone: +}) +``` + +##### 示例代码 + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + date: $.dateFromString({ + dateString: "2019-05-14T09:38:51.686Z" + }) + }) + .end() +``` +输出如下: + + +```js +{ + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` + +#### dateToString + +聚合操作符。根据指定的表达式将日期对象格式化为符合要求的字符串。 + +**平台差异说明** + +|阿里云 |腾讯云 | +|---- |---- | +|× |√ | + +##### API 说明 + `dateToString` 的调用形式如下: + + +```js +db.command.aggregate.dateToString({ + date: <日期表达式>, + format: <格式化表达式>, + timezone: <时区表达式>, + onNull: <空值表达式> +}) +``` +下面是四种表达式的详细说明: + +|名称 |描述 | +|---- |---- | +|日期表达式 |必选。指定字段值应该是能转化为字符串的日期。 | +|格式化表达式 |可选。它可以是任何包含“格式说明符”的有效字符串。 | +|时区表达式 |可选。指明运算结果的时区。它可以解析格式为 [UTC Offset](https://en.wikipedia.org/wiki/List_of_UTC_time_offsets) 或者 [Olson Timezone Identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) 的字符串。| +|空值表达式 |可选。当 <日期表达式> 返回空或者不存在的时候,会返回此表达式指明的值。 | + +下面是格式说明符的详细说明: + +|说明符 |描述 |合法值 | +|---- |---- |---- | +|%d |月份的日期(2位数,0填充) |01 - 31 | +|%G |ISO 8601 格式的年份 |0000 - 9999| +|%H |小时(2位数,0填充,24小时制) |00 - 23 | +|%j |一年中的一天(3位数,0填充) |001 - 366 | +|%L |毫秒(3位数,0填充) |000 - 999 | +|%m |月份(2位数,0填充) |01 - 12 | +|%M |分钟(2位数,0填充) |00 - 59 | +|%S |秒(2位数,0填充) |00 - 60 | +|%w |星期几 |1 - 7 | +|%u |ISO 8601 格式的星期几 |1 - 7 | +|%U |一年中的一周(2位数,0填充) |00 - 53 | +|%V |ISO 8601 格式的一年中的一周 |1 - 53 | +|%Y |年份(4位数,0填充) |0000 - 9999| +|%z |与 UTC 的时区偏移量 |+/-[hh][mm]| +|%Z |以分钟为单位,与 UTC 的时区偏移量|+/-mmm | +|%% |百分号作为字符 |% | + +##### 示例代码 + 假设集合 `students` 有如下记录: + + +```js +{ "date": "1999-12-11T16:00:00.000Z", "firstName": "Yuanxin", "lastName": "Dong" } +{ "date": "1998-11-10T16:00:00.000Z", "firstName": "Weijia", "lastName": "Wang" } +{ "date": "1997-10-09T16:00:00.000Z", "firstName": "Chengxi", "lastName": "Li" } +``` + + +**格式化日期** + + 下面是将 `date` 字段的值,格式化成形如 `年份-月份-日期` 的字符串: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + formatDate: $.dateToString({ + date: '$date', + format: '%Y-%m-%d' + }) + }) + .end() +``` +返回的结果如下: + + +```js +{ "formatDate": "1999-12-11" } +{ "formatDate": "1998-11-10" } +{ "formatDate": "1997-10-09" } +``` + + +**时区时间** + + 下面是将 `date` 字段值格式化为上海时区时间的例子: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + formatDate: $.dateToString({ + date: '$date', + format: '%H:%M:%S', + timezone: 'Asia/Shanghai' + }) + }) + .end() +``` +返回的结果如下: + + +```js +{ "formatDate": "00:00:00" } +{ "formatDate": "00:00:00" } +{ "formatDate": "00:00:00" } +``` + + +**缺失情况的默认值** + + 当指定的 `<日期表达式>` 返回空或者不存在的时候,可以设置缺失情况下的默认值: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + formatDate: $.dateToString({ + date: '$empty', + onNull: 'null' + }) + }) + .end() +``` +返回的结果如下: + + +```js +{ "formatDate": "null" } +{ "formatDate": "null" } +{ "formatDate": "null" } +``` + +#### dayOfMonth + +聚合操作符。返回日期字段对应的天数(一个月中的哪一天),是一个介于 1 至 31 之间的数字。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.dayOfMonth(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `dayOfMonth()` 对 `date` 字段进行投影,获取对应的日期: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + dayOfMonth: $.dayOfMonth('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "dayOfMonth": 14 +} +``` + +#### dayOfWeek + +聚合操作符。返回日期字段对应的天数(一周中的第几天),是一个介于 1(周日)到 7(周六)之间的整数。 + + +##### API 说明 + *注意:周日是每周的第 1 天** + + 语法如下: + + +```js +db.command.aggregate.dayOfWeek(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `dayOfWeek()` 对 `date` 字段进行投影,获取对应的天数(一周中的第几天): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + dayOfWeek: $.dayOfWeek('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "dayOfWeek": 3 +} +``` + +#### dayOfYear + +聚合操作符。返回日期字段对应的天数(一年中的第几天),是一个介于 1 到 366 之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.dayOfYear(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `dayOfYear()` 对 `date` 字段进行投影,获取对应的天数(一年中的第几天): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + dayOfYear: $.dayOfYear('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "dayOfYear": 134 +} +``` + +#### hour + +聚合操作符。返回日期字段对应的小时数,是一个介于 0 到 23 之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.hour(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `hour()` 对 `date` 字段进行投影,获取对应的小时数: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + hour: $.hour('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "hour": 9 +} +``` + +#### isoDayOfWeek + +聚合操作符。返回日期字段对应的 ISO 8601 标准的天数(一周中的第几天),是一个介于 1(周一)到 7(周日)之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.month(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `month()` 对 `date` 字段进行投影,获取对应的 ISO 8601 标准的天数(一周中的第几天): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + isoDayOfWeek: $.isoDayOfWeek('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "isoDayOfWeek": 2 +} +``` + +#### isoWeek + +聚合操作符。返回日期字段对应的 ISO 8601 标准的周数(一年中的第几周),是一个介于 1 到 53 之间的整数。 + + +##### API 说明 + 根据 ISO 8601 标准,周一到周日视为一周,本年度第一个周四所在的那周,视为本年度的第 1 周。 + + 例如:2016 年 1 月 7 日是那年的第一个周四,那么 2016.01.04(周一)到 2016.01.10(周日) 即为第 1 周。同理,2016 年 1 月 1 日的周数为 53。 + + 语法如下: + + +```js +db.command.aggregate.isoWeek(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `isoWeek()` 对 `date` 字段进行投影,获取对应的 ISO 8601 标准的周数(一年中的第几周): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + isoWeek: $.isoWeek('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "isoWeek": 20 +} +``` + +#### isoWeekYear + +聚合操作符。返回日期字段对应的 ISO 8601 标准的天数(一年中的第几天)。 + + +##### API 说明 + 此处的“年”以第一周的周一为开始,以最后一周的周日为结束。 + + 语法如下: + + +```js +db.command.aggregate.isoWeekYear(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `isoWeekYear()` 对 `date` 字段进行投影,获取对应的 ISO 8601 标准的天数(一年中的第几天): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + isoWeekYear: $.isoWeekYear('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "isoWeekYear": 2019 +} +``` + +#### millisecond + +聚合操作符。返回日期字段对应的毫秒数,是一个介于 0 到 999 之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.millisecond(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `millisecond()` 对 `date` 字段进行投影,获取对应的毫秒数: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + millisecond: $.millisecond('$date'), + }) + .end() +``` +输出如下: + + +```js +{ + "millisecond": 686 +} +``` + +#### minute + +聚合操作符。返回日期字段对应的分钟数,是一个介于 0 到 59 之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.minute(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `minute()` 对 `date` 字段进行投影,获取对应的分钟数: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + minute: $.minute('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "minute": 38 +} +``` + +#### month + +聚合操作符。返回日期字段对应的月份,是一个介于 1 到 12 之间的整数。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.month(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `month()` 对 `date` 字段进行投影,获取对应的月份: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + month: $.month('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "month": 5 +} +``` + +#### second + +聚合操作符。返回日期字段对应的秒数,是一个介于 0 到 59 之间的整数,在特殊情况下(闰秒)可能等于 60。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.second(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `second()` 对 `date` 字段进行投影,获取对应的秒数: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + second: $.second('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "second": 51 +} +``` + +#### week + +聚合操作符。返回日期字段对应的周数(一年中的第几周),是一个介于 0 到 53 之间的整数。 + + +##### API 说明 + 每周以周日为开头,**每年的第一个周日**即为 `week 1` 的开始,这天之前是 `week 0`。 + + 语法如下: + + +```js +db.command.aggregate.week(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `week()` 对 `date` 字段进行投影,获取对应的周数(一年中的第几周): + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + week: $.week('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "week": 19 +} +``` + +#### year + +聚合操作符。返回日期字段对应的年份。 + + +##### API 说明 + 语法如下: + + +```js +db.command.aggregate.year(<日期字段>) +``` + +##### 示例代码 + 假设集合 `dates` 有以下文档: + + +```js +{ + "_id": 1, + "date": ISODate("2019-05-14T09:38:51.686Z") +} +``` +我们使用 `year()` 对 `date` 字段进行投影,获取对应的年份: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('dates') + .aggregate() + .project({ + _id: 0, + year: $.year('$date') + }) + .end() +``` +输出如下: + + +```js +{ + "year": 2019 +} +``` + +#### subtract + +见[subtract](#subtract) + +### 常量操作符 + +#### literal + +聚合操作符。直接返回一个值的字面量,不经过任何解析和处理。 + + +##### API 说明 + `literal` 使用形式如下: + + +```js +literal(<值>) +``` +如果 `<值>` 是一个表达式,那么 `literal` **不会**解析或者计算这个表达式,而是直接返回这个表达式。 + + +##### 示例代码 + 比如我们有一个 `items` 集合,其中数据如下: + + +```js +{ "_id": "0", "price": "$1" } +{ "_id": "1", "price": "$5.60" } +{ "_id": "2", "price": "$8.90" } +``` + + +**以字面量的形式使用 $** + + 下面的代码使用 `literal`,生成了一个新的字段 `isOneDollar`,表示 `price` 字段是否严格等于 `"$1"`。 + + 注意:我们这里无法使用 `eq(['$price', '$1'])`,因为 `"$1"` 是一个表达式,代表 `"1"` 字段对应的值,而不是字符串字面量 `"$1"`。 + + +```js +const $ = db.command.aggregate +let res = await db.collection('items').aggregate() + .project({ + isOneDollar: $.eq(['$price', $.literal('$1')]) + }) + .end() +``` +输出如下: + + +```js +{ "_id": "0", "isOneDollar": true } +{ "_id": "1", "isOneDollar": false } +{ "_id": "2", "isOneDollar": false } +``` + + +**投影一个字段,对应的值为 1** + + 下面的代码使用 `literal`,投影了一个新的字段 `amount`,其值为 `1`。 + + +```js +const $ = db.command.aggregate +db.collection('items').aggregate() + .project({ + price: 1, + amount: $.literal(1) + }) + .end() +``` +输出如下: + + +```js +{ "_id": "0", "price": "$1", "amount": 1 } +{ "_id": "1", "price": "$5.60", "amount": 1 } +{ "_id": "2", "price": "$8.90", "amount": 1 } +``` + +### 对象操作符 + +#### mergeObjects + +聚合操作符。将多个文档合并为单个文档。 + +##### API 说明 + 使用形式如下: +在 `group()` 中使用时: + + +```js +mergeObjects() +``` +在其它表达式中使用时: + + +```js +mergeObjects([, , ...]) +``` + +##### 示例代码 + + +**搭配 `group()` 使用** + + 假设集合 `sales` 存在以下文档: + + +```js +{ "_id": 1, "year": 2018, "name": "A", "volume": { "2018Q1": 500, "2018Q2": 500 } } +{ "_id": 2, "year": 2017, "name": "A", "volume": { "2017Q1": 400, "2017Q2": 300, "2017Q3": 0, "2017Q4": 0 } } +{ "_id": 3, "year": 2018, "name": "B", "volume": { "2018Q1": 100 } } +{ "_id": 4, "year": 2017, "name": "B", "volume": { "2017Q3": 100, "2017Q4": 250 } } +``` +下面的代码使用 `mergeObjects()`,将用相同 `name` 的文档合并: + + +```js +const $ = db.command.aggregate +let res = await db.collection('sales').aggregate() + .group({ + _id: '$name', + mergedVolume: $.mergeObjects('$volume') + }) + .end() +``` +输出如下: + + +```js +{ "_id": "A", "mergedVolume": { "2017Q1": 400, "2017Q2": 300, "2017Q3": 0, "2017Q4": 0, "2018Q1": 500, "2018Q2": 500 } } +{ "_id": "B", "mergedVolume": { "2017Q3": 100, "2017Q4": 250, "2018Q1": 100 } } +``` + + +**一般用法** + + 假设集合 `test` 存在以下文档: + + +```js +{ "_id": 1, "foo": { "a": 1 }, "bar": { "b": 2 } } +{ "_id": 2, "foo": { "c": 1 }, "bar": { "d": 2 } } +{ "_id": 3, "foo": { "e": 1 }, "bar": { "f": 2 } } +``` +下面的代码使用 `mergeObjects()`,将文档中的 `foo` 和 `bar` 字段合并为 `foobar`: + + +```js +const $ = db.command.aggregate +let res = await db.collection('sales').aggregate() + .project({ + foobar: $.mergeObjects(['$foo', '$bar']) + }) + .end() +``` +输出结果如下: + + +```js +{ "_id": 1, "foobar": { "a": 1, "b": 2 } } +{ "_id": 2, "foobar": { "c": 1, "d": 2 } } +{ "_id": 3, "foobar": { "e": 1, "f": 2 } } +``` + +#### objectToArray + +见[objectToArray](#objectToArray) + +### 集合操作符 + +#### allElementsTrue + +聚合操作符。输入一个数组,或者数组字段的表达式。如果数组中所有元素均为真值,那么返回 `true`,否则返回 `false`。空数组永远返回 `true`。 + + +##### API 说明 + 语法如下: + + +```js +allElementsTrue([]) +``` + +##### 示例代码 + 假设集合 `test` 有如下记录: + + +```js +{ "_id": 1, "array": [ true ] } +{ "_id": 2, "array": [ ] } +{ "_id": 3, "array": [ false ] } +{ "_id": 4, "array": [ true, false ] } +{ "_id": 5, "array": [ 0 ] } +{ "_id": 6, "array": [ "stark" ] } +``` +下面的代码使用 `allElementsTrue()`,判断 `array` 字段中是否均为真值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('price') + .aggregate() + .project({ + isAllTrue: $.allElementsTrue(['$array']) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "isAllTrue": true } +{ "_id": 2, "isAllTrue": true } +{ "_id": 3, "isAllTrue": false } +{ "_id": 4, "isAllTrue": false } +{ "_id": 5, "isAllTrue": false } +{ "_id": 6, "isAllTrue": true } +``` + +#### anyElementTrue + +聚合操作符。输入一个数组,或者数组字段的表达式。如果数组中任意一个元素为真值,那么返回 `true`,否则返回 `false`。空数组永远返回 `false`。 + + +##### API 说明 + 语法如下: + + +```js +anyElementTrue([]) +``` + +##### 示例代码 + 假设集合 `test` 有如下记录: + + +```js +{ "_id": 1, "array": [ true ] } +{ "_id": 2, "array": [ ] } +{ "_id": 3, "array": [ false ] } +{ "_id": 4, "array": [ true, false ] } +{ "_id": 5, "array": [ 0 ] } +{ "_id": 6, "array": [ "stark" ] } +``` +下面的代码使用 `anyElementTrue()`,判断 `array` 字段中是否含有真值: + + +```js +const $ = db.command.aggregate +let res = await db.collection('price') + .aggregate() + .project({ + isAnyTrue: $.anyElementTrue(['$array']) + }) + .end() +``` +返回结果如下: + + +```js +{ "_id": 1, "isAnyTrue": true } +{ "_id": 2, "isAnyTrue": false } +{ "_id": 3, "isAnyTrue": false } +{ "_id": 4, "isAnyTrue": true } +{ "_id": 5, "isAnyTrue": false } +{ "_id": 6, "isAnyTrue": true } +``` + +#### setDifference + +聚合操作符,输入两个集合,输出只存在于第一个集合中的元素。 + + +##### API 说明 + 使用形式如下: + + +```js +setDifference([, ]) +``` + +##### 示例代码 + 假设集合 `test` 存在以下数据: + + +```js +{ "_id": 1, "A": [ 1, 2 ], "B": [ 1, 2 ] } +{ "_id": 2, "A": [ 1, 2 ], "B": [ 2, 1, 2 ] } +{ "_id": 3, "A": [ 1, 2 ], "B": [ 1, 2, 3 ] } +{ "_id": 4, "A": [ 1, 2 ], "B": [ 3, 1 ] } +{ "_id": 5, "A": [ 1, 2 ], "B": [ ] } +{ "_id": 6, "A": [ 1, 2 ], "B": [ {}, [] ] } +{ "_id": 7, "A": [ ], "B": [ ] } +{ "_id": 8, "A": [ ], "B": [ 1 ] } +``` +下面的代码使用 `setDifference`,找到只存在于 `B` 中的数字: + + +```js +let res = await db.collection('test') + .aggregate() + .project({ + isBOnly: $.setDifference(['$B', '$A']) + }) + .end() +``` + +```js +{ "_id": 1, "isBOnly": [] } +{ "_id": 2, "isBOnly": [3] } +{ "_id": 3, "isBOnly": [3] } +{ "_id": 4, "isBOnly": [5] } +{ "_id": 5, "isBOnly": [] } +{ "_id": 6, "isBOnly": [{}, []] } +{ "_id": 7, "isBOnly": [] } +{ "_id": 8, "isBOnly": [1] } +``` + +#### setEquals + +聚合操作符,输入两个集合,判断两个集合中包含的元素是否相同(不考虑顺序、去重)。 + + +##### API 说明 + 使用形式如下: + + +```js +setEquals([, ]) +``` + +##### 示例代码 + 假设集合 `test` 存在以下数据: + + +```js +{ "_id": 1, "A": [ 1, 2 ], "B": [ 1, 2 ] } +{ "_id": 2, "A": [ 1, 2 ], "B": [ 2, 1, 2 ] } +{ "_id": 3, "A": [ 1, 2 ], "B": [ 1, 2, 3 ] } +{ "_id": 4, "A": [ 1, 2 ], "B": [ 3, 1 ] } +{ "_id": 5, "A": [ 1, 2 ], "B": [ ] } +{ "_id": 6, "A": [ 1, 2 ], "B": [ {}, [] ] } +{ "_id": 7, "A": [ ], "B": [ ] } +{ "_id": 8, "A": [ ], "B": [ 1 ] } +``` +下面的代码使用 `setEquals`,判断两个集合中包含的元素是否相同: + + +```js +let res = await db.collection('test') + .aggregate() + .project({ + sameElements: $.setEquals(['$A', '$B']) + }) + .end() +``` + +```js +{ "_id": 1, "sameElements": true } +{ "_id": 2, "sameElements": true } +{ "_id": 3, "sameElements": false } +{ "_id": 4, "sameElements": false } +{ "_id": 5, "sameElements": false } +{ "_id": 6, "sameElements": false } +{ "_id": 7, "sameElements": true } +{ "_id": 8, "sameElements": false } +``` + +#### setIntersection + +聚合操作符,输入两个集合,输出两个集合的交集。 + + +##### API 说明 + 使用形式如下: + + +```js +setIntersection([, ]) +``` + +##### 示例代码 + 假设集合 `test` 存在以下数据: + + +```js +{ "_id": 1, "A": [ 1, 2 ], "B": [ 1, 2 ] } +{ "_id": 2, "A": [ 1, 2 ], "B": [ 2, 1, 2 ] } +{ "_id": 3, "A": [ 1, 2 ], "B": [ 1, 2, 3 ] } +{ "_id": 4, "A": [ 1, 2 ], "B": [ 3, 1 ] } +{ "_id": 5, "A": [ 1, 2 ], "B": [ ] } +{ "_id": 6, "A": [ 1, 2 ], "B": [ {}, [] ] } +{ "_id": 7, "A": [ ], "B": [ ] } +{ "_id": 8, "A": [ ], "B": [ 1 ] } +``` +下面的代码使用 `setIntersection`,输出两个集合的交集: + + +```js +let res = await db.collection('test') + .aggregate() + .project({ + commonToBoth: $.setIntersection(['$A', '$B']) + }) + .end() +``` +输出如下: + + +```js +{ "_id": 1, "commonToBoth": [ 1, 2 ] } +{ "_id": 2, "commonToBoth": [ 1, 2 ] } +{ "_id": 3, "commonToBoth": [ 1, 2 ] } +{ "_id": 4, "commonToBoth": [ 1 ] } +{ "_id": 5, "commonToBoth": [ ] } +{ "_id": 6, "commonToBoth": [ ] } +{ "_id": 7, "commonToBoth": [ ] } +{ "_id": 8, "commonToBoth": [ ] } +``` + +#### setIsSubset + +聚合操作符,输入两个集合,判断第一个集合是否是第二个集合的子集。 + + +##### API 说明 + 使用形式如下: + + +```js +setIsSubset([, ]) +``` + +##### 示例代码 + 假设集合 `test` 存在以下数据: + + +```js +{ "_id": 1, "A": [ 1, 2 ], "B": [ 1, 2 ] } +{ "_id": 2, "A": [ 1, 2 ], "B": [ 2, 1, 2 ] } +{ "_id": 3, "A": [ 1, 2 ], "B": [ 1, 2, 3 ] } +{ "_id": 4, "A": [ 1, 2 ], "B": [ 3, 1 ] } +{ "_id": 5, "A": [ 1, 2 ], "B": [ ] } +{ "_id": 6, "A": [ 1, 2 ], "B": [ {}, [] ] } +{ "_id": 7, "A": [ ], "B": [ ] } +{ "_id": 8, "A": [ ], "B": [ 1 ] } +``` +下面的代码使用 `setIsSubset`,判断第一个集合是否是第二个集合的子集: + + +```js +let res = await db.collection('test') + .aggregate() + .project({ + AisSubsetOfB: $.setIsSubset(['$A', '$B']) + }) + .end() +``` + +```js +{ "_id": 1, "AisSubsetOfB": true } +{ "_id": 2, "AisSubsetOfB": true } +{ "_id": 3, "AisSubsetOfB": true } +{ "_id": 4, "AisSubsetOfB": false } +{ "_id": 5, "AisSubsetOfB": false } +{ "_id": 6, "AisSubsetOfB": false } +{ "_id": 7, "AisSubsetOfB": true } +{ "_id": 8, "AisSubsetOfB": true } +``` + +#### setUnion + +聚合操作符,输入两个集合,输出两个集合的并集。 + + +##### API 说明 + 使用形式如下: + + +```js +setUnion([, ]) +``` + +##### 示例代码 + 假设集合 `test` 存在以下数据: + + +```js +{ "_id": 1, "A": [ 1, 2 ], "B": [ 1, 2 ] } +{ "_id": 2, "A": [ 1, 2 ], "B": [ 2, 1, 2 ] } +{ "_id": 3, "A": [ 1, 2 ], "B": [ 1, 2, 3 ] } +{ "_id": 4, "A": [ 1, 2 ], "B": [ 3, 1 ] } +{ "_id": 5, "A": [ 1, 2 ], "B": [ ] } +{ "_id": 6, "A": [ 1, 2 ], "B": [ {}, [] ] } +{ "_id": 7, "A": [ ], "B": [ ] } +{ "_id": 8, "A": [ ], "B": [ 1 ] } +``` +下面的代码使用 `setUnion`,输出两个集合的并集: + + +```js +let res = await db.collection('test') + .aggregate() + .project({ + AB: $.setUnion(['$A', '$B']) + }) + .end() +``` +输出如下: + + +```js +{ "_id": 1, "AB": [ 1, 2 ] } +{ "_id": 2, "AB": [ 1, 2 ] } +{ "_id": 3, "AB": [ 1, 2, 3 ] } +{ "_id": 4, "AB": [ 1, 2, 3 ] } +{ "_id": 5, "AB": [ 1, 2 ] } +{ "_id": 6, "AB": [ 1, 2, {}, [] ] } +{ "_id": 7, "AB": [ ] } +{ "_id": 8, "AB": [ 1 ] } +``` + +### 字符串操作符 + +#### concat + +聚合操作符。连接字符串,返回拼接后的字符串。 + + +##### API 说明 + `concat` 的语法如下: + + +```js +db.command.aggregate.concat([<表达式1>, <表达式2>, ...]) +``` +表达式可以是形如 `$ + 指定字段`,也可以是普通字符串。只要能够被解析成字符串即可。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `concat` 可以拼接 `lastName` 和 `firstName` 字段,得到每位学生的名字全称: + + +```js +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .project({ + _id: 0, + fullName: $.concat(['$firstName', ' ', '$lastName']) + }) + .end() +``` +返回的结果如下: + + +```js +{ "fullName": "Yuanxin Dong" } +{ "fullName": "Weijia Wang" } +{ "fullName": "Chengxi Li" } +``` + +#### dateFromString + +见[dateFromString](#dateFromString) + +#### dateToString + +见[dateToString](#dateToString) + +#### indexOfBytes + +聚合操作符。在目标字符串中查找子字符串,并返回第一次出现的 `UTF-8` 的字节索引(从0开始)。如果不存在子字符串,返回 -1。 + + +##### API 说明 + `indexOfBytes` 的语法如下: + + +```js +db.command.aggregate.indexOfBytes([<目标字符串表达式>, <子字符串表达式>, <开始位置表达式>, <结束位置表达式>]) +``` +下面是 4 种表达式的详细描述: + +|表达式 |描述 | +|---- |---- | +|目标字符串表达式 |任何可以被解析为字符串的表达式 | +|子字符串表达式 |任何可以被解析为字符串的表达式 | +|开始位置表达式 |任何可以被解析为非负整数的表达式 | +|结束位置表达式 |任何可以被解析为非负整数的表达式 | + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `indexOfBytes` 查找字符 `"a"` 在字段 `firstName` 中的位置: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + aStrIndex: $.indexOfBytes(['$firstName', 'a']) + }) + .end() +``` +返回的结果如下: + + +```js +{ "aStrIndex": 2 } +{ "aStrIndex": 5 } +{ "aStrIndex": -1 } +``` + +#### indexOfCP + +聚合操作符。在目标字符串中查找子字符串,并返回第一次出现的 `UTF-8` 的 `code point` 索引(从0开始)。如果不存在子字符串,返回 -1。 + + +##### API 说明 + `code point` 是“码位”,又名“编码位置”。这里特指 `Unicode` 包中的码位,范围是从0(16进制)到10FFFF(16进制)。 + + `indexOfCP` 的语法如下: + + +```js +db.command.aggregate.indexOfCP([<目标字符串表达式>, <子字符串表达式>, <开始位置表达式>, <结束位置表达式>]) +``` +下面是 4 种表达式的详细描述: + +|表达式 |描述 | +|---- |---- | +|目标字符串表达式 |任何可以被解析为字符串的表达式 | +|子字符串表达式 |任何可以被解析为字符串的表达式 | +|开始位置表达式 |任何可以被解析为非负整数的表达式 | +|结束位置表达式 |任何可以被解析为非负整数的表达式 | + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `indexOfCP` 查找字符 `"a"` 在字段 `firstName` 中的位置: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + aStrIndex: $.indexOfCP(['$firstName', 'a']) + }) + .end() +``` +返回的结果如下: + + +```js +{ "aStrIndex": 2 } +{ "aStrIndex": 5 } +{ "aStrIndex": -1 } +``` + +#### split + +聚合操作符。按照分隔符分隔数组,并且删除分隔符,返回子字符串组成的数组。如果字符串无法找到分隔符进行分隔,返回原字符串作为数组的唯一元素。 + + +##### API 说明 + `split` 的语法如下: + + +```js +db.command.aggregate.split([<字符串表达式>, <分隔符表达式>]) +``` +字符串表达式和分隔符表达式可以是任意形式的表达式,只要它可以被解析为字符串即可。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "birthday": "1999/12/12" } +{ "birthday": "1998/11/11" } +{ "birthday": "1997/10/10" } +``` +通过 `split` 将每条记录中的 `birthday` 字段对应值分隔成数组,每个数组分别由代表年、月、日的3个元素组成: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + birthday: $.split(['$birthday', '/']) + }) + .end() +``` +返回的结果如下: + + +```js +{ "birthday": [ "1999", "12", "12" ] } +{ "birthday": [ "1998", "11", "11" ] } +{ "birthday": [ "1997", "10", "10" ] } +``` + +#### strLenBytes + +聚合操作符。计算并返回指定字符串中 `utf-8` 编码的字节数量。 + + +##### API 说明 + `strLenBytes` 的语法如下: + + +```js +db.command.aggregate.strLenBytes(<表达式>) +``` +只要表达式可以被解析成字符串,那么它就是有效表达式。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "name": "dongyuanxin", "nickname": "心谭" } +``` +借助 `strLenBytes` 计算 `name` 字段和 `nickname` 字段对应值的字节长度: + + +```js +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .project({ + _id: 0, + nameLength: $.strLenBytes('$name'), + nicknameLength: $.strLenBytes('$nickname') + }) + .end() +``` +返回结果如下: + + +```js +{ "nameLength": 11, "nicknameLength": 6 } +``` + +#### strLenCP + +聚合操作符。计算并返回指定字符串的UTF-8 [code points](http://www.unicode.org/glossary/#code_point) 数量。 + + +##### API 说明 + `strLenCP` 的语法如下: + + +```js +db.command.aggregate.strLenCP(<表达式>) +``` +只要表达式可以被解析成字符串,那么它就是有效表达式。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "name": "dongyuanxin", "nickname": "心谭" } +``` +借助 `strLenCP` 计算 `name` 字段和 `nickname` 字段对应值的UTF-8 [code points](http://www.unicode.org/glossary/#code_point)的数量: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + nameLength: $.strLenCP('$name'), + nicknameLength: $.strLenCP('$nickname') + }) + .end() +``` +返回结果如下: + + +```js +{ "nameLength": 11, "nicknameLength": 2 } +``` + +#### strcasecmp + +聚合操作符。对两个字符串在不区分大小写的情况下进行大小比较,并返回比较的结果。 + + +##### API 说明 + `strcasecmp` 的语法如下: + + +```js +db.command.aggregate.strcasecmp([<表达式1>, <表达式2>]) +``` +只要 `表达式1`和 `表达式2` 可以被解析成字符串,那么它们就是有效的。 + + 返回的比较结果有1,0和-1三种: + + +- 1:`表达式1` 解析的字符串 > `表达式2` 解析的字符串 - 0:`表达式1` 解析的字符串 = `表达式2` 解析的字符串 - -1:`表达式1` 解析的字符串 < `表达式2` 解析的字符串 + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `strcasecmp` 比较 `firstName` 字段值和 `lastName` 字段值的大小: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + result: $.strcasecmp(['$firstName', '$lastName']), + }) + .end() +``` +返回结果如下: + + +```js +{ "result": 1 } +{ "result": 1 } +{ "result": -1 } +``` + +#### substr + +聚合操作符。返回字符串从指定位置开始的指定长度的子字符串。它是 `db.command.aggregate.substrBytes` 的别名,更推荐使用后者。 + + +##### API 说明 + `substr` 的语法如下: + + +```js +db.command.aggregate.substr([<表达式1>, <表达式2>, <表达式3>]) +``` +`表达式1` 是任何可以解析为字符串的有效表达式,`表达式2` 和 `表达式3` 是任何可以解析为数字的有效表达式。 + + 如果 `表达式2` 是负数,返回的结果为 `""`。 + + 如果 `表达式3` 是负数,返回的结果为从 `表达式2` 指定的开始位置以及之后其余部分的子字符串。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "birthday": "1999/12/12", "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "birthday": "1998/11/11", "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "birthday": "1997/10/10", "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `substr` 可以提取 `birthday` 中的年、月、日信息,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + year: $.substr(['$birthday', 0, 4]), + month: $.substr(['$birthday', 5, 2]), + day: $.substr(['$birthday', 8, -1]) + }) + .end() +``` +返回的结果如下: + + +```js +{ "day": "12", "month": "12", "year": "1999" } +{ "day": "11", "month": "11", "year": "1998" } +{ "day": "10", "month": "10", "year": "1997" } +``` + +#### substrBytes + +聚合操作符。返回字符串从指定位置开始的指定长度的子字符串。子字符串是由字符串中指定的 `UTF-8` 字节索引的字符开始,长度为指定的字节数。 + + +##### API 说明 + `substrBytes` 的语法如下: + + +```js +db.command.aggregate.substrBytes([<表达式1>, <表达式2>, <表达式3>]) +``` +`表达式1` 是任何可以解析为字符串的有效表达式,`表达式2` 和 `表达式3` 是任何可以解析为数字的有效表达式。 + + 如果 `表达式2` 是负数,返回的结果为 `""`。 + + 如果 `表达式3` 是负数,返回的结果为从 `表达式2` 指定的开始位置以及之后其余部分的子字符串。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "birthday": "1999/12/12", "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "birthday": "1998/11/11", "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "birthday": "1997/10/10", "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `substrBytes` 可以提取 `birthday` 中的年、月、日信息,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + year: $.substrBytes(['$birthday', 0, 4]), + month: $.substrBytes(['$birthday', 5, 2]), + day: $.substrBytes(['$birthday', 8, -1]) + }) + .end() +``` +返回的结果如下: + + +```js +{ "day": "12", "month": "12", "year": "1999" } +{ "day": "11", "month": "11", "year": "1998" } +{ "day": "10", "month": "10", "year": "1997" } +``` + +#### substrCP + +聚合操作符。返回字符串从指定位置开始的指定长度的子字符串。子字符串是由字符串中指定的 `UTF-8` 字节索引的字符开始,长度为指定的字节数。 + + +##### API 说明 + `substrCP` 的语法如下: + + +```js +db.command.aggregate.substrCP([<表达式1>, <表达式2>, <表达式3>]) +``` +`表达式1` 是任何可以解析为字符串的有效表达式,`表达式2` 和 `表达式3` 是任何可以解析为数字的有效表达式。 + + 如果 `表达式2` 是负数,返回的结果为 `""`。 + + 如果 `表达式3` 是负数,返回的结果为从 `表达式2` 指定的开始位置以及之后其余部分的子字符串。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "name": "dongyuanxin", "nickname": "心谭" } +``` +借助 `substrCP` 可以提取 `nickname` 字段值的第一个汉字: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + firstCh: $.substrCP(['$nickname', 0, 1]) + }) + .end() +``` +返回的结果如下: + + +```js +{ "firstCh": "心" } +``` + +#### toLower + +聚合操作符。将字符串转化为小写并返回。 + + +##### API 说明 + `toLower` 的语法如下: + + +```js +db.command.aggregate.toLower(表达式) +``` +只要表达式可以被解析成字符串,那么它就是有效表达式。例如:`$ + 指定字段`。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `toLower` 将 `firstName` 的字段值转化为小写: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + result: $.toLower('$firstName'), + }) + .end() +``` +返回的结果如下: + + +```js +{ "result": "yuanxin" } +{ "result": "weijia" } +{ "result": "chengxi" } +``` + +#### toUpper + +聚合操作符。将字符串转化为大写并返回。 + + +##### API 说明 + `toUpper` 的语法如下: + + +```js +db.command.aggregate.toUpper(表达式) +``` +只要表达式可以被解析成字符串,那么它就是有效表达式。例如:`$ + 指定字段`。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "firstName": "Yuanxin", "group": "a", "lastName": "Dong", "score": 84 } +{ "firstName": "Weijia", "group": "a", "lastName": "Wang", "score": 96 } +{ "firstName": "Chengxi", "group": "b", "lastName": "Li", "score": 80 } +``` +借助 `toUpper` 将 `lastName` 的字段值转化为大写: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .project({ + _id: 0, + result: $.toUpper('$lastName'), + }) + .end() +``` +返回的结果如下: + + +```js +{ "result": "DONG" } +{ "result": "WANG" } +{ "result": "LI" } +``` + +### 累计器操作符 + +#### addToSet + +聚合操作符。聚合运算符。向数组中添加值,如果数组中已存在该值,不执行任何操作。它只能在 `group stage` 中使用。 + + +##### API 说明 + `addToSet` 语法如下: + + +```js +db.command.aggregate.addToSet(<表达式>) +``` +表达式是形如 `$ + 指定字段` 的字符串。如果指定字段的值是数组,那么整个数组会被当作一个元素。 + + +##### 示例代码 + 假设集合 `passages` 的记录如下: + + +```js +{ "category": "web", "tags": [ "JavaScript", "CSS" ], "title": "title1" } +{ "category": "System", "tags": [ "C++", "C" ], "title": "title2" } +``` + + +**非数组字段** + + 每条记录的 `category` 对应值的类型是非数组,利用 `addToSet` 统计所有分类: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('passages') + .aggregate() + .group({ + _id: null, + categories: $.addToSet('$category') + }) + .end() +``` +返回的结果如下: + + +```js +{ "_id": null, "categories": [ "System", "web" ] } +``` + + +**数组字段** + + 每条记录的 `tags` 对应值的类型是数组,数组不会被自动展开: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('passages') + .aggregate() + .group({ + _id: null, + tagsList: $.addToSet('$tags') + }) + .end() +``` +返回的结果如下: + + +```js +{ "_id": null, "tagsList": [ [ "C++", "C" ], [ "JavaScript", "CSS" ] ] } +``` + +#### avg + +聚合操作符。返回一组集合中,指定字段对应数据的平均值。 + + +##### API 说明 + `avg` 的语法如下: + + +```js +db.command.aggregate.avg() +``` +`avg` 传入的值除了数字常量外,也可以是任何最终解析成一个数字的表达式。它会忽略非数字值。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 `avg` 可以计算所有记录的 `score` 的平均值: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .group({ + _id: null, + average: $.avg('$score') + }) + .end() +``` +返回的结果如下: + + +```js +{ "_id": null, "average": 90 } +``` + +#### first + +聚合操作符。返回指定字段在一组集合的第一条记录对应的值。仅当这组集合是按照某种定义排序( `sort` )后,此操作才有意义。 + + +##### API 说明 + `first` 的语法如下: + + +```js +db.command.aggregate.first(<表达式>) +``` +表达式是形如 `$ + 指定字段` 的字符串。 + + `first` 只能在 `group` 阶段被使用,并且需要配合 `sort` 才有意义。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +如果需要得到所有记录中 `score` 的最小值,可以先将所有记录按照 `score` 排序,然后取出第一条记录的 `first`。 + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .sort({ + score: 1 + }) + .group({ + _id: null, + min: $.first('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": null, "min": 80 } +``` + +#### last + +聚合操作符。返回指定字段在一组集合的最后一条记录对应的值。仅当这组集合是按照某种定义排序( `sort` )后,此操作才有意义。 + + +##### API 说明 + `last` 的语法如下: + + +```js +db.command.aggregate.last(<表达式>) +``` +表达式是形如 `$ + 指定字段` 的字符串。 + + `last` 只能在 `group` 阶段被使用,并且需要配合 `sort` 才有意义。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +如果需要得到所有记录中 `score` 的最大值,可以先将所有记录按照 `score` 排序,然后取出最后一条记录的 `last`。 + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .sort({ + score: 1 + }) + .group({ + _id: null, + max: $.last('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": null, "max": 100 } +``` + +#### max + +聚合操作符。返回一组数值的最大值。 + + +##### API 说明 + `max` 的语法如下: + + +```js +db.command.aggregate.max(<表达式>) +``` +表达式是形如 `$ + 指定字段` 的字符串。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 `max` 可以统计不同组( `group` )中成绩的最高值,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .group({ + _id: '$group', + maxScore: $.max('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": "b", "maxScore": 100 } +{ "_id": "a", "maxScore": 96 } +... +``` + +#### mergeObjects + +见[mergeObjects](#mergeObjects) + +#### min + +聚合操作符。返回一组数值的最小值。 + + +##### API 说明 + `min` 的语法如下: + + +```js +db.command.aggregate.min(<表达式>) +``` +表达式是形如 `$ + 指定字段` 的字符串。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 `min` 可以统计不同组( `group` )中成绩的最低值,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .group({ + _id: '$group', + minScore: $.min('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": "b", "minScore": 80 } +{ "_id": "a", "minScore": 84 } +``` + +#### push + +聚合操作符。在 `group` 阶段,返回一组中表达式指定列与对应的值,一起组成的数组。 + + +##### API 说明 + `push` 语法如下: + + +```js +db.command.aggregate.push({ + <字段名1>: <指定字段1>, + <字段名2>: <指定字段2>, + ... +}) +``` + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 `push` 操作,对不同分组( `group` )的所有记录,聚合所有数据并且将其放入一个新的字段中,进一步结构化和语义化数据。 + + +```js +const $ = db.command.aggregate +let res = await db + .collection('students') + .aggregate() + .group({ + _id: '$group', + students: $.push({ + name: '$name', + score: '$score' + }) + }) + .end() +``` +输出结果如下: + + +```js +{ "_id": "b", "students": [{ "name": "stu3", "score": 80 }, { "name": "stu4", "score": 100 }] } +{ "_id": "a", "students": [{ "name": "stu1", "score": 84 }, { "name": "stu2", "score": 96 }] } +``` + +#### stdDevPop + +聚合操作符。返回一组字段对应值的标准差。 + + +##### API 说明 + `stdDevPop` 的使用形式如下: + + +```js +db.command.aggregate.stdDevPop(<表达式>) +``` +表达式传入的是指定字段,指定字段对应的值的数据类型必须是 `number` ,否则结果会返回 `null`。 + + +##### 示例代码 + 假设集合 `students` 的记录如下:`a` 组同学的成绩分别是84和96,`b`组同学的成绩分别是80和100。 + + +```js +{ "group":"a", "score":84 } +{ "group":"a", "score":96 } +{ "group":"b", "score":80 } +{ "group":"b", "score":100 } +``` +可以用 `stdDevPop` 来分别计算 `a` 和 `b` 两组同学成绩的标准差,以此来比较哪一组同学的成绩更稳定。代码如下: + + +```js +const $ = db.command.aggregate +let res = await db.collection('students').aggregate() + .group({ + _id: '$group', + stdDev: $.stdDevPop('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": "b", "stdDev": 10 } +{ "_id": "a", "stdDev": 6 } +``` + +#### stdDevSamp + +聚合操作符。计算输入值的样本标准偏差。如果输入值代表数据总体,或者不概括更多的数据,请改用 `db.command.aggregate.stdDevPop`。 + + +##### API 说明 + `stdDevSamp` 的使用形式如下: + + +```js +db.command.aggregate.stdDevSamp(<表达式>) +``` +表达式传入的是指定字段,`stdDevSamp` 会自动忽略非数字值。如果指定字段所有的值均是非数字,那么结果返回 `null`。 + + +##### 示例代码 + 假设集合 `students` 的记录如下: + + +```js +{ "score": 80 } +{ "score": 100 } +``` +可以用 `stdDevSamp` 来计算成绩的标准样本偏差。代码如下: + + +```js +const $ = db.command.aggregate +let res = await db.collection('students').aggregate() + .group({ + _id: null, + ageStdDev: $.stdDevSamp('$score') + }) + .end() +``` +返回的数据结果如下: + + +```js +{ "_id": null, "ageStdDev": 14.142135623730951 } +``` +如果向集合 `students` 添加一条新记录,它的 `score` 字段类型是 `string`: + + +```js +{ "score": "aa" } +``` +用上面代码计算标准样本偏差时,`stdDevSamp` 会自动忽略类型不为 `number` 的记录,返回结果保持不变。 + +#### sum + +聚合操作符。计算并且返回一组字段所有数值的总和。 + + +##### API 说明 + `sum` 的使用形式如下: + + +```js +db.command.aggregate.sum(<表达式>) +``` +表达式可以传入指定字段,也可以传入指定字段组成的列表。`sum` 会自动忽略非数字值。如果字段下的所有值均是非数字,那么结果返回 0。若传入数字常量,则当做所有记录该字段的值都给给定常量,在聚合时相加,最终值为输入记录数乘以常量。 + + +##### 示例代码 + 假设代表商品的集合 `goods` 的记录如下:`price` 代表商品销售额,`cost` 代表商品成本 + + +```js +{ "cost": -10, "price": 100 } +{ "cost": -15, "price": 1 } +{ "cost": -10, "price": 10 } +``` + + +**单独字段** + + 借助 `sum` 可以计算所有商品的销售总和,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('goods') + .aggregate() + .group({ + _id: null, + totalPrice: $.sum('$price') + }) + .end() +``` +返回的数据结果如下:销售额是 111 + + +```js +{ "_id": null, "totalPrice": 111 } +``` + + +**字段列表** + + 如果需要计算所有商品的利润总额,那么需要将每条记录的 `cost` 和 `price` 相加得到此记录对应商品的利润。最后再计算所有商品的利润总额。 + + 借助 `sum`,代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('goods') + .aggregate() + .group({ + _id: null, + totalProfit: $.sum( + $.sum(['$price', '$cost']) + ) + }) + .end() +``` +返回的数据结果如下:利润总额为 76 + + +```js +{ "_id": null, "totalProfit": 76 } +``` + +### 变量操作符 + +#### let + +聚合操作符。自定义变量,并且在指定表达式中使用,返回的结果是表达式的结果。 + + +##### API 说明 + `let` 的语法如下: + + +```js +db.command.aggregate.let({ + vars: { + <变量1>: <变量表达式>, + <变量2>: <变量表达式>, + ... + }, + in: <结果表达式> +}) +``` +`vars` 中可以定义多个变量,变量的值由 `变量表达式` 计算而来,并且被定义的变量只有在 `in` 中的 `结果表达式` 才可以访问。 + + 在 `in` 的结果表达式中访问自定义变量时候,请在变量名前加上双美元符号( `$$` )并用引号括起来。 + + +##### 示例代码 + 假设代表商品的集合 `goods` 的记录如下:`price` 代表商品价格,`discount` 代表商品折扣率,`cost` 代表商品成本 + + +```js +{ "cost": -10, "discount": 0.95, "price": 100 } +{ "cost": -15, "discount": 0.98, "price": 1 } +{ "cost": -10, "discount": 1, "price": 10 } +``` +借助 `let` 可以定义并计算每件商品实际的销售价格,并将其赋值给自定义变量 `priceTotal`。最后再将 `priceTotal` 与 `cost` 进行取和( `sum` )运算,得到每件商品的利润。 + + 代码如下: + + +```js +const $ = db.command.aggregate +let res = await db + .collection('goods') + .aggregate() + .project({ + profit: $.let({ + vars: { + priceTotal: $.multiply(['$price', '$discount']) + }, + in: $.sum(['$$priceTotal', '$cost']) + }) + }) + .end() +``` +返回的数据结果如下: + + +``` +{ "profit": 85 } +{ "profit": -14.02 } +{ "profit": 0 } +``` + 关系型数据库和 JSON 文档型数据库的概念对应关系如下表: