# 数据库

# 基础概念

# Collection

集合是一系列的文档集,通过 db.collection(name) 可以获取指定集合的引用,在集合上可以进行以下操作

类型 接口 说明
add 新增文档(触发请求)
计数 count 获取复合条件的文档条数
get 获取集合中的文档,如果有使用 where 语句定义查询条件,则会返回匹配结果集 (触发请求)
引用 doc 获取对该集合中指定 id 的文档的引用
查询条件 where 通过指定条件筛选出匹配的文档,可搭配查询指令(eq, gt, in, ...)使用
skip 跳过指定数量的文档,常用于分页,传入 offset
orderBy 排序方式
limit 返回的结果集(文档数量)的限制,有默认值和上限值
field 指定需要返回的字段

查询及更新指令用于在 where 中指定字段需满足的条件,指令可通过 db.command 对象取得。

# Record / Document

文档是数据库集合中的一个存储单元,在云开发里是一个json对象。通过 db.collection(collectionName).doc(docId) 可以获取指定集合上指定 id 的文档的引用,在文档上可以进行以下操作

接口 说明
set
update
remove
get

# Query Command

查询指令,应用于构建查询条件。以下指令皆挂载在 db.command

类型 接口 说明
比较运算 eq 字段 ==
neq 字段 !=
gt 字段 >
gte 字段 >=
lt 字段 <
lte 字段 <=
in 字段值在数组里
nin 字段值不在数组里
逻辑运算 and 表示需同时满足指定的所有条件
or 表示需同时满足指定条件中的至少一个

# Update Command

更新指令,应用于构建更新操作。以下指令皆挂载在 db.command

类型 接口 说明
更新指令 set 设定字段等于指定值
inc 指示字段自增某个值
mul 指示字段自乘某个值
remove 删除某个字段
push 向数组尾部追加元素,支持传入单个元素或数组
pop 删除数组尾部元素
shift 删除数组头部元素。使用同pop
unshift 向数组头部添加元素,支持传入单个元素或数组。使用同push

# 获取数据库实例

说明:传入 CloudBaseCore 实例,返回数据库的实例

import 'package:cloudbase_core/cloudbase_core.dart';
import 'package:cloudbase_database/cloudbase_database.dart';

void main() async {
  CloudBaseCore core = CloudBaseCore.init({'env': 'your-env-id'});

  CloudBaseDatabase db = CloudBaseDatabase(core);
}

# 获取集合的引用

说明:接受一个 name 参数,指定需引用的集合名称

// 获取 `user` 集合的引用
import 'package:cloudbase_core/cloudbase_core.dart';
import 'package:cloudbase_database/cloudbase_database.dart';

void main() async {
  CloudBaseCore core = CloudBaseCore.init({'env': 'your-env-id'});

  CloudBaseDatabase db = CloudBaseDatabase(core);
  
  Collection collection = db.collection('user');
}

# 查询指令

# eq

表示字段等于某个值。eq 指令接受一个字面量 (literal),可以是 num, bool, String, Map, List

比如筛选出所有自己发表的文章,除了用传对象的方式:

var myOpenID = 'xxx';
db.collection('articles').where({
  '_openid': myOpenID
});

还可以用指令:

var _ = db.command;
var myOpenID = 'xxx';
db.collection('articles').where({
  '_openid': _.eq(myOpenID)
});

注意 eq 指令比对象的方式有更大的灵活性,可以用于表示字段等于某个对象的情况,比如:

// 这种写法表示匹配 stat['publishYear'] == 2018 且 stat['language'] == 'zh-CN'
db.collection('articles').where({
  'stat': {
    'publishYear': 2018,
    'language': 'zh-CN'
  }
})
// 这种写法表示 stat == { 'publishYear': 2018, 'language': 'zh-CN' }
var _ = db.command
db.collection('articles').where({
  'stat': _.eq({
    'publishYear': 2018,
    'language': 'zh-CN'
  })
})

# neq

字段不等于。neq 指令接受一个字面量 (literal),可以是 num, bool, String, Map, List

如筛选出品牌不为 X 的计算机:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'brand': _.neq('X')
  }
});

# gt

字段大于指定值。

如筛选出价格大于 2000 的计算机:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'price': _.gt(2000)
});

# gte

字段大于或等于指定值。

# lt

字段小于指定值。

# lte

字段小于或等于指定值。

# into

字段值在给定的数组中。

筛选出内存为 8g 或 16g 的计算机商品:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': _.into([8, 16])
  }
});

# nin

字段值不在给定的数组中。

筛选出内存不是 8g 或 16g 的计算机商品:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': _.nin([8, 16])
  }
});

# and

表示需同时满足指定的两个或以上的条件。

如筛选出内存大于 4g 小于 32g 的计算机商品:

流式写法:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': _.gt(4).and(_.lt(32))
  }
});

前置写法:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': _.and([_.gt(4), _.lt(32)])
  }
});

# or

表示需满足所有指定条件中的至少一个。如筛选出价格小于 4000 或在 6000-8000 之间的计算机:

流式写法:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'price': _.lt(4000).or(_.gt(6000).and(_.lt(8000)))
  }
});

前置写法:

var _ = db.command;
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'price': _.or([_.lt(4000), _.and([_.gt(6000), _.lt(8000)])])
  }
});

如果要跨字段 “或” 操作:(如筛选出内存 8g 或 cpu 3.2 ghz 的计算机)

var _ = db.command;
db.collection('goods').where(_.or([
  {
    'type': {
      'memory': _.gt(8)
    }
  },
  {
    'type': {
      'cpu': 3.2
    }
  }
]));

# RegExp

根据正则表达式进行筛选

例如下面可以筛选出 version 字段开头是 "数字+s" 的文档,并且忽略大小写:

db.collection('articles').where({
  version: db.regExp(
    '^\\ds',   // 正则表达式/^\ds/,转义后变成 '^\\ds'
    'i'        // options, i表示忽略大小写
  )
});

# 更新指令

# set

描述:用于设定字段等于指定值。

示例代码:

// 以下方法只会更新 property['location'] 和 property['size'],如果 property 对象中有
db.collection('photo').doc('doc-id').update({
  'data': {
    'property': {
      'location': 'guangzhou',
      'size': 8
    }
  }
}).then((res) {
  
});

# inc

描述:用于指示字段自增某个值,这是个原子操作,使用这个操作指令而不是先读数据、再加、再写回的好处是:

  1. 原子性:多个用户同时写,对数据库来说都是将字段加一,不会有后来者覆写前者的情况
  2. 减少一次网络请求:不需先读再写

之后的 mul 指令同理。

示例代码:

var _ = db.command;
db.collection('user').where({
  '_openid': 'my-open-id'
}).update({
  'count': {
    'favorites': _.inc(1)
  }
}).then((res) {
  
});

# mul

描述:用于指示字段自乘某个值。

# remove

更新指令。用于表示删除某个字段。如某人删除了自己一条商品评价中的评分:

var _ = db.command;
db.collection('comments').doc('comment-id').update({
  'rating': _.remove()
}).then((res) {
  
});

# push

向数组尾部追加元素,支持传入单个元素或数组

var _ = db.command;
db.collection('comments').doc('comment-id').update({
  // 'users': _.push('aaa')
  'users': _.push(['aaa', 'bbb'])
}).then((res) {
  
});

# pop

删除数组尾部元素

var _ = db.command;
db.collection('comments').doc('comment-id').update({
  'users': _.pop()
}).then((res) {
  
});

# unshift

向数组头部添加元素,支持传入单个元素或数组。使用同push

# shift

删除数组头部元素。使用同pop

# 构建查询条件

支持 where()limit()skip()orderBy()get()update()field()count() 等操作。

只有当调用get()update()时才会真正发送请求。

# where

描述:设置过滤条件。where 可接收对象作为参数,表示筛选出拥有和传入对象相同的 key-value 的文档。

输入参数: 无

比如筛选出所有类型为计算机的、内存为 8g 的商品:

db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': 8,
  }
});

如果要表达更复杂的查询,可使用高级查询指令,比如筛选出所有内存大于 8g 的计算机商品:

var _ = db.command // 取指令
db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': _.gt(8), // 表示大于 8
  }
})

# limit

描述:指定查询结果集数量上限

输入参数:

参数 类型 必填 说明
- int 限制展示的数值

使用示例

collection.limit(1).get().then((res) {
  
});

# skip

描述:指定查询返回结果时从指定序列后的结果开始返回,常用于分页 输入参数:

参数 类型 必填 说明
- int 限制展示的数值

示例代码

collection.skip(4).get().then((res) {

});

# field

描述:指定返回结果中文档需返回的字段

输入参数:

参数 类型 必填 说明
projection Map<String, bool> 要过滤的字段,不返回传false,返回传true

示例代码

collection.field({ 'age': true }).get().then((res) {

});

备注:field方法接受一个必填对象用于指定需返回的字段,对象的各个 key 表示要返回或不要返回的字段,value 传入 true|false 表示要返回还是不要返回。

# orderBy

描述:指定查询排序条件

输入参数:

参数 类型 必填 说明
field String 排序的字段
orderType String 排序的顺序,升序(asc) 或 降序(desc)

备注:方法接受一个必填字符串参数 fieldName 用于定义需要排序的字段,一个字符串参数 order 定义排序顺序。order 只能取 asc 或 desc。

同时也支持按多个字段排序,多次调用 orderBy 即可,多字段排序时的顺序会按照 orderBy 调用顺序先后对多个字段排序

示例代码

collection.orderBy("name", "asc").get().then((res) {

});

# add

# 1. 接口描述

接口功能:插入一条文档

接口声明:Future<DbCreateResponse> collection.add(dynamic data) async {}

备注:set方法也可以用来新增文档,请参看文档更新部分set方法

示例:

# 2. 输入参数

参数 类型 必填 说明
data dynamic {'_id': '10001', 'name': 'Ben'} _id 非必填

# 3. 输出参数

字段 类型 必填 说明
code String 状态码,操作成功则不返回
message String 错误描述

# 4. 示例代码

collection.add({
  'name': 'Ben'
}).then((res) {
  
}).catchError((e) {

});

# get

# 1. 接口描述

接口功能:获取数据库查询结果

接口声明:Future<DbQueryResponse> get() async {}

注:get()如不指定limit则默认取前100条数据,且最大取前100条数据。

# 2. 输入参数

# 3. 输出参数

字段 类型 必填 说明
code String 状态码,操作成功则不返回
message String 错误描述
data dynamic 查询结果
requestId String 请求序列号,用于错误排查

# 4. 示例代码

collection.where({
  'category': 'computer',
  'type': {
    'memory': 8,
  }
}).get().then((res) {
  var data = res.data;
}).catchError((e) => {

});

# count

# 1. 接口描述

接口功能:获取数据库查询结果

接口声明:Future<DbQueryResponse> count() async {}

# 2. 输入参数

# 3. 输出参数

字段 类型 必填 说明
code String 状态码,操作成功则不返回
message String 错误描述
total int 计数结果
requestId String 请求序列号,用于错误排查

# 4. 示例代码

db.collection('goods').where({
  'category': 'computer',
  'type': {
    'memory': 8,
  }
}).count().then((res) {
  var total = res.total; //符合条件的文档的数量
})

# remove

# 1. 接口描述

接口功能:删除一条文档

接口声明:Future<DbRemoveResponse> remove() async {}

# 2. 输入参数

# 3. 输出参数

字段 类型 必填 说明
code String 状态码,操作成功则不返回
message String 错误描述
deleted int 删除的文档数量
requestId String 请求序列号,用于错误排查

# 4. 示例代码

方式1:通过指定文档ID删除

collection.doc('xxx').remove()
  .then((res) {
    var deleted = res.deleted;
  })
  .catchError((e) {

  });

方式2:条件查找文档然后直接批量删除

// 删除字段a的值大于2的文档
collection.where({
  'a': _.gt(2)
}).remove().then((res) {
  var deleted = res.deleted;
})

# update / set

# 1. 接口描述

接口功能:更新文档

接口声明:

Future<DbUpdateResponse> update(dynamic data) async {}

Future<DbUpdateResponse> set(dynamic data) async {}

备注:update和set都可以用来更新文档,区别是set方法在要更新的文档不存在时新增一个文档;而update方法什么也不会做,返回updated为0

# 2. 输入参数

字段 类型 必填 说明
data dynamic 替换文档的定义

# 3. 输出参数

字段 类型 必填 说明
code String 状态码,操作成功则不返回
message String 错误描述
updated int 影响的文档数量
upsertedId String 插入的文档的id
requestId String 请求序列号,用于错误排查

# 4. 示例代码

更新指定文档

//更新指定文档
collection.doc('doc-id').update({
  'name': "Hey"
}).then((res) {
  
});

更新文档,如果不存在则创建

//更新单个文档
collection.doc('doc-id').set({
  'name': "Hey"
}).then((res) {
  
});

//批量更新文档
collection.where({'name': _.eq('hey')}).update({
  'age': 18,
}).then((res) {
  
});

# GEO地理位置

注意:如果需要对类型为地理位置的字段进行搜索,一定要建立地理位置索引

# GEO数据类型

# Point

用于表示地理位置点,用经纬度唯一标记一个点,这是一个特殊的数据存储类型。

签名:Point(num longitude, num latitude)

示例:

var ponit = new Point(longitude, latitude);

# LineString

用于表示地理路径,是由两个或者更多的 Point 组成的线段。

签名:LineString(List<Point> points)

示例:

var line = new LineString([
  new Point(lngA, latA),
  new Point(lngB, latB),
  // ...
]);

# Polygon

用于表示地理上的一个多边形(有洞或无洞均可),它是由一个或多个闭环 LineString 组成的几何图形。

由一个环组成的 Polygon 是没有洞的多边形,由多个环组成的是有洞的多边形。对由多个环(LineString)组成的多边形(Polygon),第一个环是外环,所有其他环是内环(洞)。

签名:Polygon(List<LineString> lines)

示例:

var polygon = new Polygon([
  new LineString(...),
  new LineString(...),
  // ...
]);

# MultiPoint

用于表示多个点 Point 的集合。

签名:MultiPoint(List<Point> points)

示例:

var points = new MultiPoint([
  new Point(lngA, latA),
  new Point(lngB, latB),
  // ...
]);

# MultiLineString

用于表示多个地理路径 LineString 的集合。

签名:MultiLineString(List<LineString> lines)

示例:

var lines = new MultiLineString([
  new LineString(...),
  new LineString(...),
  // ...
]);

# MultiPolygon

用于表示多个地理多边形 Polygon 的集合。

签名:MultiPolygon(List<Polygon> polygons)

示例:

var polygons = new MultiPolygon([
  new Polygon(...),
  new Polygon(...),
  // ...
]);

# GEO操作符

# geoNear

按从近到远的顺序,找出字段值在给定点的附近的文档。

示例:

db.collection('user').where({
  'location': db.command.geoNear(
    new Point(lngA, latA),
    maxDistance: 1000,
    minDistance: 0
  )
});

# geoWithin

找出字段值在指定 Polygon / MultiPolygon 内的文档,无排序

示例:

// 一个闭合的区域
var area = new Polygon([
  new LineString([
    new Point(lngA, latA),
    new Point(lngB, latB),
    new Point(lngC, latC),
    new Point(lngA, latA)
  ]),
]);

// 搜索 location 字段在这个区域中的 user
db.collection('user').where({
  'location': db.command.geoWithin(area)
});

# geoIntersects

找出字段值和给定的地理位置图形相交的文档

示例:

// 一条路径
var line = new LineString([
  new Point(lngA, latA),
  new Point(lngB, latB)
]);

// 搜索 location 与这条路径相交的 user
db.collection('user').where({
  'location': db.command.geoIntersects(line)
});

# 数据库实时推送

监听指定集合中符合查询条件的文档,通过onChange回调获得文档的变化详情 (where参数为查询条件 参考 查询文档)

  CloudBaseCore core = CloudBaseCore.init({'env': 'your-env-id'});
  CloudBaseDatabase db = CloudBaseDatabase(core);
  Command _ = db.command;
  Collection collection = db.collection('collName'); // collName 需填当前env下集合名称

  RealtimeListener ref = collection.where({ 'test': _.gt(0) }).watch(
    onChange: (Snapshot snapshot) {
      print('收到snapshot********** $snapshot');
    },
    onError: (error) {
      print('收到error********** $error');
    }
  );

单个doc的监听,也可以采用doc('docId').watch()形式

  RealtimeListener ref = collection.doc('one docId').watch(
    onChange: (Snapshot snapshot) {
      print('收到snapshot********** $snapshot');
    },
    onError: (error) {
      print('收到error********** $error');
    }
  );

手动关闭监听,当前监听将不再收到推送

  await ref.close();