第3章云数据库 云数据库提供高性能的数据库写入和查询服务。通过腾讯云开发(Tencent Cloud Base, TCB)的SDK,可以直接在客户端对数据进行读写,也可以在云函数中读写数据,还可以通过控制台对数据进行可视化的增、删、查、改等操作。微信小程序云开发所使用的数据库本质上就是一个MongoDB数据库。MongoDB数据库是介于关系数据库和非关系数据库之间的产品,是非关系数据库中功能最丰富、最像关系数据库的。 数据库: 默认情况下,云开发的函数可以使用当前环境对应的数据库。可以根据需要使用不同的数据库。对应MySQL中的数据库。 集合: 数据库中多个记录的集合。对应MySQL中的表。 文档: 数据库中的一条记录。对应MySQL中的行。 字段: 数据库中特定记录的值。对应MySQL中的列。 3.1云数据库上手 1. 创建第一个集合 打开云开发控制台,选择“数据库”标签页,通过单击集合名称右侧的“+”按钮创建一个集合。假设要创建一个设备查询类微信小程序,集合名称填写为device。创建成功后,可以看到device集合管理界面,在界面中可以添加记录、查找记录、管理索引和管理权限,如图31所示。 2. 创建第一条记录 控制台提供了可视化添加数据的交互界面,选中集合device,单击“添加记录”按钮添加第一条设备数据,如图32所示。 图31创建集合 图32手动添加数据库记录 添加完成后可在控制台中查看到刚添加的数据,如图33所示。 图33数据库添加记录结果 3. 数据库高级操作 目前云开发控制台新增了数据库高级操作,如图34所示。 图34数据库高级操作 3.2数据迁移 云开发支持从文件导入已有的数据。目前仅支持导入CSV、JSON格式的文件数据。有云开发控制台和HTTP API两种导入方式。 使用云开发控制台导入数据: 要把文件导入云数据库,需打开云开发控制台,切换到“数据库”标签页,并选择要导入数据的集合,单击“导入”按钮,在“导入数据库”对话框中就可以将数据导入云数据库了,如图35所示。 图35将数据导入云数据库 选择要导入的CSV或者JSON文件,以及冲突处理模式,单击“确定”按钮即可开始导入。目前云数据库仅支持导入CSV、JSON格式的文件数据,有时数据通常以Excel的形式出现,这里简单介绍Excel文件如何转换为CSV和JSON文件。 Excel文件转换为CSV文件: 打开Excel文件,然后单击“另存为”,保存类型选择“CSV(逗号分隔)(*.csv)”,因为数据库只支持UTF8格式(默认为ANSI编码,导入后中文会显示成乱码),然后用记事本再次打开CSV文件,另存为时选择编码为UTF8,如图36所示,替换原来的CSV文件即可。 图36用CSV格式保存为UTF8编码 Excel文件转换为JSON文件: 开发者可以专门下载Excel转换为JSON工具,目前网站也提供了很多在线转换工具,如http://www.bejson.com/json/col2json/,在线Excel文件转换为JSON文件的过程如图37所示。 图37在线Excel文件转换为JSON文件的过程 然后把在线转换的结果复制到新建的JSON文件中,需要注意导入模板的JSON格式每条数据记录最后都没有逗号分隔,需要开发人员自行用文本替换把逗号去掉。数据导入结果如图38所示。 图38数据导入结果 需要注意以下几点: (1) JSON数据不是数组,而是类似JSON Lines,即各个记录对象之间使用\n分隔,而非逗号; (2) JSON数据每个键值对的键名首尾不能是“.”,例如“.a”“abc.”,且不能包含多个连续的“.”,例如“a..b”; (3) 键名不能重复,且不能有歧义,例如{"a":1, "a":2}或{"a": {"b":1},"a.b":2}; (4) 时间格式须为ISODate格式,例如"date":{"$date": "20180831T17:30:00. 882Z"}; (5) 当使用Insert冲突处理模式时,同一文件不能存在重复的_id字段,或与数据库已有记录相同的_id字段; (6) CSV格式的数据默认以第一行作为导入后的所有键名,余下的每一行则是与首行键名一一对应的键值记录。 目前提供了Insert、Upsert两种冲突处理模式。Insert模式会在导入时总是插入新记录,Upsert则会判断有无该条记录,如果有则更新记录,否则就插入一条新记录。 导入完成后,可以在提示信息中看到本次导入记录的情况。 如果开发人员不想通过在线转换工具(如http://www.bejson.com/json/col2json/)进行转换,那么如何通过代码将Excel文件转换为JSON文件?本节通过引入依赖模块xlstojson来实现将Excel文件转换为JSON文件,具体代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var node_xj = require("xls-to-json") var fs = require('fs') node_xj({ input: "设备.xlsx", // input xls output: "output.json", // output json //sheet: "sheet1", // specific sheetname }, function (err, result) { if (err) { console.error(err) } else { console.log(result) console.log("JSON文件已经被保存为output.json.") fs.readFile('output.json', 'utf8', function (err, data) { if (err) { return console.log(err) } var result = data.replace(/},/g, '}\n').replace(/\[{/, '{').replace(/\}]/, '}') fs.writeFile('data.json', result, 'utf8', function (err) { if (err) return console.log(err) }) }) } }); 这里使用VS Code将本地的设备Excel文件转换成JSON文件,这里用到xlstojson依赖库,读者使用之前需要在终端使用npm install xlstojson安装此依赖库,代码第3~7行将Excel文件转换为JSON文件,输出文件名为output.json,但是输出的JSON文件数据格式和图37一致,因此需要进一步修改格式。代码第13~16行读取output.json文件。代码第17行把最前面和最后面的[]去掉,并去掉每条记录后的逗号,最终生成云数据库可直接导入的JSON文件为data.json; 读者可以直接把data.json文件导入云数据库。 有时记录可能比较复杂,如图39中的Excel数据,其中的选项字段中需要用一个数组来存储,这些数据总不能让用户一条一条地输入数据库,那么这时就需要写代码将Excel数据记录转换为JSON文件。 针对图39中的Excel数据,Node.js的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 var xlsx = require('node-xlsx') var fs = require('fs') var sheets = xlsx.parse('charpter1.xlsx') var sheet = sheets[0].data var fs = require('fs') var json_data = [] for (var rowId = 1; rowId< sheet.length; rowId++) { let data = {} var row = sheet[rowId] data.type = row[0] data.title = row[1] data.option = row[2].split("\r\n"); 图39Excel数据 13 14 15 16 17 18 19 20 21 22 23 data.answer = row[3] data.explain = row[4] data.level = row[5] json_data.push(data) } var json_str = JSON.stringify(json_data) var result = json_str.replace(/},/g, '}\n').replace(/\[{/, '{').replace(/\}]/, '}') console.log(result) fs.writeFile('result.json', result, 'utf8', function (err) { if (err) return console.log(err); }); 其中,代码第3行采用nodexlsx解析charpter1.xlsx文件,读者需要安装nodexlsx依赖库,在终端中输入npm install nodexlsx; 代码第7~17行读取每条记录,将每条记录转行成JSON对象,因为第一行对应表头,因此rowId从1开始,代码第12行通过split("\r\n")方式,把字符串转换为数组; 代码第18行将JSON对象转换为字符串; 代码第19行把最前面和最后面的[]去掉,并去掉每条记录后的逗号; 代码第21~23行把JSON字符串写入result.json文件。然后通过云开发控制台将result.json文件导入数据库,如图310所示。 图310JSON文件导入数据库 3.3基础概念 1. 数据类型 云开发数据库提供以下几种数据类型:  string: 字符串。  number: 数字。  object: 对象。  array: 数组。  bool: 布尔值。  date: 时间。  geo: 多种地理位置类型。  null。 date类型用于表示时间,精确到毫秒,在微信小程序端可用JavaScript内置Date对象创建。需要特别注意的是,在微信小程序端创建的时间是客户端时间,不是服务端时间,这意味着在微信小程序端的时间与服务端时间不一定吻合,如果需要使用服务端时间,应该用API中提供的serverDate对象来创建一个服务端当前时间的标记,当使用了serverDate对象的请求抵达服务端处理时,该字段会被转换为服务端当前的时间,更棒的是,在构造serverDate对象时还可通过传入一个有offset字段的对象来标记一个与当前服务端时间偏移offset毫秒的时间,这样可以达到类似如下效果: 指定一个字段为服务端时间往后一个小时。 如果需要使用客户端时间,存放Date对象和存放毫秒数效果是否一样呢?不一样,因为数据库有针对日期类型的优化,建议使用时都用Date或serverDate构造时间对象。 null相当于一个占位符,表示一个字段存在但值为空。 2. 索引管理 建立索引是保证数据库性能、保证微信小程序体验的重要手段。开发人员应为所有需要成为查询条件的字段都建立索引。建立索引的入口在控制台中,可分别对各个集合的字段添加索引。创建索引时可以指定增加唯一性限制,具有唯一性限制的索引会要求被索引集合不能存在被索引字段值都相同的两个记录。需特别注意的是,假如记录中不存在某个字段,则对索引字段来说其值默认为null,如果索引有唯一性限制,则不允许存在两个或以上的该字段为空/不存在该字段的记录。在创建索引的时候索引属性选择“唯一”即可添加唯一性限制。 3. 权限控制 数据库的权限分为管理端和微信小程序端。管理端包括云函数端和云控制台。微信小程序端运行在微信小程序中,读写数据库受权限控制限制; 管理端运行在云函数上,拥有所有读写数据库的权限。云控制台的权限同管理端,拥有所有权限。微信小程序端操作数据库应有严格的安全规则限制。 初期对操作数据库开放以下4种权限配置: 仅创建者可写,所有人可读; 仅创建者可读写; 仅管理者可写,所有人可读; 仅管理者可读写。每个集合可以拥有一种权限配置,权限配置的规则是作用在集合的每个记录上的。出于易用性和安全性的考虑,云开发为云数据库做了微信小程序深度整合,在微信小程序中创建的每个数据库记录都会带有该记录创建者(即微信小程序用户)的信息,以_openid字段保存用户的openid在每个相应用户创建的记录中。因此,权限控制也相应围绕一个用户是否应该拥有权限操作其他用户创建的数据展开。 权限级别按照从宽到紧排列如下:  仅创建者可写,所有人可读: 数据只有创建者可写、所有人可读,如文章。  仅创建者可读写: 数据只有创建者可读写,其他用户不可读写,如用私密相册。  仅管理端可写,所有人可读: 该数据只有管理端可写,所有人可读,如商品信息。  仅管理端可读写: 该数据只有管理端可读写,如后台用的不暴露的数据。 简而言之,管理端始终拥有读写所有数据的权限,微信小程序端始终不能写他人创建的数据。微信小程序端的记录的读写权限其实分为: (1) 所有人可读,只有创建者可写; (2) 仅创建者可读写; (3) 所有人可读,仅管理端可写; (4) 所有人不可读,仅管理端可读写。 云数据库权限设置如表31所示。 表31云数据库权限设置 模式 微信小程序端 读自己创建的 数据 微信小程序端 写自己创建的 数据 微信小程序端 读他人创建的 数据 微信小程序端 写他人创建的 数据 管理端读写 任意数据 仅创建者可写,所有人可读√√√×√ 仅创建者可读写√√××√ 仅管理端可写,所有人可读√×√×√ 仅管理端可读写××××√ 集合权限设置如图311所示。在设置集合权限时应谨慎设置,防止出现越权操作。 图311集合权限设置 值得注意的是,新建集合的默认权限是: 仅创建者可读写,如果是非创建者在微信小程序端是无法读取集合数据的。有些读者的数据是通过云开发控制台导入的,没有对集合的权限进行修改(默认权限是仅创建者可读写),就会出现在微信小程序读取不到集合数据的情况。 3.4云数据库API列表 微信小程序云开发提供了丰富的数据库操作API,数据库API都是“懒”执行的,这意味着只有真实需要网络请求的API调用才会发起网络请求,其余如获取数据库、集合、记录的引用、在集合上构造查询条件等都不会触发网络请求。 1. 请求 请求表示链式调用终结。触发网络请求的API有get、add、update、set、remove和count,如表32所示。 表32触发网络请求的API API说明 get获取集合/记录数据 add 在集合上新增记录 update 更新集合/记录数据 set 替换更新一个记录 remove 删除记录 count 统计查询语句对应的记录条数 2. 引用 获取引用的API有database、collection和doc,如表33所示。 表33 获取引用的API API说明 database获取数据库引用,返回 Database 对象 collection获取集合引用,返回Collection对象 doc获取对一个记录的引用,返回 Document 对象 3. 数据库 数据库对象的字段有command、serverDate和Geo,如表34所示。 表34数据库对象的字段 字段说明 command获取数据库查询及更新指令,返回 Command serverDate构造服务端时间 Geo获取地理位置操作对象,返回 Geo 对象 4. 集合 集合对象API有doc、add、where、orderBy、limit、skip和field,如表35所示。 表35集合对象API API说明 doc 获取对一个记录的引用,返回 Document 对象 add 在集合上新增记录 where 构建一个在当前集合上的查询条件,返回Query,查询条件中可使用查询指令 orderBy 指定查询数据的排序方式 limit 指定返回数据的数量上限 skip 指定查询时从选中的记录列表中的第几项之后开始返回 field 指定返回结果中每条记录应包含的字段 5. 记录/文档 记录/文档对象API有get、update、set、remove和field,如表36所示。 表36记录/文档对象API API说明 get获取记录数据 update局部更新数据 set替换更新记录 remove删除记录 field指定返回结果中记录应包含的字段 6. 查询指令 Command对象查询指令如表37所示。 表37Command对象查询指令 类别指令说明 比较eq字段是否等于指定值 neq字段是否不等于指定值 lt字段是否小于指定值 lte字段是否小于或等于指定值 gt字段是否大于指定值 gte字段是否大于或等于指定值 in字段值是否在指定数组中 nin字段值是否不在指定数组中 逻辑and条件与,表示需同时满足多个查询筛选条件 or条件或,表示只需满足其中一个条件即可 nor表示需所有条件都不满足 not条件非,表示对给定条件取反 字段exists字段存在 mod字段值是否符合给定取模运算 数组all数组所有元素是否满足给定条件 elemMatch数组是否有一个元素满足所有给定条件 size数组长度是否等于给定值 地理位置geoNear找出字段值在给定点的附近的记录 geoWithin找出字段值在指定区域内的记录 geoIntersects找出与给定的地理位置图形相交的记录 7. 更新指令 Command对象更新指令如表38所示。 表38Command对象更新指令 类别指令说明 字段set设置字段为指定值 remove删除字段 inc原子操作,自增字段值 mul原子操作,自乘字段值 续表 类别指令说明 min如果字段值小于给定值,则设为给定值 max如果字段值大于给定值,则设为给定值 rename字段重命名 数组push往数组尾部增加指定值 pop从数组尾部删除一个元素 shift从数组头部删除一个元素 unshift往数组头部增加指定值 addToSet原子操作,如果不存在给定元素则添加元素 pull剔除数组中所有满足给定条件的元素 pullAll剔除数组中所有等于给定值的元素 3.5云数据库操作 从开发者工具1.02.1906202开始,在云控制台数据库管理页中可以编写和执行数据库脚本,脚本可对数据库进行CRUD操作,语法同SDK数据库语法,如图312所示。 图312控制台编写和执行数据库脚本 3.5.1增加记录 可以通过在集合对象上调用add()方法往集合中插入一条记录,目前add()方法一次只能插入一条记录。添加一条记录的执行语句为: 1 2 3 4 5 6 7 8 9 10 11 db.collection('device').add({ data:{ "deviceid":"20130410", "devicename":"红外狭缝扫描光束分析仪", "price":42042.29, "deviceuser":"王五", "place":"1-A101", "manufacturer":"THORLABS", "purchasedate":"2013/3/26", "supplier":"浙江赛因科学仪器有限公司"} }) 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行数据库插入操作,结果如图313所示。 图313数据库插入操作 3.5.2查询记录 在记录和集合上都提供了get()方法用于获取单个记录或集合中多个记录的数据。 1. 获取一个记录的数据 获取一个记录的数据执行语句为: 1 2 3 db.collection('device') .doc('muRMVtf5R8RSu74VNS3juEdD7w3IRr2LJWXv6tQU4rZAXYrM') .get() 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行获取一个记录的数据,结果如图314所示。 图314获取一个记录的数据 2. 获取多个记录的数据 当然也可以一次性获取多条记录。通过调用集合上的where()方法可以指定查询条件,再调用get()方法即可只返回满足指定查询条件的记录,比如获取用户张三名下所有的设备信息,获取多个记录的数据执行语句为: 1 2 3 4 db.collection('device').where({ deviceuser:"张三" }) .get() where()方法接收一个对象参数,该对象中每个字段和它的值构成一个需满足的匹配条件,各个字段间的关系是“与”的关系,即需同时满足这些匹配条件。在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行获取多个记录的数据,结果如图315所示。 图315获取多个记录的数据 3. 数据库查询指令 使用数据库API提供的where()方法可以构造复杂的查询条件完成复杂的查询任务。 API提供的查询指令,如表39所示。 表39API提供的查询指令 指令说明 eq等于 neq不等于 lt小于 lte小于或等于 gt大于 gte大于或等于 in字段值在给定数组中 nin字段值不在给定数组中 除了指定一个字段满足一个条件之外,还可以通过指定一个字段需同时满足多个条件,比如用and逻辑指令查询价格为3500~10000元的设备,执行代码为: 1 2 3 db.collection('device').where({ price: _.gt(3500).and(_.lt(10000)) }).get() 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行满足多个条件的查询,结果如图316所示。 图316满足多个条件的查询 既然有and,当然也有or了,Command.or用于表示逻辑“或”的关系,表示任意满足一个查询筛选条件。“或”指令有两种用法: 一是可以进行字段值的“或”操作; 二是可以进行跨字段的“或”操作。比如进行字段值的“或”操作,以查询设备为例,查询设备名称为“万用表”或“直流电源”的设备信息,执行代码为: 1 2 3 db.collection('device').where({ devicename: _.eq('万用表').or(_.eq('直流电源')) }).get() 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行字段值“或”操作,结果如图317所示。 图317执行字段值“或”操作 还可以进行跨字段的“或”操作,以查询设备为例,查询设备名称为“万用表”或价格为69300元的设备信息,执行代码为: 1 2 3 4 5 6 7 8 9 10 db.collection('device').where( _.or([{ devicename: _.eq('万用表') }, { price: 69300 } ]) ) .get() 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行跨字段“或”操作,结果如图318所示。 图318执行跨字段“或”操作 在一个实际的项目中,通常对数据库的操作需要用到集合上多个API,接下来演示集合上多个API如何同时使用。首先用户在云数据库集合device中导入124条数据记录,用户在云开发控制台高级操作中执行命令: 1 2 3 4 5 6 7 8 9 10 11 12 db.collection('device') .where({ price: _.gt(1000) }) .field({ deviceid: true, price: true, }) .orderBy('price', 'desc') .skip(1) .limit(120) .get() 代码第2~4行中加入了查询的限制条件; 代码第5~8行指定返回结果中每条记录应包含的字段; 代码第9行指定查询数据的排序方式,其中desc表示降序,asc表示升序; 代码第10行指定查询时从选中的记录列表中的第几项之后开始返回; 代码第11行指定返回数据的数量上限。在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行集合对象多个API查询,结果如图319所示。 图319集合对象多个API查询 这里有两点需要说明: (1) API(add、where、orderBy、limit、skip和field等)在调用时没有先后顺序; (2) 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行查询的返回记录没有数量限制,而在实际开发中微信小程序端在获取集合数据时服务器一次默认并且最多返回20条记录,云函数端这个数字则是100。 4. 获取一个集合的数据 如果要获取一个集合的数据,比如获取device集合上的所有记录,可以在集合上调用get()方法获取,但通常不建议这么使用,在微信小程序中需要尽量避免一次性获取过量的数据,只应获取必要的数据。开发者可以通过limit()方法指定需要获取的记录数量,但微信小程序端不能超过20条,云函数端不能超过100条。如果要获取集合中所有记录的数据,很可能一个请求无法取出所有数据,需要分批次取。获取一个集合数据的执行代码为: 1 2 3 4 5 6 7 8 9 10 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const MAX_LIMIT = 100 exports.main = async (event, context) =>{ let databasename = event.databasename //先取出集合记录总数 const countResult = await db.collection(databasename).count() const total = countResult.total //计算需分几次取 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const batchTimes = Math.ceil(total / 100) //承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i< batchTimes; i++) { const promise = db.collection(databasename).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } //等待所有 return (await Promise.all(tasks)).reduce((acc, cur) =>{ return { data: acc.data.concat(cur.data), errMsg: acc.errMsg, } }) } async顾名思义是“异步”的意思,async用于声明一个函数是异步的。而await从字面意思上是“等待”的意思,就是用于等待异步完成。也就是平常所说的异步等待。不过需注意await只能在async函数中使用。因此在云函数中使用asyncawait()方法,可以把异步请求变为同步请求。代码第6行读取的从微信小程序调用云函数传递的数据库集合名称; 第8、9行读取集合中记录总数; 因为云函数读取数据库集合每次最多只能读取100条记录,因此代码第11行计算需要分几次读取集合中的数据; 代码第13~17行分批读取集合中的数据。代码第19~24行把所有分批读取的数据进行汇总。 云函数的具体使用见第5章云函数的介绍,在微信小程序中调用云函数database代码如下: 1 2 3 4 5 6 7 8 wx.cloud.callFunction({ name: 'database', data: { databasename: "device" } }).then(res =>{ console.log(res.result) }) 获取集合device的所有数据,结果如图320所示,通过云函数database把集合device中所有的124条数据都取出来了。 3.5.3更新数据 更新数据主要有两个方法,如表310所示。 图320获取集合device的所有数据 表310更新数据的方法 API说明 update局部更新一个或多个记录 set替换更新一个记录 1. 局部更新 使用update()方法可以局部更新一个记录或一个集合中的记录,局部更新意味着只有指定的字段会得到更新,其他字段不受影响。 局部更新执行代码为: 1 2 3 4 5 6 db.collection('device').doc('muRMVtf5R8RSu74VNS3juEdD7w3IRr2LJWXv6tQU4rZAXYrM') .update({ data: { price: _.inc(100) } }) 上面的示例代码演示了对设备某一条数据执行价格增加100的原子操作,用inc指令而不是取出值、加100再写进去的,其好处在于这个写操作是个原子操作,不会受到并发写的影响,比如同时有两名用户A和B取了同一个字段值,然后分别加上100和200再写进数据库,那么这个字段最终结果会是加了200而不是300。如果使用inc指令则不会有这个问题。在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行局部更新数据,结果如图321所示。 2. 替换更新 如果需要替换更新一条记录,可以在记录上使用set()方法。替换更新意味着用传入的对象替换指定的记录,执行代码为: 图321局部更新数据 1 2 3 4 5 db.collection('device').doc('muRMVtf5R8RSu74VNS3juEdD7w3IRr2LJWXv6tQU4rZAXYrM') .set({ data: { "deviceid":"20130613", "devicename":"直流电源供应器", 6 7 8 9 10 11 12 13 "price":2800.00, "deviceuser":"李四", "place":"1-B105", "manufacturer":"固纬电子有限公司", "purchasedate":"2013/5/25", "supplier":"杭州炜煌电子有限公司" } }) 使用上面的代码就会把指定的设备信息进行整个替换。 3.5.4删除数据 对记录使用remove()方法可以删除该条记录,比如: 1 2 db.collection('device').doc('83de3a0e-bcd0-483d-a197-402a5e1e80b5') .remove() 在云开发控制台中选择“数据库”标签页,在左侧树形目录中选择“高级操作”,执行完上面的数据库删除操作后,会把_id为'83de3a0ebcd0483da197402a5e1e80b5'这条记录进行删除操作。如果需要删除多条记录,则可在Server端进行操作(云函数)。 前面介绍了集合中数据的增、删、改、查操作,这些操作是通过在云开发控制台中进行演示,而微信小程序端,云函数端和控制台对数据库操作的权限是不同的,而且能够调用的API也是不同的,这里列举微信小程序端和云函数端对数据库调用API接口的不同之处,如表311所示。 表311微信小程序端和云函数端对数据库调用API接口的不同 类别CollectionDocument getaddupdateremovegetaddupdateremove 微信小程序端√√××√-- √√ 云函数√√√√ √--√ √ 从表中可以看出,针对Document(单条记录操作) ,微信小程序端和云函数都支持查询、更新和删除操作,针对Collection(批量记录操作),云函数支持批量查询、单条记录增加(目前一次只能添加一条记录)、批量更新和批量删除操作,但是微信小程序端只支持批量查询和单条记录增加操作,并不支持批量更新和批量删除操作。 3.5.5正则表达式查询 数据库支持正则表达式查询,开发者可以在查询语句中使用JavaScript原生正则对象或使用db.RegExp()方法来构造正则对象然后进行字符串匹配。在查询条件中对一个字段进行正则匹配即要求该字段的值可以被给定的正则表达式匹配。注意,正则表达式不可用于db.command内(如db.command.in)。 db.RegExp()方法定义如下: 1 2 3 4 5 function RegExp(initOptions: IInitOptions): DBRegExp interface IInitOptions { regexp: string//正则表达式,字符串形式 options: string//flags,包括 i, m, s,但前端不做强限制 } Options支持i、m和s这3个flag,注意JavaScript原生正则对象构造时仅支持其中的i和m两个flag,因此需要使用s这个flag时必须使用db.RegExp()构造正则对象。flag的含义如表312所示。 表312flag的含义 flag说明 i大小写不敏感 m跨行匹配; 让开始匹配符^ 或结束匹配符$ 除了匹配字符串的开头和结尾外,还匹配行的开头和结尾 s让.可以匹配包括换行符在内的所有字符 首先用户在云数据库集合device中导入124条数据记录,用户可以在regexp参数中输入正则表达式(见图322),查询contact集合中mobile字段以138开头、后面8个数字结尾的手机号记录,其中^ 表示匹配输入字行首,\d表示匹配数字,$表示匹配输入行尾。 图322db.RegExp正则匹配案例1 图323db.RegExp正则匹配案例2 图323演示了查询contact集合中name字段是中文4个字的记录,其中^ 表示匹配输入字行首,$表示匹配输入行尾,“\u4e00”和“\u9fa5”是Unicode编码,并且正好是中文编码的开始和结束的两个值,所以这个正则表达式可以用来判断name包含4个中文的记录。 图324演示了查询contact集合中name字段以白字为结尾的记录,其中$表示匹配输入行尾。 图324db.RegExp正则匹配案例3 上面都是在云开发控制台高级操作中进行演示,接下来以一个实际的通讯录查询案例演示使用正则表达式实现模糊查询。 ColorUI给出了和手机通讯录类似的页面样式,用微信开发者工具打开ColorUI中的demo文件,在tabBar中选择“扩展”选项,然后单击“索引列表”图标,进入“索引”页面(代码见pages/plugin/indexes),如图325所示。 为了便于看到通讯录的效果,本项目提前准备了JSON格式的数据(如何从Excel文件转换为JSON文件,见3.2节),导入数据库contact集合,导入后的结果如图326所示,其中searchfield字段是手工加进去的,是为了便于对人员信息进行模糊搜索,实现用户在输入框中输入姓名的中文、拼音或者拼音首字母便可以搜索人员。 图325ColorUI通讯录样式 图326通讯录数据集合 本案例的页面样式home.wxml代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 返回 通讯录 {{item.sex}} {{item.name}} {{item.mobile}} 代码第6~15行对应搜索输入框; 代码第16~29行对应搜索结果显示的样式,样式设计和ColorUI基本一致,包括用户性别、用户姓名、用户手机号。 为了显示样式,需要在home.wxss中加入如下代码: 1 2 3 page { padding-top: 100rpx; } 相应的home.js代码实现如下: 1 2 3 4 5 6 7 const app = getApp() const db = wx.cloud.database() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, contactlist:[] - 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 }, bindKeyInput: function (event) { const name = event.detail.value db.collection('contact').where({ searchfield: db.RegExp({ regexp: name, options: 'i', }) }).get().then(res =>{ this.setData({ contactCur: res.data }) }) } }) 其中,bindKeyInput对应输入框输入事件,代码第11~16行对应对searchfield字段进行模糊查询。对字段进行模糊查询的结果如图327所示,可以通过姓名的中文、拼音或者拼音首字母进行查询。 图327进行模糊查询的结果 3.5.6查询和更新数组元素和嵌套对象 云数据库允许对对象、对象中的元素、数组、数组中的元素进行匹配查询,甚至还可以对数组和对象相互嵌套的字段进行匹配查询/更新,下面从普通匹配开始讲述如何进行匹配查询/更新。 1. 普通匹配 传入的对象的每个构成一个筛选条件,有多个则表示需同时满足这些条件,是“与”的关系,如果需要“或”关系,可使用[command.or]。 比如找出未完成的进度50的待办事项: 1 2 3 4 db.collection('todos').where({ done: false, progress: 50 }).get() 2. 匹配记录中的嵌套字段 假设在集合中有如下记录: 1 2 3 4 5 { "style": { "color": "red" } } 如果想要找出集合中style.color为red的记录,那么可以传入相同结构的对象做查询条件或使用“点表示法”查询: 1 2 3 4 5 6 7 8 9 10 //方式一 db.collection('todos').where({ style: { color: 'red' } }).get() //方式二 db.collection('todos').where({ 'style.color': 'red' }).get() 3. 匹配数组 假设在集合中有如下记录: 1 2 3 { "numbers": [10, 20, 30] } 可以传入一个完全相同的数组来筛选出这条记录: 1 2 3 db.collection('todos').where({ numbers: [10, 20, 30] }).get() 4. 匹配数组中的元素 如果想找出数组字段中数组值包含某个值的记录,可以在匹配数组字段时传入想要匹配的值。如对上面的例子,可传入一个数组中存在的元素来筛选出所有numbers字段的值包含20的记录: 1 2 3 db.collection('todos').where({ numbers: 20 }).get() 5. 匹配数组第n项元素 如果想找出数组字段中数组的第n个元素等于某个值的记录,那么在匹配中可以以“字段.下标”为key,目标值为value做匹配。如对上面的例子,如果想找出number字段第二项的值为20的记录,查询如下(注意,数组下标从0开始): 1 2 3 db.collection('todos').where({ 'numbers.1': 20 }).get() 更新也是类似,比如要更新_id为test的记录的numbers字段的第二项元素至30: 1 2 3 4 5 db.collection('todos').doc('test').update({ data: { 'numbers.1': 30 }, }) 6. 结合查询指令进行匹配 在对数组字段进行匹配时,也可以使用如lt、gt等指令来筛选出字段数组中存在满足给定比较条件的记录。如对上面的例子,可查找出所有numbers字段的数组值中存在包含大于25的值的记录: 1 2 3 4 const _ = db.command db.collection('todos').where({ numbers: _.gt(25) }).get() 查询指令也可以通过逻辑指令组合条件,比如找出所有numbers数组中存在包含大于25的值同时也存在小于15的值的记录: 1 2 3 4 const _ = db.command db.collection('todos').where({ numbers: _.gt(25).and(_.lt(15)) }).get() 7. 匹配并更新数组中的元素 如果想要匹配并更新数组中的元素,而不是替换整个数组,除了指定数组下标外,还可以: (1) 更新数组中第一个匹配到的元素。 更新数组字段时可以用字段路径.$的表示法来更新数组字段的第一个满足查询匹配条件的元素。注意,使用这种更新时,查询条件必须包含该数组字段。 假如有如下记录: 1 2 3 4 5 6 7 8 { "_id": "doc1", "scores": [10, 20, 30] } { "_id": "doc2", "scores": [20, 20, 40] } 让所有scores中的第一个20的元素更新为25: 1 2 3 4 5 6 7 8 9 //注意:批量更新需在云函数中进行 const _ = db.command db.collection('todos').where({ scores: 20 }).update({ data: { 'scores.$': 25 } }) 如果记录是对象数组也可以做到,路径如字段路径.$.字段路径。 注意事项:  不支持用在数组嵌套数组;  如果用unset更新操作符,不会从数组中去除该元素,而是置为null;  如果数组元素不是对象,且查询条件用了neq、not或nin,则不能使用.$。 (2) 更新数组中所有匹配的元素。 更新数组字段时可以用字段路径.$[]的表示法来更新数组字段的所有元素。 假如有如下记录: 1 2 3 4 5 6 { "_id": "doc1", "scores": { "math": [10, 20, 30] } } 比如让scores.math字段中所有数字加10: 1 2 3 4 5 6 const _ = db.command db.collection('todos').doc('doc1').update({ data: { 'scores.math.$[]': _.inc(10) } }) 更新后scores.math数组从[10, 20, 30]变为[20, 30, 40]。 如果数组是对象数组也是可以的。假如有如下记录: 1 2 3 4 5 6 7 8 9 10 { "_id": "doc1", "scores": { "math": [ { "examId": 1, "score": 10 }, { "examId": 2, "score": 20 }, { "examId": 3, "score": 30 } ] } } 可以更新scores.math下各个元素的score原子自增10: 1 2 3 4 5 6 const _ = db.command db.collection('todos').doc('doc1').update({ data: { 'scores.math.$[].score': _.inc(10) } }) 8. 匹配多重嵌套的数组和对象 上面所讲述的所有规则都是可以嵌套使用的。假设在集合中有如下记录: 1 2 3 4 5 6 7 8 9 10 11 12 { "root": { "objects": [ { "numbers": [10, 20, 30] }, { "numbers": [50, 60, 70] } ] } } 下面的查询语句找出集合中所有满足root.objects字段数组的第二项的numbers字段的第三项等于70的记录: 1 2 3 db.collection('todos').where({ 'root.objects.1.numbers.2': 70 }).get() 注意,指定下标不是必需的,例如可以找出集合中所有满足root.objects字段数组中任意一项的numbers字段包含30的记录: 1 2 3 db.collection('todos').where({ 'root.objects.numbers': 30 }).get() 更新操作也是类似,例如要更新_id为test的root.objects字段数组的第二项的numbers字段的第三项为80: 1 2 3 4 5 db.collection('todos').doc('test').update({ data: { 'root.objects.1.numbers.2': 80 }, }) 3.5.7数据库操作data赋值 数据库操作中通常需要设置data的值,data的值本质上是JSON格式的对象,JOSN格式的对象在2.1.3节中进行了介绍,JSON格式的对象必须遵守如下写法: 对象被花括号{}包围; 对象以键值对书写; 键必须是字符串,值必须是有效的JSON数据类型; 键和值由冒号分隔; 每个键值对由逗号分隔。 例如,常见的添加记录的方式为(需要在云数据库添加device集合): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 addData: function(e) { const db = wx.cloud.database() db.collection('device').add({ data: { "deviceid": "20130613", "devicename": "直流电源供应器", "price": 2800.00, "deviceuser": "张三", "place": "1-B115", "manufacturer": "固纬电子有限公司", "purchasedate": new Date('2013-4-25'), "supplier": "杭州炜煌电子有限公司", "submitdata":db.serverDate() } }) 16 17 18 19 .then(res =>{ console.log(res) }) } 代码第7行price字段的类型为数字,第11行和第13行字段类型为date类型,第13行获取服务器时间。调用addData事件,在数据库中device集合中插入一条设备记录,如图328所示。 图328微信小程序端插入数据库记录 有时在插入数据库之前已经有JSON对象了,那么是不是也一定需要用键值对的方式写data呢?既然data的值本质上是JSON格式的对象,就可以直接把JSON格式的对象赋值给data,例如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 addData: function(e) { var device1 = { "deviceid": "20130613", "devicename": "直流电源供应器", "price": 2800.00, "deviceuser": "张三", "place": "1-B115", "manufacturer": "固纬电子有限公司", "purchasedate": new Date('2013-4-25'), "supplier": "杭州炜煌电子有限公司" } const db = wx.cloud.database() device1.submitdata=db.serverDate() db.collection('device').add({ data: device1 }) 17 18 19 20 .then(res =>{ console.log(res) }) } 上面例子实现的结果和图328一致。 此外,data数据中的字段还支持对象和数组类型。接下来以数组为例演示如何在vote集合中插入一条投票记录(需要在云数据库添加vote集合): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 addData: function (e) { var options = [] options[0] = { option: 'A: 800元以下', num: 0 } options[1] = { option: 'B: 800元至1000元', num: 0 } options[2] = { option: 'C: 1000元至2000元', num: 0 } options[3] = { option: 'D: 2000元以上', num: 0 } const db = wx.cloud.database() db.collection('vote').add({ data: { "title": "投票标题: 你平均一月用去多少生活费", "type": "单选题", options } }) .then(res =>{ console.log(res) }) } 代码第21~25行的data数据中,插入了options数组,插入数据库后该字段的字段名为数组的变量名,插入云数据库后的结果如图329所示。 前面演示了设置对象字段的值,那么如果需要访问的字段名是一个变量,在data中如何进行访问呢?接下来以上面插入的投票记录为例,演示如何更新其中一个字段的值,例如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 addData: function (e) { var opt = '1' var options = 'options.' + opt + '.num' const db = wx.cloud.database() const _ = db.command console.log(options) db.collection('vote').doc('72527ac65dfd84f1056f295e5b6df49e').update({ data: { [options]: _.inc(1) } }) .then(console.log) .catch(console.error) } 图329data字段值为数组 为了演示,代码第7行记录的_id值来源于图329中的记录,这里假设用户在实际投票页面选择了B选项,相应地需要把投票记录中options[1].num的值自增1,在这里相应票数增加采用原子操作db.command.inc,难点在于票数都在options字段中,而options本身是个数组,对于更新数组元素的操作读者可以参考3.5.6节,这里使用“点表示法”,例如options.1.num表示options数组中第一个元素中的num的值。在这里读者需要注意,data中的字段名是变量,需要把变量放入[]中,调用addData事件以后vote集合中相应的选项num会增加1。操作后数据库中的结果如图330所示。 图330data字段名是变量 3.5.8增、删、改、查案例 接下来以设备数据为例,演示云数据库增、删、改、查的方法。 选择样式: 用微信开发者工具打开ColorUI中的demo文件,在tabBar中选择“扩展”选项,然后单击“垂直导航”图标,进入“Tab索引”页面,ColorUI中设备查询条目样式如图331所示。 图331ColorUI中设备查询条目样式 在ColorUI中打开 plugin/verticalnav/verticalnav.wxml页面找到对应代码,选取如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 凯尔 我以天理为凭,踏入这片荒芜,不再受凡人的枷锁遏制。我以天理为凭,踏入这片荒芜,不再受凡人的枷锁遏制。 22:20 5 开发者可以直接使用上面的样式,也可以对上面的样式进行适当的修改,修改后的样式如下(backgroundimage的图片从https://www.iconfont.cn中搜索设备图标下载后,上传至云存储): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 设备名称: {{item.devicename}} 设备编号: {{item.deviceid}} 领用人: {{item.deviceuser}} 在微信小程序页面加载时进行设备查询,进入微信小程序微信官方文档(https://developers.weixin.qq.com/miniprogram/dev/framework/),选择“云开发”子页面,在左侧树形目录中选择“SDK文档”→“数据库”→Collection→get选项,选择数据库集合查询语句,如图332所示。 图332数据库集合查询语句 复制图332中的数据库集合查询语句到home.js文件中的onLoad事件中,并在data中加入deviceslist对象用于在home.wxml中显示,具体如下: 1 2 3 4 5 6 7 8 9 10 data: { deviceslist: {}, }, onLoad: function(options) { const db = wx.cloud.database() db.collection('device').get().then(res =>{ //console.log(res.data) this.setData({ deviceslist: res.data }) }) } 采用Collection.get获取了多条数据库记录,如图333所示。 图333数据库多条数据集合查询结果 接下来演示如何根据条件进行数据查询。首先选择搜索框的样式: 用微信开发者工具打开ColorUI中的demo文件,在tabBar中选择“扩展”选项,然后单击“索引列表”图标,进入“索引”页面,选择的搜索框样式如图334所示。 图334选择的搜索框样式 相应的代码目录为plugin/indexes/indexes.wxml,选取代码加入home.wxml页面,代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 返回 设备清单 设备名称: {{item.devicename}} 设备编号: {{item.deviceid}} 领用人: {{item.deviceuser}} 36 37 38 相应地,本案例增加了输入框的输入事件,以及“搜索”按钮的搜索事件,在该事件中,根据用户在输入框输入的设备ID(deviceid),对云数据库进行数据查询,相应的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const app = getApp(); const db = wx.cloud.database() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, deviceslist: {}, deviceid: '', }, onLoad: function(options) { db.collection('device').get().then(res =>{ this.setData({ deviceslist: res.data }) }) }, bindKeyInput: function (event) { this.setData({ deviceid: event.detail.value }) }, deviceSearch:function(event){ var deviceid=this.data.deviceid if (deviceid != '') { db.collection('device').where({ deviceid: deviceid }).get().then(res =>{ this.setData({ deviceslist: res.data }) }) } else { db.collection('device').get().then(res =>{ this.setData({ deviceslist: res.data }) }) } }, recordEdit:function(event) { wx.navigateTo({ url: '../edit/edit?_id=' + event.currentTarget.dataset.id }) }, }) 根据条件进行数据查询,页面效果如图355所示。代码第20~35行实现了设备记录的查询功能,当输入框中有设备编号时(输入框不为空),对该设备编号进行查询; 如果输入框为空,则查找所有设备记录。在这里为了便于演示数据库的操作,查找所有设备记录时,实际上微信小程序只能查找20条数据,读者需要在onReachBottom事件(触底刷新事件)中,使用数据库Collection.skip加载后续的设备记录,该功能在后续章节案例中进行讲解。 接下来演示对单条数据进行更新和删除操作。添加页面pages/edit/edit,选择表单样式。用微信开发者工具打开ColorUI中的demo文件,在tabBar中选择“组件”选项,然后单击“表单”,进入“表单”页面,单条设备数据编辑样式选择如图336所示。 图335根据条件进行数据查询 图336单条设备数据编辑样式选择 相应的代码目录为componet/form/form.wxml,选取代码加入edit.wxml页面,代码如下: 1 2 3 4 5 6 7 8 9 10 返回 设备编辑
*设备编号: *设备名称: *设备领用人: *设备产商: *设备存放地: *设备价格: *购买日期: *供应商:
相应的edit.js的完整代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const db = wx.cloud.database() Page({ data: { deviceinfo: {} }, onLoad: function(options) { const _id = options._id db.collection('device').doc(_id).get().then(res =>{ this.setData({ deviceinfo: res.data }) }) }, updateValue: function(event) { let name = event.currentTarget.dataset.name; let deviceinfo = this.data.deviceinfo deviceinfo[name] = event.detail.value this.setData({ deviceinfo: deviceinfo }) }, deviceupdate: function(event) { let deviceinfo = this.data.deviceinfo db.collection('device').doc(deviceinfo._id).set({ data: { deviceid: deviceinfo.deviceid, devicename: deviceinfo.devicename, deviceuser: deviceinfo.deviceuser, manufacturer: deviceinfo.manufacturer, place: deviceinfo.place, price: parseInt(deviceinfo.price), purchasedate: deviceinfo.purchasedate, supplier: deviceinfo.supplier }, success: function(res) { if (res.stats.updated) { console.log("更新成功...") } } }) }, deviceDelete: function(event) { let deviceinfo = this.data.deviceinfo db.collection('device').doc(deviceinfo._id).remove() .then(res =>{ 49 50 51 52 53 54 55 console.log("删除成功...") wx.navigateTo({ url: '../home/home' }) }) }, }) 代码第6~13行实现页面加载(onLoad事件)时,根据上一个页面传过来的参数_id,查询数据库中的记录; 代码第15~22行实现页面中输入框输入事件的监听; 代码第24~43行实现数据库记录的更新; 代码第45~55行实现数据库记录的删除操作。 在home页面单击设备记录后面的“编辑”按钮,就会进入edit页面,edit页面样式如图337所示。 图337edit页面样式 需要说明的是,本案例在云开发控制台中设置集合device的权限设置为“所有用户可读,仅创建者可读写”。由于数据记录是通过导入的方式添加进集合,因此每条数据都没有记录创建者。如果是通过微信小程序添加的记录,就会在每条记录中添加一个_openid字段,而通过数据导入的方式不会自动添加_openid字段,因此在edit页面中直接单击“修改设备”按钮,会提示类似的错误: 1 2 3 Error: errCode: -502001 database request fail | errMsg: [FailedOperation.set] multiple write errors: [{write errors: [{E11000 duplicate key error collection: tnt-12p3936xo.x-j-l index: id dup key: { : "xjl" }}]}, {}] 造成上面这种错误的主要原因是这条数据不是用户自己创建的,因为集合device只支持创建者可读写。要解决这个问题,有两种方法: (1) 在每条记录中添加_openid字段,值为开发者自己的openid,但是该方法也仅支持创建者对该记录的写操作; (2) 通过云函数,所有用户都可对该记录进行读写。 在这里通过添加_openid字段的方式,如图338所示。 图338数据添加_openid字段 _openid的值可以通过云函数获得,也可以进入云开发控制台,选择“运营分析”→“用户访问”选项,直接复制openid。