简体中文
DB Schema
是基于 JSON 格式定义的数据结构的规范。
每张表/集合,都有一个表名.schema.json的文件,来描述表的信息、字段的信息。
一个表的简单schema.json示例如下
{
"bsonType": "object", // 固定节点
"description": "该表的描述",
"required": [], // 必填字段列表
"properties": { // 该表的字段清单
"_id": { // 字段名称,每个表都会带有_id字段
"description": "ID,系统自动生成"
// 这里还有很多字段属性可以设置
},
"field2": { // 字段2,每个表都会带有_id字段
"description": ""
// 这里还有很多字段属性可以设置
}
}
}
DB Schema
有很多重要的作用:
除schema外jql还支持使用js编写schema扩展,在数据的增删改查时触发相应的触发器,详见:DB schema 扩展
MongoDB支持通过 $jsonSchema 操作符在插入和更新文档时进行结构验证(非空、类型校验等), $jsonSchema 支持 JSON Schema的草案4,包括core specification和validation specification。uniCloud在MongoDB基础上进行了JSON Schema扩展。
编写DB Schema
是uniCloud的数据库开发的重要环节。但必须通过JQL操作数据库才能发挥DB Schema
的价值。
所以注意,在云函数中使用传统MongoDB API操作数据库时DB Schema
不生效。不管在客户端还是云端,都必须使用JQL操作数据库。
如果你的应用可以通过clientDB完成,那么这样将无需编写服务器代码,整体开发效率会极大提升。客户端操作数据库时必须完全编写DB Schema
,尤其权限部分。
如果应用的权限系统比较复杂,使用clientDB不如使用云对象方便,也应该编写好除了权限部分以外的其他的schema。这样联表查询、tree查询、默认值、值域校验等其他功能仍然可以方便使用。
具体来说,如自己在云函数中编写权限控制代码,则需要把DB Schema
的权限都设为false,在云函数中将操作角色设为admin(通过setuser API),以跳过schema的权限验证。
当然,云函数中代码控制的权限和DB Schema
中的权限也可以混合使用,简单权限交由DB Schema
处理,负责权限再编写代码处理。
所以建议开发者编写好schema,无论云端还是前端操作数据库。最多是云函数处理权限时忽略schema中的权限部分。
在HBuilderX中编写schema,有良好的语法提示和语法校验,还可以本地调试,是更为推荐的schema编写方案。
创建schema
uniCloud
项目右键,选择创建database目录
(如已有目录则忽略)新建数据集合schema
HBuilderX内创建的schema新建和保存时不会自动上传
上传schema
下载schema
HBuilderX中运行前端项目,在控制台选择连接本地云函数,或者本地云函数/云对象直接运行,此时本地编写的schema可直接生效,无需上传。方便编写调试。
web控制台上编辑DB Schema
保存后是实时在现网生效的,请注意对现网商用项目的影响。
{
"bsonType": "object", // 固定节点
"description": "表的描述",
"required": [], // 必填字段
"permission": {
"read": false, // 前端非admin的读取记录权限控制。默认值是false,即可以不写。可以简单的true/false,也可以写表达式
"create": false, // 前端非admin的新增记录权限控制。默认值是false,即可以不写。可以简单的true/false,也可以写表达式
"update": false, // 前端非admin的更新记录权限控制。默认值是false,即可以不写。可以简单的true/false,也可以写表达式
"delete": false, // 前端非admin的删除记录权限控制。默认值是false,即可以不写。可以简单的true/false,也可以写表达式
"count": false // 前端非admin的求数权限控制。默认值是true,即可以不写。可以简单的true/false,也可以写表达式
},
"properties": { // 表的字段清单
"_id": { // 字段名称,每个表都会带有_id字段
"description": "ID,系统自动生成"
// 这里还有很多字段属性可以设置
}
},
"fieldRules":[
// 字段之间的约束关系。比如字段开始时间小于字段结束时间。也可以只校验一个字段。支持表达式
]
}
注意
properties里的字段列表,每个字段都有很多可以设置的属性,如下:
属性分类 | 属性 | 类型 | 描述 |
---|---|---|---|
基本 | bsonType | any | 字段类型,如json object、字符串、数字、bool值、日期、时间戳,具体见下表bsonType可用类型 |
基本 | arrayType | String | 数组项类型,bsonType="array" 时有效,HBuilderX 3.1.0+ 支持,具体见下表arrayType可用类型 |
基本 | title | string | 标题,开发者维护时自用。在schema2code生成前端表单代码时,默认用于表单项前面的label |
基本 | description | string | 描述,开发者维护时自用。在生成前端表单代码时,如果字段未设置componentForEdit,且字段被渲染为input,那么input的placehold将默认为本描述 |
基本 | defaultValue | string|Object | 默认值 |
基本 | forceDefaultValue | string|Object | 强制默认值,不可通过clientDB的代码修改,常用于存放用户id、时间、客户端ip等固定值。具体参考下表的defaultValue |
值域校验 | required | array | 是否必填。支持填写必填的下级字段名称。required可以在表级的描述出现,约定该表有哪些字段必填。也可以在某个字段中出现,如果该字段是一个json对象,可以对这个json中的哪些字段必填进行描述。详见下方示例 |
值域校验 | enum | Array | 字段值枚举范围,数组中至少要有一个元素,且数组内的每一个元素都是唯一的。 |
值域校验 | enumType | String | 字段值枚举类型,可选值tree。设为tree时,代表enum里的数据为树形结构。此时schema2code可生成多级级联选择组件 |
值域校验 | fileMediaType | String | 文件类型,bsonType="file" 时有效,可选值 all|image|video 默认值为all,表示所有文件,image表示图片类型文件,video表示视频类型文件 HBuilderX 3.1.0+ |
值域校验 | fileExtName | String | 文件扩展名过滤,bsonType="file" 时有效,多个文件扩展名用 "," 分割,例如: jpg,png,HBuilderX 3.1.0+ 支持 |
值域校验 | maximum | number | 如果bsonType为数字时,可接受的最大值 |
值域校验 | exclusiveMaximum | boolean | 是否排除 maximum |
值域校验 | minimum | number | 如果bsonType为数字时,可接受的最小值 |
值域校验 | exclusiveMinimum | boolean | 是否排除 minimum |
值域校验 | minLength | number | 限制字符串或数组的最小长度 |
值域校验 | maxLength | number | 限制字符串或数组的最大长度 |
值域校验 | trim | String | 去除空白字符,支持 none|both|start|end,默认none,仅bsonType="string"时有效 |
值域校验 | format | 'url'|'email' | 数据格式,不符合格式的数据无法入库。目前只支持'url'和'email',未来会扩展其他格式 |
值域校验 | pattern | String | 正则表达式,如设置为手机号的正则表达式后,不符合该正则表达式则校验失败,无法入库 |
值域校验 | validateFunction | string | 扩展校验函数名 |
权限校验 | permission | Object | 数据库权限,控制什么角色可以对什么数据进行读/写,可控制表和字段,可设置where条件。见下文详述 |
错误返回 | errorMessage | string|Object | 当数据写入或更新时,校验数据合法性失败后,返回的错误提示 |
关联关系 | foreignKey | String | 关联字段。表示该字段的原始定义指向另一个表的某个字段,值的格式为表名.字段名 ,比如订单表的下单用户uid字段指向uni-id-users表的_id字段,那么值为uni-id-users._id 。关联字段定义后可用于联表查询,通过关联字段合成虚拟联表,极大的简化了联表查询的复杂度 |
关联关系 | parentKey | String | 同一个数据表内父级的字段。详情参考:树状数据查询 |
schema2code | label | string | 字段标题。schema2code生成前端代码时,渲染表单项前面的label标题。如果不填,会使用title属性。适用于title不便显示在表单项前面的情况 |
schema2code | group | string | 分组id。schema2code生成前端代码时,多个字段对应的表单项可以合并显示在一个uni-group组件中 |
schema2code | order | int | 表单项排序序号。schema2code生成前端代码时,默认是以schema中的字段顺序从上到下排布表单项的,但如果指定了order,则按order规定的顺序进行排序。如果表单项被包含在uni-group中,则同组内按order排序 |
schema2code | component | Object|Array | schema2code生成前端代码时,使用什么组件渲染这个表单项。已废弃。请使用下面的componentForEdit和componentForShow |
schema2code | componentForEdit | Object|Array | HBuilderX 3.1.0+, 生成前端编辑页面文件时(add.vue、edit.vue),使用什么组件渲染这个表单项。比如使用input输入框。 |
schema2code | componentForShow | Object|Array | HBuilderX 3.1.0+, 生成前端展示页面时(list.vue、detail.vue),使用什么组件渲染。比如使用uni-dateformat格式化日期。 |
注意:
示例
如果你阅读过数据库入门文档,那么你的服务空间此时应该有表resume
,且里面有一条数据。
我们仍以 resume
表为例,除了_id
外,该表有6个业务字段:name
, birth_year
, tel
, email
, address
, intro
。
业务规则如下:
name
字段是字符串,长度大于等于2小于等于17,必填,需要去除头尾空白字符birth_year
字段是大于等于1950小于2020的数字,必填tel
字段是字符串,但格式是手机号,必填email
字符是字符串,但格式是email,必填address
字段类型为json object,它下面又有2个子字段,city
和street
,其中"city"字段必填intro
字段类型为string,非必填,需要去除头尾空白字符则resume.schema.json
按如下编写。
{
"bsonType": "object",
"required": ["name", "birth_year", "tel", "email"],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"name": {
"bsonType": "string",
"title": "姓名",
"trim": "both",
"minLength": 2,
"maxLength": 17
},
"birth_year": {
"bsonType": "int",
"title": "出生年份",
"minimum": 1950,
"maximum": 2020
},
"tel": {
"bsonType": "string",
"title": "手机号码",
"pattern": "^\\+?[0-9-]{3,20}$",
"trim": "both"
},
"email": {
"bsonType": "string",
"title": "email",
"format": "email",
"trim": "both"
},
"address": {
"bsonType": "object",
"title": "地址",
"required": ["city"],
"properties": {
"city": {
"bsonType": "string",
"title": "城市"
},
"street": {
"bsonType": "string",
"title": "街道",
"trim": "both"
}
}
},
"intro":{
"bsonType": "string",
"title": "简介",
"trim": "both"
}
}
}
注意:
schema保存后,可以通过代码测试。注意在uniCloud web控制台修改数据不受schema限制,只有通过JQL操作数据时schema才生效。
我们在前端测试工程里新加一个按钮“添加数据”
<template>
<view class="content">
<button @click="addresume()">添加数据</button>
</view>
</template>
<script>
const db = uniCloud.database();
export default {
data() {
return {}
},
methods: {
addresume() {
db.collection("resume").add({
"name": "1",
"birth_year": 1949,
"tel": "1",
"email": "1"
}).then((res) => {
// res 为数据库查询结果
console.log(res)
}).catch((err) => {
console.log(err.message)
});
}
}
</script>
可以看到,不符合规则的数据无法通过JQL操作入库。可以依次把各个字段的测试值修正为合法格式测试,直到可以正常入库。
成功后,res会返回新增记录的id,也可以在web控制台看到新增的数据。
失败的提示语也可以通过errorMessage自定义。
成功后,再次点击“添加数据”按钮,会发现重复数据插入。避免这种情况需要设置索引,比如将tel字段设为唯一索引。详见
官方推出了openDB
开源数据库规范,包括用户表、文章表、商品表等很多模板表,这些模板表均已经内置DB Schema
,可学习参考。详见
schema 国际化方案 详见
复杂格式说明:
var timestamp = new Date().getTime();
。它的好处是屏蔽了时区差异。阿里云和腾讯云的云端时区是0,但在HBuilderX本地运行云函数时,如果是中国的电脑,时区则会变成8,导致显示错乱。所以推荐使用时间戳。但时间戳是一串记录毫秒的数字,不合适直接渲染到前端界面上。推荐的做法是在前端渲染时使用<uni-dateformat>
组件。{
"name": "filename.jpg",
"extname": "jpg",
"fileType": "image",
"url": "https://xxxx", // 必填
"size": 0, //单位是字节
"image": { //图片扩展
"width":10,//单位是像素
"height":10
},
"video":{ //video和image不会同时存在。此处仅为列举所有数据规范
"duration":123,//视频时长,单位是秒
"poster":"https://xxx" //视频封面
}
}
在上述格式中,除了url
外,其他均为非必填。
image
键是图片的扩展键,除了基本的宽高像素外,开发者可以自己扩展其他键,比如色位。同理video
也可以自行扩展。
示例
以resume
表为例,新加一个照片字段photo
,设为file类型,定义格式如下(省略了其他老字段):
{
"schema": {
"bsonType": "object",
"required": ["name", "birth_year", "tel", "email"],
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"photo": {
"bsonType": "file",
"title": "照片",
"fileMediaType": "image", // 可选值 all|image|video 默认值为all,表示所有文件,image表示图片类型文件,video表示视频类型文件
"fileExtName": "jpg,png", // 扩展名过滤,多个用 , 分割
}
}
}
}
file的前端配套组件:
uni-ui组件库中包含组件:<uni-file-picker>
。该组件和file字段的数据库完美搭配。
组件首先选择文件,并上传到uniCloud云存储,在表单提交后将上传文件的地址写入file字段中。详见:https://ext.dcloud.net.cn/plugin?id=4079
file和schema2code:
DB Schema定义字段类型为file后,可以通过schema2code工具,直接生成上传表单页面,前端页面包含<uni-file-picker>
组件,选择、上传、写库一气呵成。详见:schema2code
一个字段如果bsonType是array,那么它可以进一步通过arrayType指定这个数组里每个数组项目的bsonType,值域仍然是所有的字段类型。
比如一个字段存储了多张图片,那么可以设置bsonType为array,然后进一步设置arrayType为file。
示例
以resume
表为例,新加一个照片字段photos
,设为file类型,定义格式如下(省略了其他老字段):
{
"schema": {
"bsonType": "object",
"required": [],
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"images": {
"bsonType": "array",
"arrayType": "file",
"title": "照片",
"multiple": true, // 允许选择多张图片,schema2code生效
"fileMediaType": "image", // 可选值 all|image|video 默认值为all,表示所有文件,image表示图片类型文件,video表示视频类型文件
"fileExtName": "jpg,png", // 扩展名过滤,多个用 , 分割
"maxLength": 3 // 限制最大数量
}
}
}
}
<uni-file-picker>
组件和schema2code均可使用。defaultValue和forceDefaultValue都是默认值,即新增一行数据记录时,如果字段内容未提供,则按默认值填充该字段内容。但2者也有区别,如下:
在实际开发中,forceDefaultValue常用于设置为当前服务器时间、当前登录用户id、客户端ip等。 这些数据都不能通过前端上传,不安全。过去只能在云端写云函数操作。在schema配置后则可以不用写云函数。使用JQL新增数据记录时会自动补齐这些数据。
defaultValue/forceDefaultValue
内可以使用固定值,还可以使用预置变量$env
,形式如下:
"forceDefaultValue": {
"$env": "now"
}
预置变量$env
可取值如下:
变量 | 说明 |
---|---|
now | 当前服务器时间戳 |
clientIP | 当前客户端IP |
uid | 当前用户Id,基于uni-id 。如果当前用户未登录或登录状态无效会报错 |
示例:
// 指定默认值为true
"defaultValue": true
// 指定强制默认值为当前服务器时间戳
"forceDefaultValue": {
"$env": "now"
}
// 指定强制默认值为当前客户端IP
"forceDefaultValue": {
"$env": "clientIP"
}
// 指定强制默认值为当前客户id
"forceDefaultValue": {
"$env": "uid"
}
以resume
表为例,新增一个字段create_time
,表示记录的创建时间。
该字段的defaultValue
指定为服务器时间。新增记录时,若前端不传该字段,则默认为当前服务器时间。若前端传一个指定的值,则以传的值为准。
{
"bsonType": "object",
"required": [],
"properties": {
"create_time": {
"bsonType": "timestamp",
"title": "创建时间",
"defaultValue": {
"$env": "now"
}
}
}
}
强制默认值forceDefaultValue
,指定为当前服务器时间戳。此时前端传任何值均无效,新增记录时一定会变成当前云端时间。
{
"bsonType": "object",
"required": [],
"properties": {
"create_time": {
"bsonType": "timestamp",
"title": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}
在实际业务中,记录的创建时间不能由客户端篡改,比如强制为云端时间。所以这个场景下必须使用forceDefaultValue
。
一个复杂的业务系统,有很多张数据表。表与表之间,存在的数据关联。foreignKey用于描述数据关联关系。
比如一个文章系统,至少需要用户表、文章分类表、文章表、评论表。opendb已经包含了这4张表,可以点击链接看这些表的结构:
我们先不展开描述上面这几张表,首先讲解为什么分表、怎么分表。
user_id
配置了"foreignKey": "uni-id-users._id"
因为MongoDB的灵活性,理论上可以在用户表[uni-id-users]中新增一个字段articles,在articles下面通过数组来存放该作者的每一遍文章,然后在该文章中再来一个字段comments,存放该文章的每一条评论。
如下,uni-id-users表的数据内容,假使里面有2个用户,zhangsan和lisi,然后lisi写了1篇文章,这篇文章又被zhangsan评论了1条。
[{
"_id": "60b92a42e22fbe00018c359d",
"username": "zhangsan",
"password": "03caebb36670995fc261a275d212cad65e4bbebd",
"register_date": 1622747714731,
"register_ip": "192.168.0.1",
},
{
"_id": "60b9315801033700011ba9ed",
"username": "lisi",
"password": "03caebb36670995fc261a275d212cad65e4bbebd",
"register_date": 1622747714731,
"register_ip": "192.168.0.2",
"articles":[
{
"title": "文章标题",
"content": "文章内容",
"publish_date": 1617850851000,
"publish_ip": "192.168.0.2",
"comments":[
{
"user_id":"60b92a42e22fbe00018c359d",
"comment_content":"评论内容",
"comment_date":1617850851022,
"comment_ip": "192.168.0.1"
}
]
}
]
}]
可以看出,这个uni-id-users表形成了用户、文章、评论的三层嵌套。
虽然MongoDB可以这么嵌套,但实际业务中不该这样设计。会导致查询性能低下甚至某些查询条件无法实现。
数据库是数字系统的底层,它应该清晰有条理,人、文章、评论以及这3者的关系,都应该清晰且不冗余。
MongoDB的嵌套,更多的适用于不会被单独拎出来查询的、记录条数较少的场景。
比如简历表中的工作经历,就可以嵌套。因为工作经历数量较少、且不会出现单独查工作经历而不查人的情况。
但文章表,是一定需要独立的,因为文章数量会非常多,它会单独搜索;
评论表其实不太有单独搜索的需求,它总是伴随指定文章出现。但因为数量会很多,评论也需要分页查询,嵌套在文章表下不利于分页查询。
所以正确的数据库设计,还是分开这几张表。另外很多文章系统都会有文章分类,比如 社会、教育、娱乐、体育、科技...,所以还需要一个文章分类表。
opendb的这4张表,才是正确的分表设计。
可以看到注册用户都在uni-id-users表中,而文章内容在opendb-news-articles表中。一个用户可能写了很多文章,这些文章不会存入uni-id-users表。
既然有了分表的概念,就存在表与表之间关系的概念了。
比如在文章表中,如何存放文章的作者信息?如何表示这篇文章是哪个用户写的?是存放作者的用户名吗?
实际上,文章表中的作者字段,也就是user_id
字段,存放的是用户表中的这个作者的_id
字段的值。_id
是uniCloud数据库每张表的每个记录都有的唯一字段。
可以看下用户表uni-id-users和文章表opendb-news-articles具体数据,直观感受下:
uni-id-users用户表,还是假使里面有2个作者,zhangsan和lisi
[{
"_id": "60b92a42e22fbe00018c359d",
"username": "zhangsan",
"password": "03caebb36670995fc261a275d212cad65e4bbebd",
"register_date": 1622747714731,
"register_ip": "127.0.0.1",
},
{
"_id": "60b9315801033700011ba9ed",
"username": "lisi",
"password": "03caebb36670995fc261a275d212cad65e4bbebd",
"register_date": 1622747714731,
"register_ip": "127.0.0.1",
}]
opendb-news-articles文章表,里面只有1篇文章,这篇文章是 lisi 写的,所以在字段user_id
中存的就是60b9315801033700011ba9ed
,这就是uni-id-users表中 lisi 对应的 _id
{
"_id": "606e721280b8450001e773c6",
"category_id": "606e6feb653b8400017214a3",
"title": "这里是标题",
"content": "这里是正文",
"user_id": "60b9315801033700011ba9ed",
"publish_date": 1617850851000,
"publish_ip": "119.131.174.251"
}
只要user_id
这个关联关系映射起来,数据就清晰且完整了。
并不需要在文章表opendb-news-articles存放作者的用户名、昵称、头像、注册时间甚至密码,只需要存它的user_id
,就精准、最小冗余的表达数据关系。
当然也有的系统设计为了减少联表查询而在文章表里冗余存放作者昵称和头像,是否使用冗余可以视需求而定,但一定需要用user_id
来做数据表的关联。
上面显示的是2个表的数据内容,但回到 DB Schema 中,如何在schema中表达这种关联关系?那就是foreignKey,外键。
文章表opendb-news-articles的 DB Schema 中的user_id
字段的描述如下:
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "文章作者ID, 参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id",
"defaultValue": {
"$env": "uid"
}
},
"title":{},
"content":{}
}
上面的重点,就在这个foreignKey,它的前半部分是另一张表的表名,中间用.
分割,后半部分是另一张表的字段名。
它代表文章表这个user_id
字段,在关系上实质指向uni-id-users表的_id
字段。也就是文章表的user_id
字段里存的值,其实是源自uni-id-users表的_id
字段里的值。
注意不要搞反,并不是在uni-id-users表的schema的_id
字段里配foreignKey。uni-id-users表的_id
字段是原值,不引用自任何地方。而是在其他引用uid的字段来配。
同样,评论表opendb-news-comments的schema里,
user_id
的foreignKey需指向"uni-id-users._id"
article_id
的foreignKey需指向"opendb-news-articles._id"
配置foreignKey,除了清晰描述数据关系,它最大的作用是联表查询。
JQL没有像SQL那样提供了join、leftjoin、innerjoin这些语法,只需要配置好数据关系,配好foreignKey,查询时就可以自动联表查询。
联表查询内容较多,详见
在传统关系型数据库中,tree是很难表达的,只有oracle这种商业数据库提供了tree查询。其他关系型数据库需要开发者通过复杂的代码实现tree查询。
在MongoDB中,虽然自身天然支持tree,但实际业务中并不会使用MongoDB的json嵌套方式来描述tree。
比如部门tree,部门可以动态的新增、删除、改名、挪动层级。实际上每个部门,在部门表里的数据仍然是一条独立的行数据记录,并不是一条记录里无限嵌套下去。
如部门表,里面有2条数据,一条数据记录是“总部”,另一条数据记录“一级部门A”
{
"_id": "5fe77207974b6900018c6c9c",
"name": "总部",
"child": [
{
"_id": "5fe77232974b6900018c6cb1",
"name": "一级部门A",
"child": [],
"status": 0
}
],
"status": 0
}
除非你的部门就这2个,永远不变。否则就不该使用上面的做法。
parent_id
来描述层级关系。[{
"_id": "5fe77207974b6900018c6c9c",
"name": "总部",
"parent_id": "",
"status": 0
},
{
"_id": "5fe77232974b6900018c6cb1",
"name": "一级部门A",
"parent_id": "5fe77207974b6900018c6c9c",
"status": 0
}]
在"一级部门A"的parent_id
中,值为5fe77207974b6900018c6c9c
,它其实就是"总部"的_id
。
那么在 DB Schema 中如何表达这种关系呢?就要使用parentKey。
部门表opendb-department的schema中,将字段parent_id
的"parentKey"设为"_id"
,即指定了数据之间的父子关系,如下:
{
"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将数据表不同记录的父子关系描述了出来。一个字段A的属性设置了parentKey并指向另一个字段B,那么这个A的值,就一定等于B的值。
使用parentKey描述了字段父子关系后,就可以通过JQL的getTree方便的做tree查询了。因内容较多,详见
DB Schema
提供了一套完善的字段值域描述规则,并且自动进行数据入库校验,不符合规则的数据无法写入数据库。
注意只有要对数据库写入内容时(新增记录或修改记录)才涉及字段值域的校验问题。读与删不涉及。
DB Schema
里的字段值域校验系统由4部分组成:
在schema一级节点的required
中,可以以数组的方式填入多个字段名称。比如以下示例将name字段设为必填
{
"bsonType": "object",
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"errorMessage": "{title}不能为空"
}
}
}
一个字段的required
,和字段的其他规则的关系如下:
required
时,必须传入该字段required
时,如果传入的数据包含该字段则进行其他规则校验,否则忽略该字段的其他校验规则。以下面的代码为例,如果不传name的值可以通过校验;如果传了name则要求name最小长度为2,否则校验失败
{
"bsonType": "object",
"required": [],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"minLength": 2,
"errorMessage": {
"required": "{title}不能为空",
"minLength": "{title}不能小于 {minLength} 个字符"
}
}
}
}
仅对string类型字段生效。
其中format的url格式补充说明如下:
http://
| https://
| ftp://
开头, //
后必须包含一个 .
(localhost除外)
有效格式
无效格式
示例
可以在resume
表中增加一个email字段,使用format来约束其格式。
{
"bsonType": "object",
"required": ["email"],
"properties": {
"email": {
"bsonType": "string",
"title": "邮箱",
"format": "email",
"errorMessage": {
"required": "{title}不能为空",
"format": "{title}格式无效"
}
}
}
}
例如: 验证手机号 "pattern": "^\\+?[0-9-]{3,20}$"
{
"bsonType": "object",
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"pattern": "",
"errorMessage": {
"required": "{title}不能为空",
"pattern": "{title}格式无效"
}
}
}
}
enum,即枚举。一个字段设定了enum后,该字段的合法内容,只能在enum设定的候选数据项中取值。HBuilderX 3.7.13移除校验数据时enum最多只可以枚举500条的限制。
enum支持3种数据格式来描述候选:
比如给resume
表增加一个性别字段,合法值域只能是“0”、“1”、“2”中的一个。
{
"bsonType": "object",
"required": [],
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"gender": {
"bsonType": "array",
"title": "性别",
"description": "用户性别:0 未知 1 男性 2 女性",
"enum": [0,1,2]
}
}
}
字段gender设成这样后,插入或修改的数据如果不是 0 或 1 或 2,就会报错,无法插入或更新数据。
通过schema2code生成前端表单页面时,带有enum的字段会生成 picker 组件。该组件在界面上渲染时会生成“1、2、3”这3个候选的复选框。所以一般不推荐使用简单数组,而是推荐下面的 支持描述的数组。
仍然使用性别字段举例,合法值域只能是“0”、“1”、“2”中的一个。但通过schema2code生成前端表单页面时,该字段会生成uni-data-checkbox组件,该组件在界面上渲染时会生成“未知”、“男”、“女”这3个候选的复选框。
{
"bsonType": "object",
"required": [],
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"gender": {
"bsonType": "int",
"title": "性别",
"defaultValue": 0,
"enum": [
{
"text": "未知",
"value": 0
},
{
"text": "男",
"value": 1
},
{
"text": "女",
"value": 2
}
]
}
}
}
这种带描述的方式,让schema可读性提高,同时也让schema2code生成的前端界面可用性更高。
对于候选比较少的情况,schema2code使用需要弹出一次的picker未必合适。如果想在界面中平铺候选,比如 男、女、未知 直接显示在表单中,此时可以在schema的componentForEdit属性中改用uni-data-checkbox组件来表达性别选择。
一个字段的合法值域,可以是从另一个数据查询而来。也即,在enum中可以配置JQL查询语句。
这种方式需要搭配foreignKey来使用,也就是需要关联另一个表
在opendb中有一个民族表opendb-nation-china,里面存放了中国的56个民族。
我们要在resume
表中加一个民族字段,就应该从这个opendb-nation-china
表取值。
在项目根目录uniCloud/database点右键,新建schema,选择opendb-nation-china
这种opendb表的预置数据,需要上传schema到云端,才会添加到数据库中。所以需要对这个opendb-nation-china.schema.json
点右键,上传 DB Schema
就可以uniCloud web控制台创建,此时会自动入库数据,但需要对项目根目录uniCloud/database点右键->下载 DB Schema,才可以在本地调试时使用。
设置nation
字段的外键"foreignKey": "opendb-nation-china.name"
。民族比较简单,这里我们直接取了民族表的汉字名称为关联key,没有取数据库ID。
然后设置nation
字段的enum如下:
{
"bsonType": "object",
"required": [],
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"nation": {
"bsonType": "string",
"title": "民族",
"foreignKey": "opendb-nation-china.name",
"enum": {
"collection": "opendb-nation-china",
"orderby": "first_letter asc",
"field": "name as value, name as text"
}
}
}
}
这样客户端如果传上来一个不在opendb-nation-china
表里的民族名称,是无法入库的。
通过schema2code生成前端表单页面时,该字段会生成 picker 组件,该组件被点击后,会弹出候选项,这些候选项都是从民族表中查询数据并显示的。
除了普通的二维数据表,enum还支持tree型数据。即enumType为tree。
在opendb中有一个城市表opendb-city-china,里面存放了中国的各个城市。城市是按 省、市、区 分三级的树形数据。
在resume
表中,有一个city字段,其合理的字段规则应该是从opendb-city-china
表取值,
设置enumType
为"tree",代表enum里的数据为树形结构,比如下面的例子,代表opendb-city-china表以getTree方式查询。在schema2code时,可自动生成多级级联选择组件,详见
{
"schema": {
"bsonType": "object",
"required": ["city_id"],
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"city_id": {
"bsonType": "string",
"title": "地址",
"description": "城市编码",
"foreignKey": "opendb-city-china.code",
"enumType": "tree",
"enum": {
"collection": "opendb-city-china",
"orderby": "value asc",
"field": "code as value, name as text"
}
}
}
}
}
是否将字符串两边空格trim掉。仅对string类型字段生效。
值 | 描述 |
---|---|
none | 不处理。默认为none |
both | 从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR 等) |
start | 从字符串的开头移除空白字符 |
end | 从一个字符串的末端移除空白字符 |
trim的优先级,高于字符串的其他验证规则,比如format、pattern、minLength、validateFunction、fileRules。配置trim后,JQL引擎会首先将字符串trim后再交给其他验证系统验证。
配置trim后,schema2code生成的前端页面中的输入框也将自动trim用户的输入内容,然后再提交云端。
示例
以resume
表为例,name字段有minLength为2的限制,假使插入name的值为“a ”,由于a
后面的空格会先被trim掉,长度变成1,导致这个数据无法被写入数据库。
{
"bsonType": "object",
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"minLength": "2",
"trim" : "both"
}
}
}
自HBuilderX 3.1.0
起,支持schema内配置一级节点fieldRules对字段之间的关系进行约束和校验。当然只校验一个字段也可以。
fieldRules的写法等同JQL的where写法(也可以使用各种数据库运算方法),参考:JQL where
fieldRules内配置如下,数组内可以配置多个rule,每个rule都有rule表达式、错误提示语、运行兼容环境这3部分。
{
"fieldRules": [{
"rule": "end_date == null || end_date != null && create_date < end_date", // 校验规则
"errorMessage": "创建时间和结束时间不匹配", // 错误提示信息(仅在新增时生效,更新数据时不会提示此信息)
"client": false // schema2code时,当前规则是否带到前端也进行校验。目前此属性暂不生效,fieldRules不会在客户端校验数据,仅会在云端进行校验
}],
}
rule表达式,是一组js,返回值必须为true或false。返回false则触发提示错误,错误提示显示的是errorMessage的内容。
rule表达式里支持:
new Date()
来获取时间。需要注意的是不同于数据库运算方法,new Date()
内不可传入数据库字段作为参数上述配置中,create_date
、end_date
为字段名称。schema内也支持写字段操作方法,如add方法。
例:在todo表内可以使用fieldRules限制end_date
大于create_date
{
"bsonType": "object",
"required": ["title","create_date"],
"fieldRules": [{
"rule": "end_date == null || end_date != null && create_date < end_date",
"errorMessage": "结束时间需大于创建时间"
}],
"properties": {
"title": {
"bsonType": "string",
"title": "标题"
},
"create_date": {
"bsonType": "timestamp",
"title": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
},
"end_date": {
"bsonType": "timestamp",
"title": "结束时间"
}
}
}
上述示例中,create_date
为必填项,只需限制end_date
存在时大于create_date
即可
注意
end_date
字段或者create_date
字段均会触发校验扩展校验函数
当属性配置不满足需求,需要写js函数进行校验时,使用本功能。(当然也可以使用schema.ext.js来替代)
注意
如何使用
HBuilderX 3.0.0
及以上版本,可以在项目下创建扩展校验云函数并上传,使用方法如下:uniCloud
目录点右键,选择创建database目录
(如果已有该目录则忽略本步骤)创建数据库扩展校验函数目录
validateFunction
目录右键选择新建数据库扩展校验函数
对validateFunction
目录右键,还可以上传和下载validateFunction
,和uniCloud web控制台进行同步。
扩展校验函数示例如下
// 扩展校验函数示例
module.exports = function (rule, value, data, callback) {
// rule 当前规则
// value 当前规则校验数据
// data 全部校验数据
// callback 可选,一般用于自定义 errorMessage,如果执行了callback return 值无效,callback 传入的 message 将替换 errorMessage
// callback('message') 传入错误消息时校验不通过
// callback() 无参时通过
// 注意 callback 不支持异步调用,异步请使用 Promise/await/async
return value.length < 10
}
// 异步校验 Promise
module.exports = function (rule, value, data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (value > 10) {
// 校验通过
resolve()
} else {
// 校验失败
resolve('error') // 等于 reject(new Error('error'))
// reject(new Error('error'))
}
}, 3000);
})
}
// 异步校验 await/async
module.exports = async function (rule, value, data) {
let result = await uni.request({...})
if (result > 10) {
// 校验通过
return true
} else {
// 校验失败
return 'error message'
}
}
在HBuilderX中编写好validateFunction
后,按Ctrl+u可以快捷上传validateFunction
到uniCloud云端。
validateFunction
编写扩展校验函数
后,在表结构 schema 中确定要配置的字段,在该字段的validateFunction
属性上,配置上面编写的扩展校验函数
的名称。
如下例中,当name字段的内容要入库前,就会触发执行 "checkabc" 这个 扩展校验函数
。如果"checkabc"校验没有返回true,则该次数据库操作会失败。
validateFunction
类型为字符串时,云端和客户端同时生效
{
"bsonType": "object",
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"validateFunction": "checkabc",
"errorMessage": {
"required": "{title}不能为空"
}
}
}
}
validateFunction
类型为对象时,可配置客户端同不生效,云端仍然生效
HBuilder 3.1.0+ 支持
{
"bsonType": "object",
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"validateFunction": {
"name": "checkabc", // 扩展校验函数名
"client": false //如果不配置默认是 true
},
"errorMessage": {
"required": "{title}不能为空"
}
}
}
}
提示:如果配置了 "client": false
客户端也可以在生成的代码中改为自己的校验函数,此时客户端的校验仍然生效,客户端对应的校验文件目录为 js_sdk/validator/collection
, collection
为表名,非固定值
扩展校验函数
是服务空间级的,一个扩展校验函数
可以被这个服务空间下的任意表中的任意字段引用。
扩展校验函数
里的代码是可以联网的。一个常见场景是内容的敏感词过滤,可以将内容提交到三方校验服务里,如果校验通过再入库。
不建议在扩展校验函数
里编写大量的代码,会影响性能。
扩展校验函数 的运行环境注意事项
扩展校验函数
的默认运行环境与普通云函数的环境相同,可以调用云函数里可用的各种API。
* 如果要连接外网,要调用uniCloud.httpclient;
* 如果要调用数据库,需使用云函数里操作数据库的方式,即不支持JQL,详见
但是,在schema2code中,扩展校验函数
也会被生成到前端页面的校验规则里。
也就是说,如果使用schema2code生成前端页面,那么写扩展校验函数
需要多一层注意。
比如调用了uniCloud.httpclient这样在前端并不存在的API时,前端的表单校验会出错。
此时就需要在扩展校验函数
中多写一个if判断,避免undefined的问题。
if (uniCloud.httpclient) {
console.log("此处运行在云函数环境里。前端没有这个API");
}
// 或者另一种写法
if (uni) {
console.log("此处运行在前端环境里。云函数没有uni对象,除非你在validateFunction里自己定义了这个对象");
}
schema.ext.js是schema.json的扩展和补充,它可以以编程的方式对数据的增删改查进行监听,然后执行任意操作。所以同样可以用于字段的值域校验。
schema.ext.js与validator function的区别是,validator function是针对某一个字段的控制,返回布尔值。而schema.ext.js是对整个表的自由编程。
schema.ext.js篇幅较长,另见schema.ext.js
数据不符合schema配置的规范时,无法入库,此时会报错。
uniCloud有一些基本错误提示语的格式化,如需自定义错误提示语,就需要使用本功能,根据errorMessage的定义报出错误提示。
errorMessage支持字符串,也支持json object。类型为object时,可定义多个校验提示。
{} 为占位符,可在其中引用已有属性,如title、label等。
属性 | 类型 | 描述 |
---|---|---|
minLength | string | 消息 |
maxLength | string | 消息 |
... | ... | ... |
示例
{
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"title": "姓名",
"minLength": 2,
"maxLength": 8,
"errorMessage": {
"required": "{title}必填",
"minLength": "{title}不能小于{minLength}个字符",
"maxLength": "{title}不能大于{maxLength}个字符"
},
...
},
"age": {
"bsonType": "int",
"title": "年龄",
"minimum": 1,
"maximum": 150,
"errorMessage": "{title}应该大于 {minimum} 岁,小于 {maximum} 岁"
}
}
}
从示例可以看出,errorMessage支持配一条,也支持根据不同的错误类型配不同的errorMessage。
其他注意事项
“数据库中某字段值不能在多条记录中重复”,这个需求一般不是在字段值域校验里实现,而是在数据库索引里配置该字段为唯一索引。详见
可以在web控制台配置索引,db_init.json也可以创建索引。注意如果数据库中多条记录中该字段已经有重复内容,那么设该字段为唯一索引时会报错,需先把重复数据去掉。
DB Schema
的数据权限系统,是为JQL
设计的,尤其clientDB
强依赖这套权限系统。因为客户端是无法信任的,没有缜密的权限系统,会导致客户端任意改动云数据库内容。
在过去,开发者需要在后端写代码来处理权限控制,但实际上有了DB Schema
和uni-id
后,这种权限控制的后台代码就不用再写了。
只要配好DB Schema
的权限,放开让前端写业务即可。配置里声明不能读写的数据,前端就无法读写。
DB Schema
的permission规则,分为两部分,一边是对操作数据的指定,一边是对角色的指定,规则中对两者进行关联,匹配则校验通过。
增删改查或计数
控制读写
控制删改查
控制uni-id
定义的其他角色
uni-id
中自定义各种角色,比如部门管理员,然后在DB Schema
的permission中配置其可操作的数据。详见uni-id的角色权限注意:如果登录用户是uni-id
的admin角色,即超级管理员,则不受DB Schema
的配置限制的,admin角色拥有对所有数据的读写权限。
例如在uniCloud admin
等管理端系统,只要使用admin用户登录就可以在前端操作数据库。
在更新云端DB Schema
时,如果发现服务空间下没有uni-id
公共模块,会自动安装uni-id
。如果服务空间已经存在uni-id
,则不会再自动安装。此时需要注意及时升级uni-id
,避免太老的uni-id
有兼容问题。(从HBuilderX 3.5起,改为uni-id-common
公共模块)
表级控制,包括增删改查四种权限,分别称为:create、delete、update、read。(注意这里使用的是行业通用的crud命名,与操作数据库的方法add()、remove()、update()、get()在命名上有差异,但表意是相同的)
HBuilderX 3.1.0起还新增了count权限,即是否有权对该表进行统计计数。
所有的操作的默认值均为false。也就是不配置permission代表不能操作数据库(角色为admin用户例外)。
例如一个user表,里面有_id、name、pwd等字段,该表的DB Schema
如下,代表前端用户可读(包括游客),但前端非admin用户不可新增、删除、更新数据记录。
// user表的schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true, // 任何用户都可以读
"create": false, // 禁止新增数据记录(admin权限用户不受限)
"update": false, // 禁止更新数据(admin权限用户不受限)
"delete": false, // 禁止删除数据(admin权限用户不受限)
"count": false // 禁止查询数据条数(admin权限用户不受限),新增于HBuilderX 3.1.0
},
"properties": {
"_id":{},
"name":{},
"age": {},
"pwd": {}
}
}
关于count权限的说明
permission的字段级控制,包括读写两种权限,分别称为:read、write。
也就是对于一个指定的字段,可以控制什么样的角色可以读取该字段内容,什么样的角色可以修改写入字段内容。
以上述的user表为例,假如要限制前端禁止读取age字段,那么按如下配置,在字段age下面再写permission节点,设定read为false。
// user表的schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true, // 任何用户都可以读
"create": false, // 禁止新增数据记录(admin权限用户不受限)
"update": false, // 禁止更新数据(admin权限用户不受限)
"delete": false // 禁止删除数据(admin权限用户不受限)
},
"properties": {
"_id":{
},
"name":{
},
"age": {
"bsonType": "number",
"title": "年龄",
"permission": {
"read": false, // 禁止读取 age 字段的数据(admin权限用户不受限)
"write": false // 禁止写入 age 字段的数据(admin权限用户不受限)
}
}
}
}
按上述配置,前端查询数据时,如果不包含age字段,仍然可以查询。但如果查询请求包含age字段,该请求会被拒绝,提示无权访问。
子级会继承父级的权限,即需要同时满足父级权限以及本节点权限,方可操作此节点。例如上述schema中如果配置表级read权限为false,在为name设置read权限为true的情况下,name字段仍不可读
如果字段的bsonType配置为password,则clientDB完全不可操作此字段(即使是admin用户也不可以在客户端读写)。
// user表的schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true, // 任何用户都可以读
"create": false, // 禁止新增数据记录(admin权限用户不受限)
"update": false, // 禁止更新数据(admin权限用户不受限)
"delete": false // 禁止删除数据(admin权限用户不受限)
},
"properties": {
"_id":{
},
"name":{
},
"pwd": {
"bsonType": "password", // 即使不配置权限,此字段也无法在客户端读写
"title": "密码"
}
}
}
DB Schema
提供了一个内置变量doc,表示要意图操作的数据记录。并支持用各种表达式来描述指定的记录。
仍然以user表举例,假使该表有个字段叫status
表示用户是否被禁用。status
是bool值,true代表用户状态正常,false代表被禁用。
然后有个需求,JQL只能查用户状态正常的用户信息,禁用用户信息无法查。那么schema配置如下,表级控制的read节点的值不再是简单的true/false,而是变成一个表达式:"doc.status==true"
// user表的schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": "doc.status==true", // 任何用户都可以读status字段的值为true的记录,其他记录不可读
"create": false, // 禁止新增数据记录(admin权限用户不受限)
"update": false, // 禁止更新数据(admin权限用户不受限)
"delete": false // 禁止删除数据(admin权限用户不受限)
},
"properties": {
"_id":{
},
"name":{
},
"pwd": {
"bsonType": "string",
"title": "密码",
"permission": {
"read": false, // 禁止读取 pwd 字段的数据(admin权限用户不受限)
"write": false // 禁止写入 pwd 字段的数据(admin权限用户不受限)
}
},
"status": {
"bsonType": "bool",
"title": "用户状态",
"description": "true代表用户正常。false代表用户被禁用"
}
}
}
根据这个配置,如JQL查询user表的所有数据,则会报权限校验失败;如JQL查询里在where条件里声明了只查询status字段为true的数据,则查询会放行。
除了上述例子提到的doc变量,事实上DB Schema
的权限规则支持很多变量和运算符,可以满足各种配置需求。
权限规则内可用的全局变量
变量名 | 说明 |
---|---|
auth.uid | 用户id |
auth.role | 用户角色数组,参考uni-id 角色权限,注意admin 为内置的角色,如果用户角色列表里包含admin 则认为此用户有完全数据访问权限 |
auth.permission | 用户权限数组,参考uni-id 角色权限 |
doc | 数据库中的目标数据记录,用于匹配记录内容/查询条件 |
now | 当前服务器时间戳(单位:毫秒),时间戳可以进行额外运算,如doc.publish_date > now - 60000表示publish_date在最近一分钟 |
action | 已废弃,使用数据库触发器替代action云函数 |
注意
auth
表示正在执行操作的用户对象auth.xxx
均由uni-id提供,依赖于uni-id公共模块doc.xxx
表示将要查询/修改/删除的每条数据(注意并不包括新增数据,新增数据应通过值域校验进行验证),如果将要访问的数据不满足permission规则将会拒绝执行uni-id
的角色和权限,也即auth.role和auth.permission是不一样的概念。注意阅读uni-id 角色权限权限规则内可以使用的运算符
运算符 | 说明 | 示例 | 示例解释(集合查询) |
---|---|---|---|
== | 等于 | auth.uid == 'abc' | 用户id为abc |
!= | 不等于 | auth.uid != null | 用户要处于登录状态 |
> | 大于 | doc.age>10 | 目标数据的 age 属性大于 10 |
>= | 大于等于 | doc.age>=10 | 目标数据的 age 属性大于等于 10 |
< | 小于 | doc.age<10 | 目标数据的 age 属性小于 10 |
<= | 小于等于 | doc.age<=10 | 目标数据的 age 属性小于等于 10 |
in | 存在在数组中 | doc.status in ['a','b'] | 目标数据的 status 是['a','b']中的一个,数组中所有元素类型需一致 |
! | 非 | !(doc.status in ['a','b']) | 目标数据的 status 不是['a','b']中的任何一个,数组中所有元素类型需一致 |
&& | 与 | auth.uid == 'abc' && doc.age>10 | 用户id 为 abc 并且目标数据的 age 属性大于 10 |
|| | 或 | auth.uid == 'abc'||doc.age>10 | 用户Id为abc或者目标数据的 age 属性大于 10 |
我们继续使用user表举例,目前需求如下,前端用户如果登录,那么该用户可以修改自己的name字段。此时需要在schema中配置name字段的permission为"write":"(doc._id == auth.uid)"
// user表的schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": "doc.status==true", // 任何用户都可以读status字段的值为true的记录,其他记录不可读
"create": false, // 禁止新增数据记录(admin权限用户不受限)
"update": "'updateuser' in auth.permission", // 权限标记为updateuser的用户,和admin管理员,可以更新数据,其他人无权更新数据
"delete": false // 禁止删除数据(admin权限用户不受限)
},
"properties": {
"_id":{
},
"name":{
"bsonType": "string",
"title": "名称",
"permission": {
"read": true,
"write": "doc._id == auth.uid" // 允许登录的用户修改自己的name字段
}
},
"pwd": {
"bsonType": "string",
"title": "密码",
"permission": {
"read": false, // 禁止读取 pwd 字段的数据(admin权限用户不受限)
"write": false // 禁止写入 pwd 字段的数据(admin权限用户不受限)
}
},
"status": {
"bsonType": "bool",
"title": "用户状态",
"description": "true代表用户正常。false代表用户被禁用"
}
}
}
根据这个配置,如前端应用已经登录,且登录的用户发起修改自己的name的请求,则允许修改。其他修改数据请求则会被拒绝。
注意
要分清 数据权限permission
和 字段值域校验validator
的区别。
在权限规则的变量中只有数据库中的数据doc,并没有前端提交的待入库数据data。所以如果要对待入库的数据data做校验,应该在字段值域validator中校验,而不是在权限permission中校验。
如果想获取和判断目标数据记录doc之外的其他数据,则需要使用get方法,见下一章节。
forceDefaultValue属于数据校验的范畴,在数据写入时生效,但是如果配置forceDefaultValue为{"$env": "uid"}
也会进行用户身份的校验,未登录用户不可插入数据。
例如在news表新增一条记录,权限需求是“未登录用户不能创建新闻”,其实不需要在news表的create权限里写auth.uid != null
。只需把news表的uid字段的forceDefaultValue设为"$env": "uid"
,create权限配置为true即可,未登录用户自然无法创建。当然实际使用时你可能需要更复杂的权限,直接使用true作为权限规则时务必注意
permission和role的使用注意
在schema中使用uni-id的permission和role,首先需要在uniCloud admin中创建好权限,然后创建角色并给该角色分配权限,最后创建用户并授权角色。
这样用户登录后,uniCloud会自动分析它的permission和role,在schema里编写的关于permission和role的限制也可以一一对应上,进行有效管理。
admin中创建权限、角色和用户授权,另见文档
权限规则内置了doc变量,但只能用于要操作的数据表的判断,如果要获取其他表的数据做判断就需要get方法了。
权限规则内通过get方法,根据id获取数据库中的数据。get方法接收一个字符串作为参数,字符串形式为database.表名.记录ID
例如有个论坛,要求用户积分大于100分才可以发帖。那么帖子表的create权限应该配成:
// 使用模板字符串语法拼接产生`database.表名.记录ID`形式字符串
"create": get(`database.uni-id-users.${auth.uid}`).score > 100"
使用get方法时需要注意get方法的参数必须是唯一确定值,例如schema配置的get权限如下:
// 这句的含义是,本次查询where条件内传入的shop_id需要满足以下条件:shop表内_id为此shop_id的记录的owner字段等于当前用户uid
"get(`database.shop.${doc.shop_id}`).owner == auth.uid"
前端js如下:
// 此条件内doc.shop_id只能是'123123',可以通过get(`database.shop.${doc.shop_id}`)获取shop表内_id为123123的记录验证其owner是否等于当前用户uid
db.collection('street').where("shop_id=='123123'").get()
// 此条件内doc.shop_id可能是'123123'也可能是'456456',`"get(`database.shop.${doc.shop_id}`).owner == auth.uid"`会直接返回false不会获取shop表数据进行验证
db.collection('street').where("shop_id=='123123 || shop_id=='456456'").get()
非jql不会走权限校验,jql报了权限校验未通过从以下几点进行检查
schema.json是一个json方式的配置,配置的特点是简单易用,但无法编程。
当出现配置难以满足的需求,比如复杂的数据权限校验规则、复杂的字段值域校验规则,此时应当使用编程的方式来解决。
这就是 scheme.js。每个表都有一个schema.json和一个schema.ext.js(可选)。
在schema.ext.js里可以监听数据的增删改查,可自由做前置校验、前置数据加工或后置加工,可引用扩展库和公共模块。
因篇幅较多,请另见数据库schema.ext.js触发器
再次强调,schema.json和schema.ext.js的生效前提,均是JQL。使用传统MongoDB写法无法执行这些。
DB Schema
里有大量的信息,其实有了这些信息,前端将无需自己开发表单维护界面,uniCloud可以自动生成新增、修改、列表、详情的前端页面,以及admin端的列表、新增、修改、删除全套功能。
因内容较长,请另见文档schema2code