## clientDB简介 > 自`HBuilderX 2.9.5`起支持在客户端直接使用`uniCloud.database()`方式获取数据库引用,即在前端直接操作数据库,这个功能被称为`clientDB` > `HBuilderX 2.9.5`以前的用户如使用过`clientDB`,在升级后请将`clientDB`的前端库和云函数删除,新版已经在前端和云端内置了`clientDB` 使用`clientDB`的好处:**不用写服务器代码了!** 1个应用开发的一半的工作量,就此直接省去。 当然使用`clientDB`需要扭转传统后台开发观念,不再编写云函数,直接在前端操作数据库。但是为了数据安全,需要在数据库上配置`DB Schema`。 在`DB Schema`中,配置数据操作的权限和字段值域校验规则,阻止前端不恰当的数据读写。详见:[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema) 如果想在数据库操作之前或之后需要在云端执行额外的动作(比如获取文章详情之后阅读量+1),`clientDB`提供了action云函数机制。在HBuilderX项目的`cloudfunctions/uni-clientDB-actions`目录编写上传js,参考:[action](uniCloud/database?id=action) **注意** - `clientDB`依赖uni-id(`1.1.10+版本`)提供用户身份和权限校验,如果你不了解uni-id,请参考[uni-id文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id) - `clientDB`依赖的uni-id需要在uni-id的config.json内添加uni-id相关配置,通过uni-id的init方法传递的参数不会对clientDB生效 - 通常在管理控制台使用`clientDB`,需要获取不同角色用户拥有的权限(在权限规则内使用auth.permission),请先查阅[uni-id 角色权限](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=rbac) ## clientDB图解 ![](https://static-eefb4127-9f58-4963-a29b-42856d4205ee.bspapp.com/clientdb.jpg) `clientDB`的前端部分包括js API和``组件两部分。 js API可以执行所有数据库操作。``组件适用于查询数据库,它是js API的再封装,进一步简化查询的代码量。 目前``组件没有内置,而是作为一个插件单独下载,它的文档另见:[https://uniapp.dcloud.net.cn/uniCloud/uni-clientdb-component](https://uniapp.dcloud.net.cn/uniCloud/uni-clientdb-component) 以下文章重点介绍`clientDB`的js API,组件的查询语法与js API是一致的。 ## clientDB前端API@jssdk `clientDB`的客户端部分主要负责提供API,允许前端编写数据库操作指令,以及处理一些客户端不太方便表示的字段,比如用户ID(详情看下面语法扩展部分) `clientDB`支持传统的nosql查询语法,并新增了`jql`查询语法。`jql`是一种更易用的查询语法。 **传统nosql查询语法示例** 这段示例代码,在一个前端页面,直接查询了云数据库的`list`表,并且指定了`name`字段值为`hello-uni-app`的查询条件,then里的res即为返回的查询结果。 ```js // 获取db引用 const db = uniCloud.database() // 使用uni-clientDB db.collection('list') .where({ name: "hello-uni-app" //传统MongoDB写法,不是jql写法。实际开发中推荐使用jql写法 }).get() .then((res)=>{ // res 为数据库查询结果 }).catch((err)=>{ }) ``` **使用说明** 前端操作数据库的语法与云函数一致,但有以下限制(使用jql语法时也一样): - 上传时会对query进行序列化,除Date类型、RegExp之外的所有不可序列化的参数类型均不支持(例如:undefined) - 为方便控制权限,禁止前端使用set方法,一般情况下也不需要前端使用set - 更新数据库时不可使用更新操作符`db.command.inc`等 - 更新数据时键值不可使用`{'a.b.c': 1}`的形式,需要写成`{a:{b:{c:1}}}`形式(后续会对此进行优化) ### 返回值说明@returnvalue `clientDB`云端默认返回值形式如下,开发者可以在[action](uniCloud/database?id=action)的`after`内用js修改返回结果,传入`after`内的result不带code和message。 ```js { code: "", // 错误码 message: "" // 错误信息 ... // 数据库指令执行结果 } ``` **错误码列表** |错误码 |描述 | |:-: |:-: | |TOKEN_INVALID_INVALID_CLIENTID |token校验未通过(设备特征校验未通过) | |TOKEN_INVALID |token校验未通过(云端已不包含此token) | |TOKEN_INVALID_TOKEN_EXPIRED |token校验未通过(token已过期) | |TOKEN_INVALID_WRONG_TOKEN |token校验未通过(token校验未通过) | |SYNTAX_ERROR |语法错误 | |PERMISSION_ERROR |权限校验未通过 | |VALIDATION_ERROR |数据格式未通过 | |SYSTEM_ERROR |系统错误 | ### 前端环境变量@variable `clientDB`目前内置了3个变量可以供客户端使用,客户端并非直接获得这三个变量的值,而是需要传递给云端,云数据库在数据入库时会把变量替换为实际值。 |参数名 |说明 | |:-: |:-: | |db.env.uid |用户uid,依赖uni-id| |db.env.now |服务器时间戳 | |db.env.clientIP|当前客户端IP | 使用这些变量,将可以避免过去在服务端代码中写代码获取用户uid、时间和客户端ip的麻烦。 ```js const db = uniCloud.database() let res = await db.collection('table').where({ user_id: db.env.uid // 查询当前用户的数据 }).get() ``` ### JQL查询语法@jsquery `jql`,全称javascript query language,是一种js方式操作数据库的语法规范。 `jql`大幅降低了js工程师操作数据库的难度、大幅缩短开发代码量。并利用json数据库的嵌套特点,极大的简化了联表查询的复杂度。 #### jql的诞生背景 传统的数据库查询,有sql和nosql两种查询语法。 - sql是一种字符串表达式,写法形如: ``` select * from table1 where field1="123" ``` - nosql是js方法+json方式的参数,写法形如: ```js const db = uniCloud.database() let res = await db.collection('table').where({ field1: '123' }).get() ``` sql写法,对js工程师而言有学习成本,而且无法处理非关系型的MongoDB数据库,以及sql的联表查询inner join、left join也并不易于学习。 而nosql的写法,实在过于复杂。 1. 运算符需要转码,`>`需要使用`gt`方法、`==`需要使用`eq`方法 比如一个简单的查询,取field1>0,则需要如下复杂写法 ```js const db = uniCloud.database() const dbCmd = db.command let res = await db.collection('table1').where({ field1: dbCmd.gt(0) }).get() ``` 如果要表达`或`关系,需要用`or`方法,写法更复杂 ```js field1:dbCmd.gt(4000).or(dbCmd.gt(6000).and(dbCmd.lt(8000))) ``` 2. nosql的联表查询写法,比sql还复杂 sql的inner join、left join已经够乱了,而nosql的代码无论写法还是可读性,都更“令人发指”。比如这个联表查询: ```js const db = uniCloud.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() ``` 3. 列表分页写法复杂 需要使用skip,处理offset 这些问题竖起一堵墙,让后端开发难度加大,成为一个“专业领域”。但其实这堵墙是完全可以推倒的。 `jql`将解决这些问题,让js工程师没有难操作的数据。 具体看以下示例 ```js const db = uniCloud.database() // 上面的示例中的where条件可以使用以下写法 db.collection('list') .where('name == "hello-uni-app"') .get() .then((res)=>{ // res 为数据库查询结果 }).catch((err)=>{ // err.message 错误信息 // err.code 错误码 }) ``` 除了js写法,uniCloud还提供了``组件,可以在前端页面中直接查询云端数据并绑定到界面上。[详情](https://ext.dcloud.net.cn/plugin?id=3256) 比如下面的代码,list表中查询到符合条件的记录可以直接绑定渲染到界面上 ```html {{error}} {{item.name}} 加载中... ``` **jql条件语句内变量** 以下变量同[前端环境变量](uniCloud/database.md?id=variable) |参数名 |说明 | |:-: |:-: | |$env.uid |用户uid,依赖uni-id| |$env.now |服务器时间戳 | |$env.clientIP|当前客户端IP | **jql条件语句的运算符** |运算符 |说明 |示例 |示例解释(集合查询) | |:-: |:-: |:-: |:-: | |== |等于 |name == 'abc' |查询name属性为abc的记录,左侧为数据库字段 | |!= |不等于 |name != 'abc' |查询name属性不为abc的记录,左侧为数据库字段 | |> |大于 |age>10 |查询条件的 age 属性大于 10,左侧为数据库字段 | |>= |大于等于 |age>=10 |查询条件的 age 属性大于等于 10,左侧为数据库字段 | |< |小于 |age<10 |查询条件的 age 属性小于 10,左侧为数据库字段 | |<= |小于等于 |age<=10 |查询条件的 age 属性小于等于 10,左侧为数据库字段 | |in |存在在数组中 |status in ['a','b'] |查询条件的 status 是['a','b']中的一个,左侧为数据库字段 | |! |非 |!(status in ['a','b']) |查询条件的 status 不是['a','b']中的任何一个 | |&& |与 |uid == auth.uid && age > 10 |查询记录uid属性 为 当前用户uid 并且查询条件的 age 属性大于 10 | ||| |或 |uid == auth.uid||age>10 |查询记录uid属性 为 当前用户uid 或者查询条件的 age 属性大于 10 | |test |正则校验 |/abc/.test(content) |查询 content字段内包含 abc 的记录。可用于替代sql中的like。还可以写更多正则实现更复杂的功能 | 这里的test方法比较强大,格式为:`正则规则.test(fieldname)`。 具体到这个正则 `/abc/.test(content)`,类似于sql中的`content like '%abc%'`,即查询所有字段content包含abc的数据记录。 **注意编写查询条件时,除test外,均为运算符左侧为数据库字段,右侧为常量** ### JQL联表查询@lookup `JQL`提供了更简单的联表查询方案。不需要学习join、lookup等复杂方法。 只需在db schema中,将两个表的关联字段建立映射关系,就可以把2个表当做一个虚拟表来直接查询。 比如有2个表,book表,存放书籍商品;order表存放书籍销售订单记录。 book表内有以下数据,title为书名、author为作者: ```js { "_id": "1", "title": "西游记", "author": "吴承恩" } { "_id": "2", "title": "水浒传", "author": "施耐庵" } { "_id": "3", "title": "三国演义", "author": "罗贯中" } { "_id": "4", "title": "红楼梦", "author": "曹雪芹" } ``` order表内有以下数据,book_id字段为book表的书籍_id,quantity为该订单销售了多少本书: ```js { "book_id": "1", "quantity": 111 } { "book_id": "2", "quantity": 222 } { "book_id": "3", "quantity": 333 } { "book_id": "4", "quantity": 444 } { "book_id": "3", "quantity": 555 } ``` 如果我们要对这2个表联表查询,在订单记录中同时显示书籍名称和作者,那么首先要建立两个表中关联字段`book`的映射关系。 即,在order表的db schema中,配置字段 book_id 的`foreignKey`,指向 book 表的 _id 字段,如下 ```json // order表schema { "bsonType": "object", "required": [], "permission": { "read": true }, "properties": { "book_id": { "bsonType": "string", "foreignKey": "book._id" // 使用foreignKey表示,此字段关联book表的_id。 }, "quantity": { "bsonType": "int" } } } ``` book表的db schema也要保持正确 ```json // book表schema { "bsonType": "object", "required": [], "permission": { "read": true }, "properties": { "title": { "bsonType": "string" }, "author": { "bsonType": "string" } } } ``` schema保存至云端后,即可在前端直接查询。查询表设为order和book这2个表名后,即可自动按照一个合并虚拟表来查询,filed、where等设置均按合并虚拟表来设置。 ```js // 客户端联表查询 const db = uniCloud.database() db.collection('order,book') // 注意collection方法内需要传入所有用到的表名,用逗号分隔,主表需要放在第一位 .where('book_id.title == "三国演义"') // 查询order表内书名为“三国演义”的订单 .field('book_id{title,author},quantity') // 这里联表查询book表返回book表内的title、book表内的author、order表内的quantity .get() .then(res => { console.log(res); }).catch(err => { console.error(err) }) // 上面的写法是clientDB的jql语法,如果不使用jql的话,写法会变得很长,大致如下 // 注意clientDB内联表查询需要用拼接子查询的方式(let+pipeline) const db = uniCloud.database() const dbCmd = db.command const $ = dbCmd.aggregate db.collection('order') .aggregate() .lookup({ from: 'book', let: { book_id: '$book_id' }, pipeline: $.pipeline() // 此match方法内的条件会和book表对应的权限规则进行校验,{status: 'OnSell'}会参与校验,整个expr方法转化成一个不与任何条件产生交集的特别表达式。这里如果将dbCmd.and换成dbCmd.or会校验不通过 .match(dbCmd.expr( $.eq(['$_id', '$$book_id']) )) .done() as: 'book' }) .match({ book: { title: '三国演义' } }) .end() ``` 上述查询会返回如下结果,可以看到书籍信息被嵌入到order表的book字段下,成为子节点。同时根据where条件设置,只返回书名为三国演义的订单记录。 ```js { "code": "", "message": "", "data": [{ "_id": "b8df3bd65f8f0d06018fdc250a5688bb", "book": [{ "author": "罗贯中", "title": "三国演义" }], "quantity": 555 }, { "_id": "b8df3bd65f8f0d06018fdc2315af05ec", "book": [{ "author": "罗贯中", "title": "三国演义" }], "quantity": 333 }] } ``` 关系型数据库做不到这种设计。`jql`充分利用了json文档型数据库的特点,实现了这个简化的联表查询方案。 不止是2个表,3个、4个表也可以通过这种方式查询。 不止js,``组件也支持所有`jql`功能,包括联表查询。 **注意** - field参数字符串内没有冒号,{}为联表查询标志 ### 查询列表分页 `jql`提供了更简单的分页方法,包括两种模式: 1. 滚动到底加载下一页 2. 点击页码按钮切换不同页 推荐通过``组件处理分页,详见:[https://uniapp.dcloud.net.cn/uniCloud/uni-clientdb-component?id=page](https://uniapp.dcloud.net.cn/uniCloud/uni-clientdb-component?id=page) ### 排序orderBy@orderby 传统的MongoDB的排序参数是json格式,jql支持类sql的字符串格式,书写更为简单。 sort方法和orderBy方法内可以传入一个字符串来指定排序规则。 orderBy允许进行多个字段排序,以逗号分隔。每个字段可以指定 asc(升序)、desc(降序)。 写在前面的排序字段优先级高于后面。 示例如下: ```js orderBy('quantity asc, create_date desc') //按照quantity字段升序排序,quantity相同时按照create_date降序排序 // asc可以省略,上述代码和以下写法效果一致 orderBy('quantity, create_date desc') // 注意不要写错成全角逗号 ``` 以上面的order表数据为例: ```js const db = uniCloud.database() db.collection('order') .orderBy('quantity asc, create_date desc') // 按照quantity字段升序排序,quantity相同时按照create_date降序排序 .get() .then(res => { console.log(res); }).catch(err => { console.error(err) }) // 上述写法等价于 const db = uniCloud.database() db.collection('order') .orderBy('quantity','asc') .orderBy('create_date','desc') .get() .then(res => { console.log(res); }).catch(err => { console.error(err) }) ``` ### 查询结果返回总数getcount@getcount 使用`clientDB`时可以在get方法内传入`getCount:true`来同时返回总数 ```js // 这以上面的order表数据为例 const db = uniCloud.database() db.collection('order') .get({ getCount:true }) .then(res => { console.log(res); }).catch(err => { console.error(err) }) // 如果不使用getCount,需要再调用一次count方法来返回总数 const db = uniCloud.database() db.collection('order') .get() .then(res => { console.log(res); }).catch(err => { console.error(err) }) db.collection('order') .count() .then(res => { console.log(res); }).catch(err => { console.error(err) }) ``` 返回结果为 ```js { "code": "", "message": "", "data": [{ "_id": "b8df3bd65f8f0d06018fdc250a5688bb", "book": "3", "quantity": 555 }], "count": 5 } ``` ### 查询结果时返回单条记录getone@getone 使用`clientDB`时可以在get方法内传入`getOne:true`来返回一条数据 ```js // 这以上面的book表数据为例 const db = uniCloud.database() db.collection('book') .where({ title: '西游记' }) .get({ getOne:true }) .then(res => { console.log(res); }).catch(err => { console.error(err) }) ``` 返回结果为 ```js { "code": "", "message": "", "data": { "_id": "1", "title": "西游记", "author": "吴承恩" } } ``` ### 新增数据记录add 获取到db的表对象后,通过`add`方法新增数据记录。 方法:collection.add(data) **参数说明** | 参数 | 类型 | 必填 | | ---- | ------ | ---- | | data | object | array | 是 | data支持一条记录,也支持多条记录一并新增到集合中。 data中不需要包括`_id`字段,数据库会自动维护该字段。 **返回值** 单条插入时 | 参数 | 类型 | 说明 | | ---- | ------| ---------------------------------------- | |id | String|插入记录的`_id` | 批量插入时 | 参数 | 类型 | 说明 | | ---- | ------| ---------------------------------------- | | inserted | Number| 插入成功条数 | |ids | Array |批量插入所有记录的`_id` | **示例:** 比如在user表里新增一个叫王五的记录: ```js const db = uniCloud.database(); db.collection('user').add({name:"王五"}) ``` 也可以批量插入数据并获取返回值 ```js const db = uniCloud.database(); const collection = db.collection('user'); let res = await collection.add([{ name: '张三' },{ name: '李四' },{ name: '王五' }]) ``` 如果上述代码执行成功,则res的值将包括inserted:3,代表插入3条数据,同时在ids里返回3条记录的`_id`。 如果新增记录失败,会抛出异常,以下代码示例为捕获异常: ```js // 插入1条数据,同时判断成功失败状态 const db = uniCloud.database(); db.collection("user") .add({name: '张三'}) .then((res) => { uni.showToast({ title: '新增成功' }) }) .catch((err) => { uni.showModal({ content: err.message || '新增失败', showCancel: false }) }) .finally(() => { }) ``` **Tips** - 如果是非admin账户新增数据,需要在数据库中待操作表的`db schema`中要配置permission权限,赋予create为true。 - 云服务商选择阿里云时,若集合表不存在,调用add方法会自动创建集合表,并且不会报错。 ### 删除数据记录remove 获取到db的表对象,然后指定要删除的记录,通过remove方法删除。 注意:如果是非admin账户删除数据,需要在数据库中待操作表的`db schema`中要配置permission权限,赋予delete为true。 指定要删除的记录有2种方式: #### 方式1 通过指定文档ID删除 collection.doc(_id).remove() 删除单条记录 ```js const db = uniCloud.database(); db.collection("table1").doc("5f79fdb337d16d0001899566").remove() ``` 删除该表所有数据 ```js const db = uniCloud.database(); let collection = db.collection("table1") let res = await collection.get() res.data.map(async(document) => { return await collection.doc(document.id).remove(); }); ``` #### 方式2 条件查找文档后删除 collection.where().remove() ```js // 删除字段a的值大于2的文档 try { await db.collection("table1").where("a>2").remove() } catch (e) { uni.showModal({ title: '提示', content: e.message }) } ``` #### 回调的res响应参数 | 字段 | 类型 | 必填 | 说明 | | --------- | ------- | ---- | ------------------------ | | deleted | Number | 否 | 删除的记录数量 | 示例:判断删除成功或失败,打印删除的记录数量 ```js const db = uniCloud.database(); db.collection("table1") .where({ _id: "5f79fdb337d16d0001899566" }) .remove() .then((res) => { uni.showToast({ title: '删除成功' }) console.log("删除条数: ",res.deleted); }).catch((err) => { uni.showModal({ content: err.message || '删除失败', showCancel: false }) }).finally(() => { }) ``` ### 更新数据记录update 获取到db的表对象,然后指定要删除的记录,通过remove方法删除。 注意:如果是非admin账户修改数据,需要在数据库中待操作表的`db schema`中要配置permission权限,赋予update为true。 collection.doc().update(Object data) **参数说明** | 参数 | 类型 | 必填 | 说明 | | ---- | ------ | ---- | ---------------------------------------- | | data | object | 是 | 更新字段的Object,{'name': 'Ben'} _id 非必填| **回调的res响应参数** | 参数 | 类型 | 说明 | | ---- | ------| ---------------------------------------- | |updated| Number| 更新成功条数,数据更新前后没变化时会返回0。用法与删除数据的响应参数示例相同 | ```js const db = uniCloud.database(); let collection = db.collection("table1") let res = await collection.where({_id:'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 const db = uniCloud.database(); let collection = db.collection("table1") let res = await collection.where({_id:'doc-id'}) .update({ arr: { 1: "uniCloud" } }) ``` ```json // 更新前 { "_id": "doc-id", "arr": ["hello", "world"] } // 更新后 { "_id": "doc-id", "arr": ["hello", "uniCloud"] } ``` #### 批量更新文档 ```js const db = uniCloud.database(); let collection = db.collection("table1") let res = await collection.where("name=='hey'").update({ age: 18, }) ``` #### 更新数组内指定下标的元素 ```js const db = uniCloud.database(); const res = await db.collection('table1').where({_id:'1'}) .update({ // 更新students[1] ['students.' + 1]: { name: 'wang' } }) ``` ```json // 更新前 { "_id": "1", "students": [ { "name": "zhang" }, { "name": "li" } ] } // 更新后 { "_id": "1", "students": [ { "name": "zhang" }, { "name": "wang" } ] } ``` #### 更新数组内匹配条件的元素 **注意:只可确定数组内只会被匹配到一个的时候使用** ```js const db = uniCloud.database(); const res = await db.collection('table1').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" } ] } ``` 注意: - 为方便控制权限,禁止前端使用set方法,一般情况下也不需要前端使用set - 更新数据库时不可使用更新操作符`db.command.inc`等 - 更新数据时键值不可使用`{'a.b.c': 1}`的形式,需要写成`{a:{b:{c:1}}}`形式(后续会对此进行优化) ### 刷新token@refreshtoken 透传uni-id自动刷新的token给客户端 **用法** ```js const db = uniCloud.database() function refreshToken({ token, tokenExpired }) { uni.setStorageSync('uni_id_token', token) uni.setStorageSync('uni_id_token_expired', tokenExpired) } // 绑定刷新token事件 db.auth.on('refreshToken', refreshToken) // 解绑刷新token事件 db.auth.off('refreshToken', refreshToken) ``` ## DBSchema@schema `DB Schema`是基于 JSON 格式定义的数据结构的规范。 它有很多重要的作用: - 描述现有的数据格式。可以一目了然的阅读每个表、每个字段的用途。 - 设定数据操作权限(permission)。什么样的角色可以读/写哪些数据,都在这里配置。 - 设定字段值域能接受的格式(validator),比如不能为空、需符合指定的正则格式。 - 设置数据的默认值(defaultValue/forceDefaultValue),比如服务器当前时间、当前用户id等。 - 设定多个表的字段间映射关系(foreignKey),将多个表按一个虚拟表直接查询,大幅简化联表查询。 - 根据schema自动生成表单维护界面,比如新建页面和编辑页面,自动处理校验规则。 这些工具大幅减少了开发者的开发工作量和重复劳动。 **`DB Schema`是`clientDB`紧密相关的配套,掌握clientDB离不开详读[DB Schema文档](uniCloud/schema)。** **下面示例中使用了注释,实际使用时schema是一个标准的json文件不可使用注释。**完整属性参考[schema字段](https://uniapp.dcloud.net.cn/uniCloud/schema?id=segment) ```js { "bsonType": "object", // 表级的类型,固定为object "required": ['book', 'quantity'], // 新增数据时必填字段 "permission": { // 表级权限 "read": true, // 读 "create": false, // 新增 "update": false, // 更新 "delete": false, // 删除 }, "properties": { // 字段列表,注意这里是对象 "book": { // 字段名book "bsonType": "string", // 字段类型 "permission": { // 字段权限 "read": true, // 字段读权限 "write": false, // 字段写权限 }, "foreignKey": "book._id" // 其他表的关联字段 }, "quantity": { "bsonType": "int" } } } ``` ### permission@permission `DB Schema`中的数据权限配置功能非常强大,请详读[DB Schema的数据权限控制](uniCloud/schema?id=permission) 在配置好`DB Schema`的权限后,clientDB的查询写法,尤其是非`JQL`的聚合查询写法有些限制,具体如下: - 不使用聚合时collection方法之后需紧跟一个where方法,这个where方法内传入的条件必须满足权限控制规则 - 使用聚合时aggregate方法之后需紧跟一个match方法,这个match方法内的条件需满足权限控制规则 - 使用lookup时只可以使用拼接子查询的写法(let+pipeline模式),做这个限制主要是因为需要确保访问需要lookup的表时也会传入查询条件,即pipeline参数里面`db.command.pipeline()`之后的match方法也需要像上一条里面的match一样限制 - 上面用于校验权限的match和where后的project和field是用来确定本次查询需要访问什么字段的(如果没有将会认为是在访问所有字段),访问的字段列表会用来确认使用那些字段权限校验。这个位置的project和field只能使用白名单模式 - 上面用于校验权限的match和where内如果有使用`db.command.expr`,那么在进行权限校验时expr方法内部的条件会被忽略,整个expr方法转化成一个不与任何条件产生交集的特别表达式,具体表现请看下面示例 **schema内permission配置示例** ```js // order表schema { "bsonType": "object", // 表级的类型,固定为object "required": ['book', 'quantity'], // 新增数据时必填字段 "permission": { // 表级权限 "read": "doc.uid == auth.uid", // 每个用户只能读取用户自己的数据。前提是要操作的数据doc,里面有一个字段存放了uid,即uni-id的用户id。(不配置时等同于false) "create": false, // 禁止新增数据记录(不配置时等同于false) "update": false, // 禁止更新数据(不配置时等同于false) "delete": false, // 禁止删除数据(不配置时等同于false) }, "properties": { // 字段列表,注意这里是对象 "secret_field": { // 字段名 "bsonType": "string", // 字段类型 "permission": { // 字段权限 "read": false, // 禁止读取secret_field字段的数据 "write": false // 禁止写入(包括更新和新增)secret_field字段的数据,父级节点存在false时这里可以不配 } }, "uid":{ "bsonType": "string", // 字段类型 "foreignKey": "uni-id-users._id" }, "book_id": { "bsonType": "string", // 字段类型 "foreignKey": "book._id" } } } ``` ```js // book表schema { "bsonType": "object", "required": ['book', 'quantity'], // 新增数据时必填字段 "permission": { // 表级权限 "read": "doc.status == 'OnSell'" // 允许所有人读取状态是OnSell的数据 }, "properties": { // 字段列表,注意这里是对象 "title": { "bsonType": "string" }, "author": { "bsonType": "string" }, "secret_field": { // 字段名 "bsonType": "string", // 字段类型 "permission": { // 字段权限 "read": false, // 禁止读取secret_field字段的数据 "write": false // 禁止写入(包括更新和新增)secret_field字段的数据 } } } } ``` **请求示例** ```js const db = uniCloud.database() const dbCmd = db.command const $ = dbCmd.aggregate db.collection('order') .where('uid == $env.uid && book_id.status == "OnSell"') .field('uid,book_id{title,author}') .get() ``` ## action@action action的作用是在执行前端发起的数据库操作时,额外触发一段云函数逻辑。它是一个可选模块。action是运行于云函数内的,可以使用云函数内的所有接口。 当一个前端操作数据库的方式不能完全满足需求,仍然同时需要在云端再执行一些云函数时,就在前端发起数据库操作时,通过`db.action("someactionname")`方式要求云端同时执行这个叫someactionname的action。还可以在权限规则内指定某些操作必须使用指定的action,比如`"action in ['action-a','action-b']"`,来达到更灵活的权限控制。 **注意action方法是db对象的方法,只能跟在db后面,不能跟在collection()后面** - 正确:`db.action("someactionname").collection('table1')` - 错误:`db.collection('table1').action("someactionname")` 如果使用`组件`,该组件也有action属性,设置action="someactionname"即可。 ```html ``` action是一种特殊的云函数,它不占用服务空间的云函数数量。 目前action还不支持本地运行。后续会支持。 **新建action** ![新建action](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/b6846d00-1460-11eb-b997-9918a5dda011.jpg) 每个action在uni-clientDB-actions目录下存放一个以action名称命名的js文件。 在这个js文件的代码里,包括before和after两部分,分别代表clientDB具体操作数据库前和后。 - before在clientDB执行前触发,before里的代码执行完毕后再开始操作数据库。before的常用用途: * 对前端传入的数据进行二次处理 * 在此处开启数据库事务,万一操作数据库失败,可以在after里回滚 * 如果权限或字段值域校验不想配在schema和validateFunction里,也可以在这里做校验 - after在clientDB执行后触发,clientDB操作数据库后触发before里的代码。after的常用用途: * 对将要返回给前端的数据进行二次处理 * 也可以在此处处理错误,回滚数据库事务 * 对数据库进行二次操作,比如前端查询一篇文章详情后,在此处对文章的阅读数+1。因为permission里定义,一般是要禁止前端操作文章的阅读数字段的,此时就应该通过action,在云函数里对阅读数+1 示例: ```js // 客户端发起请求,给todo表新增一行数据,同时指定action为add-todo const db = uniCloud.database() db.action('add-todo') //注意action方法是db的方法,只能跟在db后面,不能跟在collection()后面 .collection('todo') .add({ title: 'todo title' }) .then(res => { console.log(res) }).catch(err => { console.error(err) }) ``` ```js // 一个action文件示例 uni-clientDB-actions/add-todo.js module.exports = { // 在数据库操作之前执行 before: async(state,event)=>{ // state为当前clientDB操作状态其格式见下方说明 // event为传入云函数的event对象 // before内可以操作state上的newData对象对数据进行修改,比如: state.newData.create_time = Date.now() // 指定插入或修改的数据内的create_time为Date.now() // 执行了此操作之后实际插入的数据会变成 {title: 'todo title', create_time: xxxx} // 实际上,这个场景,有更简单的实现方案:在db schema内配置defaultValue或者forceDefaultValue,即可自动处理新增记录使用当前服务器时间 }, // 在数据库操作之后执行 after:async (state,event,error,result)=>{ // state为当前clientDB操作状态其格式见下方说明 // event为传入云函数的event对象 // error为执行操作的错误对象,如果没有错误error的值为null // result为执行command返回的结果 if(error) { throw error } // after内可以对result进行额外处理并返回 result.msg = 'hello' return result } } ``` **state**参数说明 ```js // state参数格式如下 { command: { // getMethod('where') 获取所有的where方法,返回结果为[{$method:'where',$param: [{a:1}]}] getMethod, // getMethod({name:'where',index: 0}) 获取第1个where方法的参数,结果为数组形式,例:[{a:1}] getParam, // setParam({name:'where',index: 0, param: [{a:1}]}) 设置第1个where方法的参数,调用之后where方法实际形式为:where({a:1}) setParam }, // 需要注意的是clientDB可能尚未获取用户信息,如果权限规则内没使用auth对象且数据库指令里面没使用db.env.uid则clientDB不会自动取获取用户信息 auth: { uid, // 用户ID,如果未获取或者获取失败uid值为null role, // 通过uni-id获取的用户角色,需要使用1.1.9以上版本的uni-id,如果未获取或者获取失败role值为[] permission // 通过uni-id获取的用户权限,需要使用1.1.9以上版本的uni-id,如果未获取或者获取失败permission值为[],注意登录时传入needPermission才可以获取permission,请参考 https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=rbac }, // 事务对象,如果需要用到事务可以在action的before内使用state.transaction = await db.startTransaction()传入 transaction, // 更新或新增的数据 newData, // 访问的集合 collection, // 操作类型,可能的值'read'、'create'、'update'、'delete' type } ```