## 云数据库简介
`uniCloud`提供了一个 JSON 格式的文档型数据库,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。
关系型数据库和 JSON 文档型数据库的概念对应关系如下表:
|关系型 |JSON 文档型 |
|:- |:- |
|数据库 database|数据库 database |
|表 table |集合 collection |
|行 row |记录 record / doc|
|列 column |字段 field |
`uniCloud`云函数中可访问云数据库。
鉴于安全问题,暂不支持客户端直接访问数据库。
**阿里云使用的mongoDB数据库版本为3.4,腾讯云使用的版本是4.0。此差异可能会导致本文档内的部分功能不能在阿里云使用,我们会进行标注,如果发现有遗漏欢迎向我们反馈**
**如果不想使用此数据库,可以自行连接其他数据库,用法可以参考nodejs连接数据库**
## 获取数据库的引用
```js
const db = uniCloud.database();
```
**DBOptions参数说明**
|字段 |类型 |必填 |描述 |平台差异说明 |
|:-: |:-: |:-: |:-: |:-: |
|spaceId|String |否 |同一账号下的,服务空间ID |仅腾讯云支持 |
## 新增集合
如果集合已存在,则报错。
**平台差异说明**
|阿里云 |腾讯云 |
|---- |---- |
|× |√ |
```
db.createCollection(collectionName)
```
## 获取集合的引用
```js
// 获取 `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
**注意**
- 阿里云数据库在存入emoji表情时会导致uniCloud控制台无法获取数据列表,目前阿里正在处理此问题,开发者可以先自行过滤一下
以下对几个特殊的数据类型做个补充说明
### 时间 Date
Date 类型用于表示时间,精确到毫秒,可以用 JavaScript 内置 Date 对象创建。需要特别注意的是,用此方法创建的时间是客户端时间,不是服务端时间。如果需要使用服务端时间,应该用 API 中提供的 serverDate 对象来创建一个服务端当前时间的标记,当使用了 serverDate 对象的请求抵达服务端处理时,该字段会被转换成服务端当前的时间,更棒的是,我们在构造 serverDate 对象时还可通过传入一个有 offset 字段的对象来标记一个与当前服务端时间偏移 offset 毫秒的时间,这样我们就可以达到比如如下效果:指定一个字段为服务端时间往后一个小时。
那么当我们需要使用客户端时间时,存放 Date 对象和存放毫秒数是否是一样的效果呢?不是的,我们的数据库有针对日期类型的优化,建议大家使用时都用 Date 或 serverDate 构造时间对象。
```js
//服务端当前时间
new db.serverDate()
```
```js
//服务端当前时间加1S
new db.serverDate({
offset: 1000
})
```
**Tips**
- 使用阿里云作为服务提供商时,如需存入日期类型,需要`2020-02-10T04:59:05.579Z`形式,即可以在云函数中使用`new Date().toISOString()`得到。
### 地理位置
**阿里云暂不支持地理位置类型**
参考:[GEO地理位置](#GEO地理位置)
### Null
Null 相当于一个占位符,表示一个字段存在但是值为空。
## 新增文档
方法1: collection.add(data)
示例:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ---------------------------------------- |
| data | object | array | 是 | {_id: '10001', 'name': 'Ben'} _id 非必填|
```js
// 单条插入数据
let res = await collection.add({
name: 'Ben'
})
// 批量插入数据,腾讯云暂不支持
let res = await collection.add([{
name: 'Alex'
},{
name: 'Ben'
},{
name: 'John'
}])
// res.inserted // 插入成功条数
// res.result // 阿里云特有,批量插入返回的所有记录 id
```
**Tips**
- 云服务商为阿里云时,若集合不存在,调用add方法会自动创建集合
方法2: collection.doc().set(data)
也可通过 `set` 方法新增一个文档,需先取得文档引用再调用 `set` 方法。
如果文档不存在,`set` 方法会创建一个新文档。
```js
let res = await collection.doc().set({
name: "Hey"
});
```
## 查询文档
支持 `where()`、`limit()`、`skip()`、`orderBy()`、`get()`、`update()`、`field()`、`count()` 等操作。
只有当调用`get()` `update()`时才会真正发送请求。
注:默认取前100条数据,最大取前100条数据。
### 添加查询条件
collection.where()
参数
设置过滤条件
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
}
})
```
`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()
```
响应参数
| 字段 | 类型 | 必填 | 说明 |
| --------- | ------- | ---- | ------------------------ |
| code | string | 否 | 状态码,操作成功则不返回 |
| message | string | 否 | 错误描述 |
| total | Integer | 否 | 计数结果 |
| requestId | string | 否 | 请求序列号,用于错误排查 |
### 设置记录数量
collection.limit()
参数说明
| 参数 | 类型 | 必填 | 说明 |
| ----- | ------- | ---- | -------------- |
| value | Integer | 是 | 限制展示的数值 |
使用示例
```js
let res = await collection.limit(1).get()
```
### 设置起始位置
collection.skip()
参数说明
| 参数 | 类型 | 必填 | 说明 |
| ----- | ------- | ---- | -------------- |
| value | Integer | 是 | 跳过展示的数据 |
使用示例
```js
let res = await collection.skip(4).get()
```
### 对结果排序
collection.orderBy()
参数说明
| 参数 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ----------------------------------- |
| field | string | 是 | 排序的字段 |
| orderType | string | 是 | 排序的顺序,升序(asc) 或 降序(desc) |
使用示例
```js
let res = await collection.orderBy("name", "asc").get()
```
### 指定返回字段
collection.field()
参数说明
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | --------------------------------------- |
| - | object | 是 | 要过滤的字段,不返回传false,返回传true |
使用示例
```js
collection.field({ 'age': true })
```
备注:只能指定要返回的字段或者不要返回的字段。即{'a': true, 'b': false}是一种错误的参数格式
### 查询指令
#### eq
表示字段等于某个值。`eq` 指令接受一个字面量 (literal),可以是 `number`, `boolean`, `string`, `object`, `array`。
比如筛选出所有自己发表的文章,除了用传对象的方式:
```js
const myOpenID = "xxx"
let res = await db.collection('articles').where({
_openid: myOpenID
}).get()
```
还可以用指令:
```js
const dbCmd = db.command
const myOpenID = "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
字段不等于。`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()
```
#### gt
字段大于指定值。
如筛选出价格大于 2000 的计算机:
```js
const dbCmd = db.command
let res = await db.collection('goods').where({
category: 'computer',
price: dbCmd.gt(2000)
}).get()
```
#### gte
字段大于或等于指定值。
#### lt
字段小于指定值。
#### lte
字段小于或等于指定值。
#### in
字段值在给定的数组中。
筛选出内存为 8g 或 16g 的计算机商品:
```js
const dbCmd = db.command
let res = await db.collection('goods').where({
category: 'computer',
type: {
memory: dbCmd.in([8, 16])
}
}).get()
```
#### nin
字段值不在给定的数组中。
筛选出内存不是 8g 或 16g 的计算机商品:
```js
const dbCmd = db.command
db.collection('goods').where({
category: 'computer',
type: {
memory: dbCmd.nin([8, 16])
}
})
```
#### and
表示需同时满足指定的两个或以上的条件。
如筛选出内存大于 4g 小于 32g 的计算机商品:
流式写法:
```js
const dbCmd = db.command
db.collection('goods').where({
category: 'computer',
type: {
memory: dbCmd.gt(4).and(dbCmd.lt(32))
}
})
```
前置写法:
```js
const dbCmd = db.command
db.collection('goods').where({
category: 'computer',
type: {
memory: dbCmd.and(dbCmd.gt(4), dbCmd.lt(32))
}
})
```
#### 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)))
}
})
```
前置写法:
```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
}
}
))
```
### 正则表达式查询
#### db.RegExp
根据正则表达式进行筛选
例如下面可以筛选出 `version` 字段开头是 "数字+s" 的记录,并且忽略大小写:
```js
// 可以直接使用正则表达式
db.collection('articles').where({
version: /^\ds/i
})
// 或者,这种方式阿里云不支持
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()
```
## 更新文档
### 更新指定文档
collection.doc().update()
```js
let res = await collection.doc('doc-id').update({
name: "Hey",
count: {
fav: 1
}
});
```
```
// 更新前
{
"_id": "xxx",
"name": "Hello",
"count": {
"fav": 0,
"follow": 0
}
}
// 更新后
{
"_id": "xxx",
"name": "Hey",
"count": {
"fav": 1,
"follow": 0
}
}
```
更新数组时,已数组下标作为key即可,比如以下示例将数组arr内下标为1的值修改为 uniCloud
```js
let res = await collection.doc('doc-id').update({
arr: {
1: "uniCloud"
}
})
```
```json
// 更新前
{
"arr": ["hello", "world"]
}
// 更新后
{
"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": "xxx",
"name": "Hello",
"count": {
"fav": 0,
"follow": 0
}
}
// 更新后
{
"_id": "xxx",
"name": "Hey",
"count": {
"fav": 1
}
}
```
### 批量更新文档
collection.update()
```js
const dbCmd = db.command
let res = await collection.where({name: dbCmd.eq('hey')}).update({
age: 18,
})
```
### 更新操作符
更多数据库操作符请查看[数据库操作符](#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
})
})
```
```
// 更新前
{
"_id": "xxx",
"name": "Hello",
"count": {
"fav": 0,
"follow": 0
}
}
// 更新后
{
"_id": "xxx",
"name": "Hello",
"count": {
"fav": 1,
"follow": 1
}
}
```
#### inc
更新指令。用于指示字段自增某个值,这是个原子操作,使用这个操作指令而不是先读数据、再加、再写回的好处是:
1. 原子性:多个用户同时写,对数据库来说都是将字段加一,不会有后来者覆写前者的情况
2. 减少一次网络请求:不需先读再写
之后的 mul 指令同理。
如给收藏的商品数量加一:
```js
const dbCmd = db.command
let res = await db.collection('user').where({
_id: 'my-doc-id'
}).update({
count: {
fav: dbCmd.inc(1)
}
})
```
```
// 更新前
{
"_id": "xxx",
"name": "Hello",
"count": {
"fav": 0,
"follow": 0
}
}
// 更新后
{
"_id": "xxx",
"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)
}
})
```
```
// 更新前
{
"_id": "xxx",
"name": "Hello",
"count": {
"fav": 2,
"follow": 0
}
}
// 更新后
{
"_id": "xxx",
"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()
})
```
```
// 更新前
{
"_id": "xxx",
"rating": 5,
"comment": "xxx"
}
// 更新后
{
"_id": "xxx",
"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": "xxx",
"users": ["a","b"]
}
// 更新后
{
"_id": "xxx",
"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": "xxx",
"users": ["a","b"]
}
// 更新后
{
"_id": "xxx",
"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": "xxx",
"users": ["a","b"]
}
// 更新后
{
"_id": "xxx",
"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": "xxx",
"users": ["a","b"]
}
// 更新后
{
"_id": "xxx",
"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 _ = 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) {
const updateAAARes = await transaction.collection('account').doc('aaa').update({
data: {
amount: _.inc(-10)
}
})
const updateBBBRes = await transaction.collection('account').doc('bbb').update({
data: {
amount: _.inc(10)
}
})
console.log(`transaction succeeded`)
// 会作为 runTransaction resolve 的结果返回
return {
aaaAccount: aaaRes.data.amount - 10,
}
} 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 _ = db.command
exports.main = async (event) => {
try {
const transaction = await db.startTransaction()
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({
data: {
amount: _.inc(-10)
}
})
const updateBBBRes = await transaction.collection('account').doc('bbb').update({
data: {
amount: _.inc(10)
}
})
await transaction.commit()
console.log(`transaction succeeded`)
return {
success: true,
aaaAccount: aaaRes.data.amount - 10,
}
} else {
await transaction.rollback()
return {
success: false,
error: `rollback`,
rollbackCode: -100,
}
}
} catch (e) {
console.error(`transaction error`, e)
return {
success: false,
error: e
}
}
}
```
## 聚合操作
获取数据库集合的聚合操作实例
```js
db.collection('scores').aggregate()
```
### 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()
.bucket({
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: 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。输出记录中也可以包括累计值,将输出字段设为累计值即会从该分组中计算累计值。
**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 *,