## 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的再封装,进一步简化查询等常用数据库操作的代码量。 - 在HBuilderX 3.0+,``组件已经内置,可以直接使用。文档另见:[组件](/uniCloud/unicloud-db) - 在HBuilderX 3.0以前的版本,使用该组件需要在插件市场单独引用`插件`,另见:[https://ext.dcloud.net.cn/plugin?id=3256](https://ext.dcloud.net.cn/plugin?id=3256) 以下文章重点介绍`clientDB`的js API。至于组件的用法,另见[文档](/uniCloud/unicloud-db)。 ## clientDB前端API@jssdk `clientDB`的客户端部分主要负责提供API,允许前端编写数据库操作指令,以及处理一些客户端不太方便表示的字段,比如用户ID(详情看下面语法扩展部分) `clientDB`支持传统的nosql查询语法,并新增了`jql`查询语法。`jql`是一种更易用的查询语法。 **传统nosql查询语法示例** 这段示例代码,在一个前端页面,直接查询了云数据库的`list`表,并且指定了`name`字段值为`hello-uni-app`的查询条件,then里的res即为返回的查询结果。 ```js // 获取db引用 const db = uniCloud.database() //代码块为cdb // 使用uni-clientDB db.collection('list') .where({ name: "hello-uni-app" //传统MongoDB写法,不是jql写法。实际开发中推荐使用jql写法 }).get() .then((res)=>{ // res 为数据库查询结果 }).catch((err)=>{ console.log(err.code); // 打印错误码 console.log(err.message); // 打印错误内容 }) ``` **使用说明** 前端操作数据库的语法与云函数一致,但有以下限制(使用jql语法时也一样): - 上传时会对query进行序列化,除Date类型、RegExp之外的所有不可序列化的参数类型均不支持(例如:undefined) - 为方便控制权限,禁止前端使用set方法,一般情况下也不需要前端使用set - 更新数据库时不可使用更新操作符`db.command.inc`等 - 更新数据时键值不可使用`{'a.b.c': 1}`的形式,需要写成`{a:{b:{c:1}}}`形式(后续会对此进行优化) ### err返回值说明@returnvalue `clientDB`如果云端返回错误,err的返回值形式如下, ```js { code: "", // 错误码 message: "" // 错误信息 ... // 数据库指令执行结果 } ``` **err.code错误码列表** |错误码 |描述 | |:-: |:-: | |TOKEN_INVALID_INVALID_CLIENTID |token校验未通过(设备特征校验未通过) | |TOKEN_INVALID |token校验未通过(云端已不包含此token) | |TOKEN_INVALID_TOKEN_EXPIRED |token校验未通过(token已过期) | |TOKEN_INVALID_WRONG_TOKEN |token校验未通过(token校验未通过) | |TOKEN_INVALID_ANONYMOUS_USER |token校验未通过(当前用户为匿名用户) | |SYNTAX_ERROR |语法错误 | |PERMISSION_ERROR |权限校验未通过 | |VALIDATION_ERROR |数据格式未通过 | |DUPLICATE_KEY |索引冲突 | |SYSTEM_ERROR |系统错误 | 如需自定义返回的err对象,可以在clientDB中挂一个[action云函数](uniCloud/database?id=action),在action云函数的`after`内用js修改返回结果,传入`after`内的result不带code和message。 ### 前端环境变量@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的写法,实在过于复杂。比如如下3个例子: 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的数据记录。 **云函数中node版本为8.9不支持正则断言** **注意编写查询条件时,除test外,均为运算符左侧为数据库字段,右侧为常量** #### 查询数组字段@querywitharr 如果数据库存在以下记录 ```js { "_id": "1", "students": ["li","wang"] } { "_id": "2", "students": ["wang","li"] } { "_id": "3", "students": ["zhao","qian"] } ``` 使用jql查询语法时,可以直接使用`student=='wang'`作为查询条件来查询students内包含wang的记录。 #### 常见正则用法@regexp **搜索用户输入值** 如果使用[unicloud-db组件](uniCloud/unicloud-db.md)写法如下,使用clientDB jssdk同理 ```html ``` 上面的示例中使用了正则修饰符`i`,用于表示忽略大小写,更多修饰符见[MDN 通过标志进行高级搜索](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions#%E9%80%9A%E8%BF%87%E6%A0%87%E5%BF%97%E8%BF%9B%E8%A1%8C%E9%AB%98%E7%BA%A7%E6%90%9C%E7%B4%A2) ### 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个表名后,即可自动按照一个合并虚拟表来查询,field、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_id字段下,成为子节点。同时根据where条件设置,只返回书名为三国演义的订单记录。 ```js { "code": "", "message": "", "data": [{ "_id": "b8df3bd65f8f0d06018fdc250a5688bb", "book_id": [{ "author": "罗贯中", "title": "三国演义" }], "quantity": 555 }, { "_id": "b8df3bd65f8f0d06018fdc2315af05ec", "book_id": [{ "author": "罗贯中", "title": "三国演义" }], "quantity": 333 }] } ``` 关系型数据库做不到这种设计。`jql`充分利用了json文档型数据库的特点,实现了这个简化的联表查询方案。 不止是2个表,3个、4个表也可以通过这种方式查询。 不止js,``组件也支持所有`jql`功能,包括联表查询。 **注意** - field参数字符串内没有冒号,{}为联表查询标志 - 联表查询时关联字段会被替换成被关联表的内容,因此不可在where内使用关联字段作为条件。举个例子,在上面的示例,`where({book_id:"1"})`,但是可以使用`where({'book_id._id':"1"})` - 上述示例中如果order表的`book_id`字段是数组形式存放多个book_id,也跟上述写法一致,clientDB会自动根据字段类型进行联表查询 ### 查询列表分页 `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) ### 指定返回字段@field 查询时可以使用field方法指定返回字段,在``组件中也支持field属性。不使用field方法时会返回所有字段 field内使用jql指定返回字段,即使没有指定_id也一定会返回_id字段 ### 别名@alias 自`2020-11-20`起clientDB jql写法支持字段别名,主要用于在前端需要的字段名和数据库字段名称不一致的情况下对字段进行重命名。 用法形如:`author as book_author`,意思是将数据库的author字段重命名为book_author。 仍以上面的order表和book表为例 ```js // 客户端联表查询 const db = uniCloud.database() db.collection('order,book') .where('book_id.title == "三国演义"') .field('book_id{title as book_title,author as book_author},quantity as order_quantity') // 这里联表查询book表返回book表内的title、book表内的author、order表内的quantity,并将title重命名为book_title,author重命名为book_author,quantity重命名为order_quantity .orderBy('order_quantity desc') // 按照order_quantity降序排列 .get() .then(res => { console.log(res); }).catch(err => { console.error(err) }) ``` 上述请求返回的res如下 ```js { "code": "", "message": "", "data": [{ "_id": "b8df3bd65f8f0d06018fdc250a5688bb", "book_id": [{ "book_author": "罗贯中", "book_title": "三国演义" }], "order_quantity": 555 }, { "_id": "b8df3bd65f8f0d06018fdc2315af05ec", "book_id": [{ "book_author": "罗贯中", "book_title": "三国演义" }], "order_quantity": 333 }] } ``` **注意** - 上面的查询指令中,上一阶段处理结果输出到下一阶段,上面的例子中表现为where中使用的是原名,orderBy中使用的是别名 - 目前不支持对联表查询的关联字段使用别名,即上述示例中的book_id不可设置别名 ### 对字段操作后返回@operator 自`HBuilderX 3.0.8`起,clientDB支持对字段进行一定的操作之后再返回,详细可用的方法列表请参考:[聚合操作符](uniCloud/clientdb.md?id=aggregate-operator) 例:数据表class内有以下数据 ```js { "_id": "1", "grade": 6, "class": "A" } { "_id": "1", "grade": 2, "class": "A" } ``` 如下写法可以由grade计算得到一个isTopGrade来表示是否为最高年级 ```js const res = await db.collection('class') .field('class,eq(grade,6) as isTopGrade') .get() ``` 返回结果如下 ```js { "_id": "1", "class": "A", "isTopGrade": true } { "_id": "1", "class": "A", "isTopGrade": false } ``` **注意** - 如果要访问数组的某一项请使用arrayElemAt操作符,形如:`arrayElemAt(arr,1)` - 在进行权限校验时,会计算field内访问的所有字段计算权限。上面的例子中会使用表的read权限和grade、class字段的权限,来进行权限校验。 ### 排序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": "吴承恩" } } ``` ### 查询树形数据@gettree HBuilderX 3.0.3+起,clientDB支持在get方法内传入getTree参数查询树状结构数据。(HBuilderX 3.0.5+ unicloud-db组件开始支持,之前版本只能通过js方式使用) 树形数据,在数据库里一般不会按照tree的层次来存储,因为按tree结构通过json对象的方式存储不同层级的数据,不利于对tree上的某个节点单独做增删改查。 一般存储树形数据,tree上的每个节点都是一条单独的数据表记录,然后通过类似parent_id来表达父子关系。 如部门的数据表,里面有2条数据,一条数据记录是“总部”,`parent_id`为空;另一条数据记录“一级部门A”,`parent_id`为总部的`_id` ```json { "_id": "5fe77207974b6900018c6c9c", "name": "总部", "parent_id": "", "status": 0 } ``` ```json { "_id": "5fe77232974b6900018c6cb1", "name": "一级部门A", "parent_id": "5fe77207974b6900018c6c9c", "status": 0 } ``` 虽然存储格式是分条记录的,但查询反馈到前端的数据仍然需要是树形的。这种转换在过去比较复杂。 clientDB提供了一种简单、优雅的方案,在DB Schema里配置parentKey来表达父子关系,然后查询时声明使用Tree查询,就可以直接查出树形数据。 department部门表的schema中,将字段`parent_id`的"parentKey"设为"_id",即指定了数据之间的父子关系,如下: ```json { "bsonType": "object", "required": ["name"], "properties": { "_id": { "description": "ID,系统自动生成" }, "name": { "bsonType": "string", "description": "名称" }, "parent_id": { "bsonType": "string", "description": "父id", "parentKey": "_id", // 指定父子关系为:如果数据库记录A的_id和数据库记录B的parent_id相等,则A是B的父级。 }, "status": { "bsonType": "int", "description": "部门状态,0-正常、1-禁用" } } } ``` parentKey字段将数据表不同记录的父子关系描述了出来。查询就可以直接写了。 注意一个表内只能有一个父子关系,即一个表的schema里只能配置一份parentKey。 schema里描述好后,查询就变的特别简单。 查询树形数据,分为 查询所有子节点 和 查询父级路径 这2种需求。 #### 查询所有子节点 指定符合条件的记录,然后查询它的所有子节点,并且可以指定层级,返回的结果是以符合条件的记录为一级节点的所有子节点数据,并以树形方式嵌套呈现。 只需要在clientDB的get方法中增加`getTree`参数,如下 ```js // get方法示例 get({ getTree: { limitLevel: 10, // 最大查询层级(不包含当前层级),可以省略默认10级,最大15,最小1 startWith: "parent_code==''" // 第一层级条件,此初始条件可以省略,不传startWith时默认从最顶级开始查询 } }) ``` 完整的代码如下: ```js db.collection("department").get({ getTree: {} }) .then((res) => { const resdata = res.result.data console.log("resdata", resdata); }).catch((err) => { uni.showModal({ content: err.message || '请求服务失败', showCancel: false }) }).finally(() => { }) ``` 查询的结果如下: ```json "data": [{ "_id": "5fe77207974b6900018c6c9c", "name": "总部", "parent_id": "", "status": 0, "children": [{ "_id": "5fe77232974b6900018c6cb1", "name": "一级部门A", "parent_id": "5fe77207974b6900018c6c9c", "status": 0, "children": [] }] }] ``` 可以看出,每个子节点,被嵌套在父节点的"children"下,这个"children"是一个固定的格式。 如果不指定getTree的参数,会把department表的所有数据都查出来,从总部开始到10级部门,以树形结构提供给客户端。 如果有多个总部,即多行记录的`parent_id`为空,则多个总部会分别作为一级节点,把它们下面的所有children一级一级拉出来。如下: ```json "data": [ { "_id": "5fe77207974b6900018c6c9c", "name": "总部", "parent_id": "", "status": 0, "children": [{ "_id": "5fe77232974b6900018c6cb1", "name": "一级部门A", "parent_id": "5fe77207974b6900018c6c9c", "status": 0, "children": [] }] }, { "_id": "5fe778a10431ca0001c1e2f8", "name": "总部2", "parent_id": "", "children": [{ "_id": "5fe778e064635100013efbc2", "name": "总部2的一级部门B", "parent_id": "5fe778a10431ca0001c1e2f8", "children": [] }] } ] ``` 如果觉得返回的`parent_id`字段多余,也可以指定`.field("_id,name")`,过滤掉该字段。 **getTree的参数limitLevel的说明** limitLevel表示查询返回的树的最大层级。超过设定层级的节点不会返回。 - limitLevel的默认值为10。 - limitLevel的合法值域为1-15之间(包含1、15)。如果数据实际层级超过15层,请分层懒加载查询。 - limitLevel为1,表示向下查一级子节点。假如数据库中有2级、3级部门,如果设limitLevel为1,且查询的是“总部”,那么返回数据包含“总部”和其下的一级部门。 **getTree的参数startWith的说明** 如果只需要查“总部”的子部门,不需要“总部2”,可以在startWith里指定(`getTree: {"startWith":"name=='总部'"}`)。 使用中请注意startWith和where的区别。where用于描述对所有层级的生效的条件(包括第一层级)。而startWith用于描述从哪个或哪些节点开始查询树。 startWith不填时,默认的条件是 `'parent_id==null||parent_id==""'`,即schema配置parentKey的字段为null(即不存在)或值为空字符串时,这样的节点被默认视为根节点。 假设上述部门表内有以下数据 ```js { "_id": "1", "name": "总部", "parent_id": "", "status": 0 } { "_id": "11", "name": "一级部门A", "parent_id": "1", "status": 0 } { "_id": "12", "name": "一级部门B", "parent_id": "1", "status": 1 } ``` 以下查询语句指定startWith为`_id=="1"`、where条件为`status==0`,查询总部下所有status为0的子节点。 ```js db.collection("department") .where('status==0') .get({ getTree: { startWith: '_id=="1"' } }) .then((res) => { const resdata = res.result.data console.log("resdata", resdata); }).catch((err) => { uni.showModal({ content: err.message || '请求服务失败', showCancel: false }) }).finally(() => { }) ``` 查询的结果如下: ```json { "data": [{ "_id": "1", "name": "总部", "parent_id": "", "status": 0, "children": [{ "_id": "11", "name": "一级部门A", "parent_id": "1", "status": 0, "children": [] }] }] } ``` **需要注意的是where内的条件也会对第一级数据生效**,例如将上面的查询改成如下写法 ```js db.collection("department") .where('status==1') .get({ getTree: { startWith: '_id=="1"' } }) .then((res) => { const resdata = res.result.data console.log("resdata", resdata); }).catch((err) => { uni.showModal({ content: err.message || '请求服务失败', showCancel: false }) }).finally(() => { }) ``` 此时将无法查询到数据,返回结果如下 ```js { "data": [] } ``` **示例** 插件市场有一个 家谱 的示例,可以参阅:[https://ext.dcloud.net.cn/plugin?id=3798](https://ext.dcloud.net.cn/plugin?id=3798) **大数据量的树形数据查询** 如果tree的数据量较大,则不建议一次性把所有的树形数据返回给客户端。建议分层查询,即懒加载。 比如地区选择的场景,全国的省市区数据量很大,一次性查询所有数据返回给客户端非常耗时和耗流量。可以先查省,然后根据选择的省再查市,以此类推。 **注意** - 暂不支持使用getTree的同时使用联表查询 - 如果使用了where条件会对所有查询的节点生效 - 如果使用了limit设置最大返回数量仅对根节点生效 #### 查询树形结构父节点路径@gettreepath getTree是查询子节点,而getTreePath,则是查询父节点。 get方法内传入getTreePath参数对包含父子关系的表查询返回树状结构数据某节点路径。 ```js // get方法示例 get({ getTreePath: { limitLevel: 10, // 最大查询层级(不包含当前层级),可以省略默认10级,最大15,最小1 startWith: 'name=="一级部门A"' // 末级节点的条件,此初始条件不可以省略 } }) ``` 查询返回的结果为,从“一级部门A”起向上找10级,找到最终节点后,以该节点为根,向下嵌套children,一直到达“一级部门A”。 返回结果只包括“一级部门A”的直系父,其父节点的兄弟节点不会返回。所以每一层数据均只有一个节点。 仍以上面department的表结构和数据为例 ```js db.collection("department").get({ getTreePath: { "startWith": "_id=='5fe77232974b6900018c6cb1'" } }) .then((res) => { const treepath = res.result.data console.log("treepath", treepath); }).catch((err) => { uni.showModal({ content: err.message || '请求服务失败', showCancel: false }) }).finally(() => { uni.hideLoading() // console.log("finally") }) ``` 查询返回结果 从根节点“总部”开始,返回到“一级部门A”。“总部2”等节点不会返回。 ```json { "data": [{ "_id": "5fe77207974b6900018c6c9c", "name": "总部", "parent_id": "", "children": [{ "_id": "5fe77232974b6900018c6cb1", "name": "一级部门A", "parent_id": "5fe77207974b6900018c6c9c" }] }] } ``` 如果startWith指定的节点没有父节点,则返回自身。 如果startWith匹配的节点不止一个,则以数组的方式,返回每个节点的treepath。 例如“总部”和“总部2”下面都有一个部门的名称叫“销售部”,且` "startWith": "name=='销售部'"`,则会返回“总部”和“总部2”两条treepath,如下 ```json { "data": [{ "_id": "5fe77207974b6900018c6c9c", "name": "总部", "parent_id": "", "children": [{ "_id": "5fe77232974b6900018c6cb1", "name": "销售部", "parent_id": "5fe77207974b6900018c6c9c" }] }, { "_id": "5fe778a10431ca0001c1e2f8", "name": "总部2", "parent_id": "", "children": [{ "_id": "5fe79fea23976b0001508a46", "name": "销售部", "parent_id": "5fe778a10431ca0001c1e2f8" }] }] } ``` **注意** - 暂不支持使用getTreePath的同时使用其他联表查询语法 - 如果使用了where条件会对所有查询的节点生效 ### 分组统计@groupby 自`HBuilderX 3.0.8`起,clientDB支持分组对数据进行分组统计(groupBy) 如果数据库`score`表为某次比赛统计的分数数据,每条记录为一个学生的分数 ```js { _id: "1", grade: "1", class: "A", name: "zhao", score: 5 } { _id: "2", grade: "1", class: "A", name: "qian", score: 15 } { _id: "3", grade: "1", class: "B", name: "li", score: 15 } { _id: "4", grade: "1", class: "B", name: "zhou", score: 25 } { _id: "5", grade: "2", class: "A", name: "wu", score: 25 } { _id: "6", grade: "2", class: "A", name: "zheng", score: 35 } ``` #### 求和、求均值等累计操作 所有可用的累计方法请参考[累计器操作符](uniCloud/clientdb.md?id=accumulator),下面以sum(求和)和avg(求均值)为例介绍如何使用 使用sum方法可以对数据进行求和统计。以上述数据为例,如下写法对不同班级进行分数统计 ```js const res = await db.collection('score') .groupBy('grade,class') .groupField('sum(score) as totalScore') .get() ``` 返回结果如下 ```js { data: [{ grade: "1", class: "A", totalScore: 20 },{ grade: "1", class: "B", totalScore: 40 },{ grade: "2", class: "A", totalScore: 60 }] } ``` 求均值方法与求和类似,将上面sum方法换成avg方法即可 ```js const res = await db.collection('score') .groupBy('grade,class') .groupField('avg(score) as avgScore') .get() ``` 返回结果如下 ```js { data: [{ grade: "1", class: "A", avgScore: 10 },{ grade: "1", class: "B", avgScore: 20 },{ grade: "2", class: "A", avgScore: 30 }] } ``` 如果额外还在groupBy之前使用了preField方法,此preField用于决定将哪些数据传给groupBy和groupField使用 例:如果上述数据中score是一个数组 ```js { _id: "1", grade: "1", class: "A", name: "zhao", score: [1,1,1,1,1] } { _id: "2", grade: "1", class: "A", name: "qian", score: [3,3,3,3,3] } { _id: "3", grade: "1", class: "B", name: "li", score: [3,3,3,3,3] } { _id: "4", grade: "1", class: "B", name: "zhou", score: [5,5,5,5,5] } { _id: "5", grade: "2", class: "A", name: "wu", score: [5,5,5,5,5] } { _id: "6", grade: "2", class: "A", name: "zheng", score: [7,7,7,7,7] } ``` 如下preField写法将上面的score数组求和之后传递给groupBy和groupField使用。在preField内没出现的字段(比如name),在后面的方法里面不能使用 ```js const res = await db.collection('score') .preField('grade,class,sum(score) as userTotalScore') .groupBy('grade,class') .groupField('avg(userTotalScore) as avgScore') .get() ``` 返回结果如下 ```js { data: [{ grade: "1", class: "A", avgScore: 10 },{ grade: "1", class: "B", avgScore: 20 },{ grade: "2", class: "A", avgScore: 30 }] } ``` **注意** - 在上面使用preField方法的情况下,会计算preField内访问的所有字段计算权限。上面的例子中会使用表的read权限和grade、class、score三个字段的权限,来进行权限校验。 - 在不使用preField,仅使用groupBy和groupField的情况下,会以groupBy和groupField内访问的所有字段的权限来校验访问是否合法。 - 与field不同groupBy不会包含_id,除非你手动指定 #### 统计数量 使用count方法可以对记录数量进行统计。以上述数据为例,如下写法对不同班级统计参赛人数 ```js const res = await db.collection('score') .groupBy('grade,class') .groupField('count(*) as totalStudents') .get() ``` 返回结果如下 ```js { data: [{ grade: "1", class: "A", totalStudents: 2 },{ grade: "1", class: "B", totalStudents: 2 },{ grade: "2", class: "A", totalStudents: 2 }] } ``` **注意** - `count(*)`为固定写法,*可以省略 ### 数据去重@distinct 通过.distinct()方法,对数据查询结果中重复的记录进行去重。 distinct方法将按照field方法指定的字段进行去重(如果field内未指定_id,不会按照_id去重) > `HBuilderX 3.0.8`+ ```js const res = await db.collection('table1') .field('field1') .distinct() // 注意distinct方法没有参数 .get() ``` 例:如果数据库`score`表为某次比赛统计的分数数据,每条记录为一个学生的分数 `score`表的数据: ```js { _id: "1", grade: "1", class: "A", name: "zhao", score: 5 } { _id: "2", grade: "1", class: "A", name: "qian", score: 15 } { _id: "3", grade: "1", class: "B", name: "li", score: 15 } { _id: "4", grade: "1", class: "B", name: "zhou", score: 25 } { _id: "5", grade: "2", class: "A", name: "wu", score: 25 } { _id: "6", grade: "2", class: "A", name: "zheng", score: 35 } ``` 以下代码可以按照grade、class两字段去重,获取所有参赛班级 ```js const res = await db.collection('score') .field('grade,class') .distinct() // 注意distinct方法没有参数 .get() ``` 查询返回结果如下 ```js { data: [{ grade:"1", class: "A" },{ grade:"1", class: "B" },{ grade:"2", class: "A" }] } ``` **注意** - distinct指对返回结果中完全相同的记录进行去重,重复的记录只保留一条。因为`_id`字段是必然不同的,所以使用distinct时必须同时指定field,且field中不可存在`_id`字段 ### 新增数据记录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}}}`形式(后续会对此进行优化) ### 其他数据库操作 clientDB API支持使用聚合操作读取数据,关于聚合操作请参考[聚合操作](uniCloud/cf-database.md?id=aggregate) 例:取status等于1的随机20条数据 ```js const db = uniCloud.database() const res = await db.collection('test').aggregate() .match({ status: 1 }) .sample({ size: 20 }) .end() ``` **注意** - 目前``组件暂不支持使用聚合操作读取数据 ### 刷新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.on('refreshToken', refreshToken) // 解绑刷新token事件 db.off('refreshToken', refreshToken) ``` **注意:HBuilderX 3.0.0之前请使用db.auth.on、db.auth.off,HBuilderX 3.0.0以上版本仍兼容旧写法,但是推荐使用新写法db.on** ### 错误处理@error 全局clientDB错误事件,HBuilderX 3.0.0起支持。 **用法** ```js const db = uniCloud.database() function onDBError({ code, // 错误码详见https://uniapp.dcloud.net.cn/uniCloud/clientdb?id=returnvalue message }) { // 处理错误 } // 绑定clientDB错误事件 db.on('error', onDBError) // 解绑clientDB错误事件 db.off('error', onDBError) ``` ## 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() ``` 在进行数据库操作之前,clientDB会使用permission内配置的规则对客户端操作进行一次校验,如果本次校验不通过还会通过数据库查询再进行一次校验 例1: ```js // 数据库内news表有以下数据 { _id: "1", user_id: "uid_1", title: "abc" } ``` ```js // news表对应的schema内做如下配置 { "bsonType": "object", "permission": { // 表级权限 "read": true, "update": "doc.user_id == auth.uid" // 只允许修改自己的数据 }, "properties": { "user_id": { "bsonType": "string" }, "title": { "bsonType": "string" } } } ``` ```js // 用户ID为uid_1的用户在客户端使用如下操作 db.collection('news').doc('1').update({ title: 'def' }) ``` 此时客户端条件里面只有`doc._id == 1`,schema内又限制的`doc.user_id == auth.uid`,所以第一次预校验无法通过,会进行一次查库校验判断是否有权限进行操作。发现auth.uid确实和doc.user_id一致,上面的数据库操作允许执行。 例2: ```js // 数据库内goods表有以下数据 { _id: "1", name: "n1", status: 1 } { _id: "2", name: "n2", status: 2 } { _id: "3", name: "n3", status: 3 } ``` ```js // news表对应的schema内做如下配置 { "bsonType": "object", "permission": { // 表级权限 "read": "doc.status > 1", }, "properties": { "name": { "bsonType": "string" }, "status": { "bsonType": "int" } } } ``` ```js // 用户在客户端使用如下操作,可以通过第一次校验,不会触发查库校验 db.collection('goods').where('status > 1').get() // 用户在客户端使用如下操作,无法通过第一次校验,会触发一次查库校验(原理大致是使用name == "n3" && status <= 1作为条件进行一次查询,如果有结果就认为没有权限访问,了解即可,无需深入) db.collection('goods').where('name == "n3"').get() // 用户在客户端使用如下操作,无法通过第一次校验,会触发一次查库校验,查库校验也会无法通过 db.collection('goods').where('name == "n1"').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中使用全局变量,如果一定要用请务必确保自己已经阅读并理解了[云函数的启动模式](uniCloud/cf-functions.md?id=launchtype)** 如果使用`组件`,该组件也有action属性,设置action="someactionname"即可。 ```html ``` action支持一次使用多个,比如使用`db.action("action-a,action-b")`,其执行流程为`action-a.before->action-b.before->执行数据库操作->action-b.after->action-a.after`。在任一before环节抛出错误直接进入after流程,在after流程内抛出的错误会被传到下一个after流程。 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里回滚 * 使用throw阻止运行 * 如果权限或字段值域校验不想配在schema和validateFunction里,也可以在这里做校验 - after在clientDB执行后触发,clientDB操作数据库后触发after里的代码。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, // getParam({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 } ``` ## 可用聚合操作符列表@aggregate-operator 为方便书写,clientDB内将聚合操作符的用法进行了简化(相对于云函数内使用聚合操作符而言)。以下是可以在clientDB中使用的聚合操作符 |操作符 |详细文档 |用法 |说明 | |--- |--- |--- |--- | |abs |[abs](uniCloud/cf-database.md?id=abs) |abs(<表达式>) |- | |add |[add](uniCloud/cf-database.md?id=add-1) |add(<表达式1>,<表达式2>) |- | |ceil |[ceil](uniCloud/cf-database.md?id=ceil) |ceil(<表达式>) |- | |divide |[divide](uniCloud/cf-database.md?id=divide) |divide(<表达式1>,<表达式2>) |- | |exp |[exp](uniCloud/cf-database.md?id=exp) |exp(<表达式>) |- | |floor |[floor](uniCloud/cf-database.md?id=floor) |floor(<表达式>) |- | |ln |[ln](uniCloud/cf-database.md?id=ln) |ln(<表达式>) |- | |log |[log](uniCloud/cf-database.md?id=log) |log(<表达式1>,<表达式2>) |- | |log10 |[log10](uniCloud/cf-database.md?id=log10) |log10(<表达式>) |- | |mod |[mod](uniCloud/cf-database.md?id=mod) |mod(<表达式1>,<表达式2>) |- | |multiply |[multiply](uniCloud/cf-database.md?id=multiply) |multiply(<表达式1>,<表达式2>) |- | |pow |[pow](uniCloud/cf-database.md?id=pow) |pow(<表达式1>,<表达式2>) |- | |sqrt |[sqrt](uniCloud/cf-database.md?id=sqrt) |sqrt(<表达式1>,<表达式2>) |- | |subtract |[subtract](uniCloud/cf-database.md?id=subtract) |subtract(<表达式1>,<表达式2>) |- | |trunc |[trunc](uniCloud/cf-database.md?id=trunc) |trunc(<表达式>) |- | |arrayElemAt |[arrayElemAt](uniCloud/cf-database.md?id=arrayelemat) |arrayElemAt(<表达式1>,<表达式2>) |- | |arrayToObject |[arrayToObject](uniCloud/cf-database.md?id=arraytoobject) |arrayToObject(<表达式>) |- | |concatArrays |[concatArrays](uniCloud/cf-database.md?id=concatarrays) |concatArrays(<表达式1>,<表达式2>) |- | |filter |[filter](uniCloud/cf-database.md?id=filter) |filter(,,) |- | |in |[in](uniCloud/cf-database.md?id=in) |in(<表达式1>,<表达式2>) |- | |indexOfArray |[indexOfArray](uniCloud/cf-database.md?id=indexofarray) |indexOfArray(<表达式1>,<表达式2>) |- | |isArray |[isArray](uniCloud/cf-database.md?id=isarray) |isArray(<表达式>) |- | |map |[map](uniCloud/cf-database.md?id=map) |map(,,) |- | |objectToArray |[objectToArray](uniCloud/cf-database.md?id=objecttoarray) |objectToArray(<表达式>) |- | |range |[range](uniCloud/cf-database.md?id=range) |range(<表达式1>,<表达式2>) |- | |reduce |[reduce](uniCloud/cf-database.md?id=reduce) |reduce(,,) |- | |reverseArray |[reverseArray](uniCloud/cf-database.md?id=reversearray) |reverseArray(<表达式>) |- | |size |[size](uniCloud/cf-database.md?id=size) |size(<表达式>) |- | |slice |[slice](uniCloud/cf-database.md?id=slice) |slice(<表达式1>,<表达式2>) |- | |zip |[zip](uniCloud/cf-database.md?id=zip) |zip(,,) |- | |and |[and](uniCloud/cf-database.md?id=and) |and(<表达式1>,<表达式2>) |- | |not |[not](uniCloud/cf-database.md?id=not) |not(<表达式>) |- | |or |[or](uniCloud/cf-database.md?id=or) |or(<表达式1>,<表达式2>) |- | |cmp |[cmp](uniCloud/cf-database.md?id=cmp) |cmp(<表达式1>,<表达式2>) |- | |eq |[eq](uniCloud/cf-database.md?id=eq) |eq(<表达式1>,<表达式2>) |- | |gt |[gt](uniCloud/cf-database.md?id=gt) |gt(<表达式1>,<表达式2>) |- | |gte |[gte](uniCloud/cf-database.md?id=gte) |gte(<表达式1>,<表达式2>) |- | |lt |[lt](uniCloud/cf-database.md?id=lt) |lt(<表达式1>,<表达式2>) |- | |lte |[lte](uniCloud/cf-database.md?id=lte) |lte(<表达式1>,<表达式2>) |- | |neq |[neq](uniCloud/cf-database.md?id=neq) |neq(<表达式1>,<表达式2>) |- | |cond |[cond](uniCloud/cf-database.md?id=cond) |cond(<表达式1>,<表达式2>) |- | |ifNull |[ifNull](uniCloud/cf-database.md?id=ifnull) |ifNull(<表达式1>,<表达式2>) |- | |switch |[switch](uniCloud/cf-database.md?id=switch) |switch(,) |- | |dateFromParts |[dateFromParts](uniCloud/cf-database.md?id=datefromparts) |dateFromParts(,,,,,,,) |- | |isoDateFromParts |[isoDateFromParts](uniCloud/cf-database.md?id=isodatefromparts)|isoDateFromParts(,,,,,,,) |云函数内此操作符对应dateFromParts| |dateFromString |[dateFromString](uniCloud/cf-database.md?id=datefromstring) |dateFromString(,,,,) |- | |dateToString |[dateToString](uniCloud/cf-database.md?id=datetostring) |dateToString(,,,) |- | |dayOfMonth |[dayOfMonth](uniCloud/cf-database.md?id=dayofmonth) |dayOfMonth(,) |- | |dayOfWeek |[dayOfWeek](uniCloud/cf-database.md?id=dayofweek) |dayOfWeek(,) |- | |dayOfYear |[dayOfYear](uniCloud/cf-database.md?id=dayofyear) |dayOfYear(,) |- | |hour |[hour](uniCloud/cf-database.md?id=hour) |hour(,) |- | |isoDayOfWeek |[isoDayOfWeek](uniCloud/cf-database.md?id=isodayofweek) |isoDayOfWeek(,) |- | |isoWeek |[isoWeek](uniCloud/cf-database.md?id=isoweek) |isoWeek(,) |- | |isoWeekYear |[isoWeekYear](uniCloud/cf-database.md?id=isoweekyear) |isoWeekYear(,) |- | |millisecond |[millisecond](uniCloud/cf-database.md?id=millisecond) |millisecond(,) |- | |minute |[minute](uniCloud/cf-database.md?id=minute) |minute(,) |- | |month |[month](uniCloud/cf-database.md?id=month) |month(,) |- | |second |[second](uniCloud/cf-database.md?id=second) |second(,) |- | |week |[week](uniCloud/cf-database.md?id=week) |week(,) |- | |year |[year](uniCloud/cf-database.md?id=year) |year(,) |- | |literal |[literal](uniCloud/cf-database.md?id=literal) |literal(<表达式>) |- | |mergeObjects |[mergeObjects](uniCloud/cf-database.md?id=mergeobjects) |mergeObjects(<表达式1>,<表达式2>) |- | |allElementsTrue |[allElementsTrue](uniCloud/cf-database.md?id=allelementstrue) |allElementsTrue(<表达式1>,<表达式2>) |- | |anyElementTrue |[anyElementTrue](uniCloud/cf-database.md?id=anyelementtrue) |anyElementTrue(<表达式1>,<表达式2>) |- | |setDifference |[setDifference](uniCloud/cf-database.md?id=setdifference) |setDifference(<表达式1>,<表达式2>) |- | |setEquals |[setEquals](uniCloud/cf-database.md?id=setequals) |setEquals(<表达式1>,<表达式2>) |- | |setIntersection |[setIntersection](uniCloud/cf-database.md?id=setintersection) |setIntersection(<表达式1>,<表达式2>) |- | |setIsSubset |[setIsSubset](uniCloud/cf-database.md?id=setissubset) |setIsSubset(<表达式1>,<表达式2>) |- | |setUnion |[setUnion](uniCloud/cf-database.md?id=setunion) |setUnion(<表达式1>,<表达式2>) |- | |concat |[concat](uniCloud/cf-database.md?id=concat) |concat(<表达式1>,<表达式2>) |- | |indexOfBytes |[indexOfBytes](uniCloud/cf-database.md?id=indexofbytes) |indexOfBytes(<表达式1>,<表达式2>) |- | |indexOfCP |[indexOfCP](uniCloud/cf-database.md?id=indexofcp) |indexOfCP(<表达式1>,<表达式2>) |- | |split |[split](uniCloud/cf-database.md?id=split) |split(<表达式1>,<表达式2>) |- | |strLenBytes |[strLenBytes](uniCloud/cf-database.md?id=strlenbytes) |strLenBytes(<表达式>) |- | |strLenCP |[strLenCP](uniCloud/cf-database.md?id=strlencp) |strLenCP(<表达式>) |- | |strcasecmp |[strcasecmp](uniCloud/cf-database.md?id=strcasecmp) |strcasecmp(<表达式1>,<表达式2>) |- | |substr |[substr](uniCloud/cf-database.md?id=substr) |substr(<表达式1>,<表达式2>) |- | |substrBytes |[substrBytes](uniCloud/cf-database.md?id=substrbytes) |substrBytes(<表达式1>,<表达式2>) |- | |substrCP |[substrCP](uniCloud/cf-database.md?id=substrcp) |substrCP(<表达式1>,<表达式2>) |- | |toLower |[toLower](uniCloud/cf-database.md?id=tolower) |toLower(<表达式>) |- | |toUpper |[toUpper](uniCloud/cf-database.md?id=toupper) |toUpper(<表达式>) |- | |addToSet |[addToSet](uniCloud/cf-database.md?id=addtoset) |addToSet(<表达式>) |- | |avg |[avg](uniCloud/cf-database.md?id=avg) |avg(<表达式>) |- | |first |[first](uniCloud/cf-database.md?id=first) |first(<表达式>) |- | |last |[last](uniCloud/cf-database.md?id=last) |last(<表达式>) |- | |max |[max](uniCloud/cf-database.md?id=max) |max(<表达式>) |- | |min |[min](uniCloud/cf-database.md?id=min) |min(<表达式>) |- | |push |[push](uniCloud/cf-database.md?id=push) |push(<表达式>) |- | |stdDevPop |[stdDevPop](uniCloud/cf-database.md?id=stddevpop) |stdDevPop(<表达式>) |- | |stdDevSamp |[stdDevSamp](uniCloud/cf-database.md?id=stddevsamp) |stdDevSamp(<表达式>) |- | |sum |[sum](uniCloud/cf-database.md?id=sum) |sum(<表达式>) |- | |let |[let](uniCloud/cf-database.md?id=let) |let(,) |- | 以上操作符还可以组合使用 例:数据表article内有以下数据 ```js { "_id": "1", "publish_date": 1611141512751, "content": "hello uniCloud content 01", "content": "hello uniCloud title 01", } { "_id": "2", "publish_date": 1611141512752, "content": "hello uniCloud content 02", "content": "hello uniCloud title 02", } { "_id": "3", "publish_date": 1611141512753, "content": "hello uniCloud content 03", "content": "hello uniCloud title 03", } ``` 可以通过以下查询将publish_date转为`2021-01-20`形式,然后进行分组统计 ```js const res = await db.collection('article') .groupBy('dateToString(add(new Date(0),publish_date),"%Y-%m-%d","+0800") as publish_date_str') .groupField('count(*) as total') .get() ``` 上述代码使用add将publish_date时间戳转为日期类型,再用dateToString将上一步的日期按照时区'+0800'(北京时间),格式化为`4位年-2位月-2位日`格式,完整格式化参数请参考[dateToString](uniCloud/cf-database.md?id=datetostring)。 上述代码执行结果为 ```js res = { result: { data: [{ publish_date_str: '2021-01-20', total: 3 }] } } ``` ### 累计器操作符@accumulator |操作符 |详细文档 |用法 |说明 | |--- |--- |--- |--- | |addToSet |[addToSet](uniCloud/cf-database.md?id=addtoset) |addToSet(<表达式>) |- | |avg |[avg](uniCloud/cf-database.md?id=avg) |avg(<表达式>) |- | |first |[first](uniCloud/cf-database.md?id=first) |first(<表达式>) |- | |last |[last](uniCloud/cf-database.md?id=last) |last(<表达式>) |- | |max |[max](uniCloud/cf-database.md?id=max) |max(<表达式>) |- | |min |[min](uniCloud/cf-database.md?id=min) |min(<表达式>) |- | |push |[push](uniCloud/cf-database.md?id=push) |push(<表达式>) |- | |stdDevPop |[stdDevPop](uniCloud/cf-database.md?id=stddevpop) |stdDevPop(<表达式>) |- | |stdDevSamp |[stdDevSamp](uniCloud/cf-database.md?id=stddevsamp) |stdDevSamp(<表达式>) |- | |sum |[sum](uniCloud/cf-database.md?id=sum) |sum(<表达式>) |- | |mergeObjects |[mergeObjects](uniCloud/cf-database.md?id=mergeobjects)|mergeObjects(<表达式1>)|在groupField内使用时仅接收一个参数 |