schema.md 72.7 KB
Newer Older
1
## DB Schema概述
Q
qiang 已提交
2

雪洛's avatar
雪洛 已提交
3
`DB Schema`是基于 JSON 格式定义的数据结构的规范。除schema外jql还支持使用js编写schema扩展,详见:[DB schema 扩展](jql-schema-ext.md)
Q
qiang 已提交
4 5 6 7 8 9 10 11

它有很多重要的作用:

- 描述现有的数据含义。可以一目了然的阅读每个表、每个字段的用途。
- 设定数据操作权限(permission)。什么样的角色可以读/写哪些数据,都在这里配置。
- 设定字段值域能接受的格式(validator),比如不能为空、需符合指定的正则格式。
- 设定字段之间的约束关系(fieldRules),比如字段结束时间需要晚于字段开始时间。
- 设置数据的默认值(defaultValue/forceDefaultValue),比如服务器当前时间、当前用户id等。
雪洛's avatar
雪洛 已提交
12
- 设定多个表的字段间映射关系(foreignKey),将多个表按一个虚拟联表直接查询,大幅简化联表查询。
Q
qiang 已提交
13 14 15 16
- 根据schema自动生成前端界面(schema2code),包括列表、详情、新建和编辑页面,自动处理校验规则。

> MongoDB支持通过 [$jsonSchema 操作符](https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/index.html)在插入和更新文档时进行结构验证(非空、类型校验等), $jsonSchema 支持 JSON Schema的草案4,包括[core specification](https://tools.ietf.org/html/draft-zyp-json-schema-04)和[validation specification](https://tools.ietf.org/html/draft-fge-json-schema-validation-00)。uniCloud在MongoDB基础上进行了JSON Schema扩展。

W
wanganxp 已提交
17
编写`DB Schema`是uniCloud的数据库开发的重要环节。但必须通过JQL操作数据库才能发挥`DB Schema`的价值。
18

W
wanganxp 已提交
19
**所以注意,在云函数中使用传统MongoDB API操作数据库时`DB Schema`不生效。不管在客户端还是云端,都必须使用JQL操作数据库。**
Q
qiang 已提交
20

W
wanganxp 已提交
21
- 如果你的应用可以通过clientDB完成,那么这样将无需编写服务器代码,整体开发效率会极大提升。客户端操作数据库时必须完全编写`DB Schema`,尤其权限部分。
Q
qiang 已提交
22

W
wanganxp 已提交
23 24 25 26 27 28 29
- 如果应用的权限系统比较复杂,使用clientDB不如使用云对象方便,也应该编写好除了权限部分以外的其他的schema。这样联表查询、tree查询、默认值、值域校验等其他功能仍然可以方便使用。

	具体来说,如自己在云函数中编写权限控制代码,则需要把`DB Schema`的权限都设为false,在云函数中将操作角色设为admin(通过setuser API),以跳过schema的权限验证。
	
	当然,云函数中代码控制的权限和`DB Schema`中的权限也可以混合使用,简单权限交由`DB Schema`处理,负责权限再编写代码处理。

所以建议开发者编写好schema,无论云端还是前端操作数据库。最多是云函数处理权限时忽略schema中的权限部分。
Q
qiang 已提交
30

31
### 如何编写DB Schema
Q
qiang 已提交
32 33 34 35 36

- **方式1,在web控制台编写schema**

1. 登录 [uniCloud控制台](https://unicloud.dcloud.net.cn),选中一个数据表
2. 点击表右侧页签 “表结构”,点击 “编辑” 按钮,在编辑区域编写 Schema,编写完毕后点保存按钮即可生效。
D
DCloud_LXH 已提交
37
  ![](https://web-assets.dcloud.net.cn/unidoc/zh/schema2code.png)
Q
qiang 已提交
38

39
**web控制台上编辑`DB Schema`保存后是实时在现网生效的,请注意对现网商用项目的影响。**
Q
qiang 已提交
40

W
wanganxp 已提交
41
- **方式2,在HBuilderX中编写schema(推荐)**
Q
qiang 已提交
42

43
在HBuilderX中编写schema,有良好的语法提示和语法校验,还可以本地调试,是更为推荐的schema编写方案。
Q
qiang 已提交
44 45 46

**创建schema**

47 48
1.`uniCloud`项目右键,选择`创建database目录`(如已有目录则忽略)
2. 在 database 目录右键选择`新建数据集合schema`
Q
qiang 已提交
49

study夏羽's avatar
study夏羽 已提交
50
<div align=center>
D
DCloud_LXH 已提交
51
  <img style="max-width:750px;" src="https://web-assets.dcloud.net.cn/unidoc/zh/hx%E6%8F%90%E7%A4%BAschema.jpg"/>
study夏羽's avatar
study夏羽 已提交
52
</div>
Q
qiang 已提交
53 54 55 56 57 58 59 60 61 62 63 64

**HBuilderX内创建的schema新建和保存时不会自动上传**

**上传schema**

- 在单个schema文件右键可以只上传当前选中的schema。快捷键是【Ctrl+u】。(Ctrl+u是HBuilderX的通用快捷键,不管是发布App还是上传云函数、schema,都是Ctrl+u)
- 在database目录右键可以上传全部schema

**下载schema**

- database目录右键可以下载所有schema及扩展校验函数

65
HBuilderX中运行前端项目,在控制台选择连接本地云函数,或者本地云函数/云对象直接运行,此时本地编写的schema可直接生效,无需上传。方便编写调试。
Q
qiang 已提交
66

67
## Schema的一级节点@schema-root
Q
qiang 已提交
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
```json
{
	"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":[
		// 字段之间的约束关系。比如字段开始时间小于字段结束时间。也可以只校验一个字段。支持表达式
	]
}
```

**注意**

- 对数据进行数量统计时(包括count方法、及groupField内的count操作)均会同时触发表级的count权限及read权限

96 97 98
## 字段的属性@segment

### 属性列表
Q
qiang 已提交
99 100 101

properties里的字段列表,每个字段都有很多可以设置的属性,如下:

102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
|属性分类|属性|类型|描述|
|:-|:-|:-|:-|
|基本|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&#124;Object|默认值|
|基本|forceDefaultValue|string&#124;Object|强制默认值,不可通过clientDB的代码修改,常用于存放用户id、时间、客户端ip等固定值。具体参考下表的defaultValue|
|值域校验|required|array|是否必填。支持填写必填的下级字段名称。required可以在表级的描述出现,约定该表有哪些字段必填。也可以在某个字段中出现,如果该字段是一个json对象,可以对这个json中的哪些字段必填进行描述。详见下方示例|
|值域校验|enum|Array|字段值枚举范围,数组中至少要有一个元素,且数组内的每一个元素都是唯一的。**enum最多只可以枚举500条**|
|值域校验|enumType|String|字段值枚举类型,可选值tree。设为tree时,代表enum里的数据为树形结构。此时schema2code可生成多级级联选择组件|
|值域校验|fileMediaType|String|文件类型,bsonType="file" 时有效,可选值 all&#124;image&#124;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&#124;both&#124;start&#124;end,默认none,仅bsonType="string"时有效|
|值域校验|format|'url'&#124;'email'|数据格式,不符合格式的数据无法入库。目前只支持'url'和'email',未来会扩展其他格式|
|值域校验|pattern|String|正则表达式,如设置为手机号的正则表达式后,不符合该正则表达式则校验失败,无法入库|
|值域校验|validateFunction|string|扩展校验函数名|
雪洛's avatar
雪洛 已提交
125
|权限校验|permission|Object|数据库权限,控制什么角色可以对什么数据进行读/写,可控制表和字段,可设置where条件。见下文[详述](#permission)|
126
|错误返回|errorMessage|string&#124;Object |当数据写入或更新时,校验数据合法性失败后,返回的错误提示|
雪洛's avatar
雪洛 已提交
127 128
|关联关系|foreignKey|String|关联字段。表示该字段的原始定义指向另一个表的某个字段,值的格式为`表名.字段名`,比如订单表的下单用户uid字段指向uni-id-users表的_id字段,那么值为`uni-id-users._id`。关联字段定义后可用于[联表查询](jql.md#lookup),通过关联字段合成虚拟联表,极大的简化了联表查询的复杂度|
|关联关系|parentKey|String|同一个数据表内父级的字段。详情参考:[树状数据查询](jql.md#gettree)|
129 130 131 132 133 134
|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&#124;Array|schema2code生成前端代码时,使用什么组件渲染这个表单项。已废弃。请使用下面的componentForEdit和componentForShow|
|schema2code|componentForEdit|Object&#124;Array|HBuilderX 3.1.0+, 生成前端编辑页面文件时(add.vue、edit.vue),使用什么组件渲染这个表单项。比如使用input输入框。|
|schema2code|componentForShow|Object&#124;Array|HBuilderX 3.1.0+, 生成前端展示页面时(list.vue、detail.vue),使用什么组件渲染。比如使用uni-dateformat格式化日期。|
Q
qiang 已提交
135 136

**注意:**
137 138 139 140 141 142
1. schema2code,是根据scheme自动生成数据的增删改查页面的功能。入口1在uniCloud web控制台的数据库schema界面;入口2在HBuilderX中点击schema右键菜单。[详见](https://ext.dcloud.net.cn/plugin?id=4684)
2. 暂不支持子属性校验

**示例**

如果你阅读过[数据库入门文档](hellodb.md),那么你的服务空间此时应该有表`resume`,且里面有一条数据。
Q
qiang 已提交
143

144
我们仍以 `resume` 表为例,除了`_id`外,该表有6个业务字段:`name`, `birth_year`, `tel`, `email`, `address`, `intro`
Q
qiang 已提交
145

146 147 148 149 150 151 152
业务规则如下:
- `name`字段是字符串,长度大于等于2小于等于17,必填,需要去除头尾空白字符
- `birth_year`字段是大于等于1950小于2020的数字,必填
- `tel`字段是字符串,但格式是手机号,必填
- `email`字符是字符串,但格式是email,必填
- `address`字段类型为json object,它下面又有2个子字段,`city``street`,其中"city"字段必填
- `intro`字段类型为string,非必填,需要去除头尾空白字符
Q
qiang 已提交
153

154
`resume.schema.json`按如下编写。
Q
qiang 已提交
155 156 157

```json
{
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
	"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"
		}
	}
Q
qiang 已提交
217
}
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257

```

注意:
- 把permission改成所有人可操作是为了测试方便,真实业务时需按照业务需求调整,需学习uni-id后方可掌握
- 真实业务不推荐address下设city和street,拉平层级更好。另外city应该通过enum关联另一个城市表,在候选城市列表中选择,后续在enum文档中举例

schema保存后,可以通过代码测试。注意在uniCloud web控制台修改数据不受schema限制,只有通过JQL操作数据时schema才生效。

我们在前端测试工程里新加一个按钮“添加数据”

```html
<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>
Q
qiang 已提交
258 259
```

260 261 262 263 264 265 266
可以看到,不符合规则的数据无法通过JQL操作入库。可以依次把各个字段的测试值修正为合法格式测试,直到可以正常入库。

成功后,res会返回新增记录的id,也可以在web控制台看到新增的数据。

失败的提示语也可以通过errorMessage自定义。

成功后,再次点击“添加数据”按钮,会发现重复数据插入。避免这种情况需要设置索引,比如将tel字段设为唯一索引。[详见](db-index.md)
Q
qiang 已提交
267

268
官方推出了`openDB`开源数据库规范,包括用户表、文章表、商品表等很多模板表,这些模板表均已经内置`DB Schema`,可学习参考。[详见](opendb.md)
Q
qiang 已提交
269

270
schema 国际化方案 [详见](/tutorial/i18n?id=schema)
d-u-a's avatar
d-u-a 已提交
271

Q
qiang 已提交
272

273
### 字段类型bsonType@bsontype
Q
qiang 已提交
274

275 276 277 278 279 280 281 282 283 284
- bool:布尔值,true|false
- string:字符串
- password:一种特殊的string。这类字段不会通过clientDB传递给前端,所有用户都不能通过clientDB读写,即使是admin管理员。[uni-id-user](https://gitee.com/dcloud/opendb/blob/master/collection/uni-id-users/collection.json)表有示例
- int:整数
- double:精度数。由于浮点精度问题,慎用
- object:json对象。地理位置也属于object
- file:一种特殊的object,固定格式存放云存储文件的信息。不直接存储文件,而是一个json object,包括云存储文件的名称、路径、文件体积等信息。(HBuilderX 3.1.0+ )
- array:数组
- timestamp:时间戳
- date:日期
Q
qiang 已提交
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312

复杂格式说明:
- timestamp是一串数字的时间戳,一般通过如下js获取`var timestamp = new Date().getTime();`。它的好处是屏蔽了时区差异。阿里云和腾讯云的云端时区是0,但在HBuilderX本地运行云函数时,如果是中国的电脑,时区则会变成8,导致显示错乱。所以推荐使用时间戳。但时间戳是一串记录毫秒的数字,不合适直接渲染到前端界面上。推荐的做法是在前端渲染时使用[`<uni-dateformat>`组件](https://ext.dcloud.net.cn/plugin?id=3279)
- 日期和地理位置在web控制台的数据库管理界面上无法直接在引号里录入值,需参考[文档](uniCloud/quickstart?id=editdb)
- double类型慎重,由于js不能精准处理浮点运算,0.1+0.2=0.30000000000000004。所以涉及金额时,建议使用int而不是double,以分为单位而不是以元为单位存储。比如微信支付默认就是以分为单位。如果使用[uniPay](uniCloud/unipay)处理支付的话,它的默认单位也是分。
- file的json object格式存储文件的基本信息和路径,如下:
```json
{
	"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`也可以自行扩展。

313 314 315
**示例**

`resume`表为例,新加一个照片字段`photo`,设为file类型,定义格式如下(省略了其他老字段):
Q
qiang 已提交
316 317 318 319 320

```json
{
  "schema": {
    "bsonType": "object",
321
    "required": ["name", "birth_year", "tel", "email"],
Q
qiang 已提交
322 323 324 325
    "properties": {
      "_id": {
        "description": "ID,系统自动生成"
      },
326
      "photo": {
Q
qiang 已提交
327
        "bsonType": "file",
328
        "title": "照片",
Q
qiang 已提交
329 330 331 332 333 334 335 336
        "fileMediaType": "image", // 可选值 all|image|video 默认值为all,表示所有文件,image表示图片类型文件,video表示视频类型文件
        "fileExtName": "jpg,png", // 扩展名过滤,多个用 , 分割
      }
    }
  }
}
```

337
**file的前端配套组件:**
Q
qiang 已提交
338

339
uni-ui组件库中包含组件:`<uni-file-picker>` 。该组件和file字段的数据库完美搭配。
Q
qiang 已提交
340

341
组件首先选择文件,并上传到uniCloud云存储,在表单提交后将上传文件的地址写入file字段中。详见:[https://ext.dcloud.net.cn/plugin?id=4079](https://ext.dcloud.net.cn/plugin?id=4079)
Q
qiang 已提交
342

343
**file和schema2code:**
Q
qiang 已提交
344

345 346 347
DB Schema定义字段类型为file后,可以通过schema2code工具,直接生成上传表单页面,前端页面包含`<uni-file-picker>`组件,选择、上传、写库一气呵成。详见:[schema2code](schema2code.md)

### 数组字段类型的子类型arrayType@arraytype
Q
qiang 已提交
348 349 350 351 352

一个字段如果bsonType是array,那么它可以进一步通过arrayType指定这个数组里每个数组项目的bsonType,值域仍然是所有的字段类型。

比如一个字段存储了多张图片,那么可以设置bsonType为array,然后进一步设置arrayType为file。

353 354 355
**示例**

`resume`表为例,新加一个照片字段`photos`,设为file类型,定义格式如下(省略了其他老字段):
Q
qiang 已提交
356 357 358 359 360 361 362 363 364 365 366 367 368

```json
{
  "schema": {
    "bsonType": "object",
    "required": [],
    "properties": {
      "_id": {
        "description": "ID,系统自动生成"
      },
      "images": {
        "bsonType": "array",
        "arrayType": "file",
369 370
        "title": "照片",
		"multiple": true, // 允许选择多张图片,schema2code生效
Q
qiang 已提交
371 372 373 374 375 376 377 378 379
        "fileMediaType": "image", // 可选值 all|image|video 默认值为all,表示所有文件,image表示图片类型文件,video表示视频类型文件
        "fileExtName": "jpg,png", // 扩展名过滤,多个用 , 分割
        "maxLength": 3 // 限制最大数量
      }
    }
  }
}
```

380 381
- arrayType为file时,与单独的bsonType为file相同,`<uni-file-picker>`组件和schema2code均可使用。
- multiple、fileMediaType、fileExtName、maxLength都可对schema2code生成的客户端页面生效
Q
qiang 已提交
382

383
### 默认值defaultValue/forceDefaultValue@defaultvalue
Q
qiang 已提交
384

385
defaultValue和forceDefaultValue都是默认值,即新增一行数据记录时,如果字段内容未提供,则按默认值填充该字段内容。但2者也有区别,如下:
Q
qiang 已提交
386

387 388
- defaultValue没有强制力,是普通的默认值,如果客户端上传一个其他值,则按客户端传的值为准。
- forceDefaultValue则是schema强制约定的值,不管客户端传什么都无法修改。只要数据库新增一条记录,字段的值就会是forceDefaultValue。
Q
qiang 已提交
389

390 391
在实际开发中,forceDefaultValue常用于设置为当前服务器时间、当前登录用户id、客户端ip等。
这些数据都不能通过前端上传,不安全。过去只能在云端写云函数操作。在schema配置后则可以不用写云函数。使用JQL新增数据记录时会自动补齐这些数据。
Q
qiang 已提交
392

393
`defaultValue/forceDefaultValue`内可以使用固定值,还可以使用预置变量`$env`,形式如下:
Q
qiang 已提交
394

395 396 397 398 399
```json
"forceDefaultValue": {
  "$env": "now"
}
```
Q
qiang 已提交
400

401
预置变量`$env`可取值如下:
Q
qiang 已提交
402

403 404 405 406 407
|变量			|说明																								|
|:-:			|:-:																								|
|now			|当前服务器时间戳																					|
|clientIP	|当前客户端IP																				|
|uid			|当前用户Id,基于`uni-id`。如果当前用户未登录或登录状态无效会报错	|
Q
qiang 已提交
408

409
示例:
Q
qiang 已提交
410

411 412 413
```json
// 指定默认值为true
"defaultValue": true
Q
qiang 已提交
414

415 416 417 418
// 指定强制默认值为当前服务器时间戳
"forceDefaultValue": {
  "$env": "now"
}
Q
qiang 已提交
419

420 421 422 423
// 指定强制默认值为当前客户端IP
"forceDefaultValue": {
  "$env": "clientIP"
}
Q
qiang 已提交
424

425 426 427 428 429
// 指定强制默认值为当前客户id
"forceDefaultValue": {
  "$env": "uid"
}
```
Q
qiang 已提交
430 431


432 433 434
`resume`表为例,新增一个字段`create_time`,表示记录的创建时间。

该字段的`defaultValue`指定为服务器时间。新增记录时,若前端不传该字段,则默认为当前服务器时间。若前端传一个指定的值,则以传的值为准。
Q
qiang 已提交
435 436 437 438 439 440

```json
{
  "bsonType": "object",
  "required": [],
  "properties": {
441 442 443 444 445 446
    "create_time": {
      "bsonType": "timestamp",
      "title": "创建时间",
      "defaultValue": {
        "$env": "now"
      }
Q
qiang 已提交
447 448 449 450 451 452
    }
  }
}
```


453
强制默认值`forceDefaultValue`,指定为当前服务器时间戳。此时前端传任何值均无效,新增记录时一定会变成当前云端时间。
Q
qiang 已提交
454 455 456 457 458 459

```json
{
  "bsonType": "object",
  "required": [],
  "properties": {
460 461 462 463 464 465
    "create_time": {
      "bsonType": "timestamp",
      "title": "创建时间",
      "forceDefaultValue": {
        "$env": "now"
      }
Q
qiang 已提交
466 467 468 469 470
    }
  }
}
```

471
在实际业务中,记录的创建时间不能由客户端篡改,比如强制为云端时间。所以这个场景下必须使用`forceDefaultValue`
Q
qiang 已提交
472

473
### foreignKey字段外键
Q
qiang 已提交
474

475
一个复杂的业务系统,有很多张数据表。表与表之间,存在的数据关联。foreignKey用于描述数据关联关系。
雪洛's avatar
雪洛 已提交
476

477 478 479 480 481
比如一个文章系统,至少需要用户表、文章分类表、文章表、评论表。opendb已经包含了这4张表,可以点击链接看这些表的结构:
- 用户表:[uni-id-users](https://gitee.com/dcloud/opendb/blob/master/collection/uni-id-users/collection.json)
- 文章分类表:[opendb-news-categories](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-categories/collection.json)
- 文章表:[opendb-news-articles](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-articles/collection.json)
- 评论表:[opendb-news-comments](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-comments/collection.json)
Q
qiang 已提交
482

483 484
#### 分表
我们先不展开描述上面这几张表,首先讲解为什么分表、怎么分表。
Q
qiang 已提交
485

486 487 488 489
- 熟悉关系型数据库的开发者对分表设计并不陌生
	只不过在uniCloud的数据库中,新增了一个传统数据库并没有的schema,其中还有一个foreignKey来表达各个表之间关联关系。
	比如上面几个表中的文章表:[opendb-news-articles](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-articles/collection.json),其中的作者字段`user_id`配置了`"foreignKey": "uni-id-users._id"`
- 而对于初学者,首先需要搞明白的是,为什么需要分表。
Q
qiang 已提交
490

491 492 493
因为MongoDB的灵活性,理论上可以在用户表[uni-id-users]中新增一个字段articles,在articles下面通过数组来存放该作者的每一遍文章,然后在该文章中再来一个字段comments,存放该文章的每一条评论。

如下,uni-id-users表的数据内容,假使里面有2个用户,zhangsan和lisi,然后lisi写了1篇文章,这篇文章又被zhangsan评论了1条。
Q
qiang 已提交
494
```json
495 496 497 498 499 500 501
[{
	"_id": "60b92a42e22fbe00018c359d",
    "username": "zhangsan",
    "password": "03caebb36670995fc261a275d212cad65e4bbebd",
    "register_date": 1622747714731,
    "register_ip": "192.168.0.1",
},
Q
qiang 已提交
502
{
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
	"_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"
				}
			]
		}
	]
}]
Q
qiang 已提交
525 526
```

527 528 529 530 531 532 533 534 535 536 537
可以看出,这个uni-id-users表形成了用户、文章、评论的三层嵌套。

虽然MongoDB可以这么嵌套,但**实际业务中不该这样设计**。会导致查询性能低下甚至某些查询条件无法实现。

数据库是数字系统的底层,它应该清晰有条理,人、文章、评论以及这3者的关系,都应该清晰且不冗余。

MongoDB的嵌套,更多的适用于**不会被单独拎出来查询的、记录条数较少的场景**

比如简历表中的工作经历,就可以嵌套。因为工作经历数量较少、且不会出现单独查工作经历而不查人的情况。

但文章表,是一定需要独立的,因为文章数量会非常多,它会单独搜索;
Q
qiang 已提交
538

539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
评论表其实不太有单独搜索的需求,它总是伴随指定文章出现。但因为数量会很多,评论也需要分页查询,嵌套在文章表下不利于分页查询。

所以正确的数据库设计,还是分开这几张表。另外很多文章系统都会有文章分类,比如 社会、教育、娱乐、体育、科技...,所以还需要一个文章分类表。

opendb的这4张表,才是正确的分表设计。
- 用户表:[uni-id-users](https://gitee.com/dcloud/opendb/blob/master/collection/uni-id-users/collection.json)
- 文章分类表:[opendb-news-categories](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-categories/collection.json)
- 文章表:[opendb-news-articles](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-articles/collection.json)
- 评论表:[opendb-news-comments](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-comments/collection.json)

可以看到注册用户都在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
Q
qiang 已提交
562
```json
563 564 565 566 567 568 569
[{
	"_id": "60b92a42e22fbe00018c359d",
    "username": "zhangsan",
    "password": "03caebb36670995fc261a275d212cad65e4bbebd",
    "register_date": 1622747714731,
    "register_ip": "127.0.0.1",
},
Q
qiang 已提交
570
{
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
	"_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`
```json
{
	"_id": "606e721280b8450001e773c6",
    "category_id": "606e6feb653b8400017214a3",
    "title": "这里是标题",
    "content": "这里是正文",
    "user_id": "60b9315801033700011ba9ed",
    "publish_date": 1617850851000,
    "publish_ip": "119.131.174.251"
Q
qiang 已提交
589
}
590
```
Q
qiang 已提交
591

592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
只要`user_id`这个关联关系映射起来,数据就清晰且完整了。

并不需要在文章表opendb-news-articles存放作者的用户名、昵称、头像、注册时间甚至密码,只需要存它的`user_id`,就精准、最小冗余的表达数据关系。

当然也有的系统设计为了减少联表查询而在文章表里冗余存放作者昵称和头像,是否使用冗余可以视需求而定,但一定需要用`user_id`来做数据表的关联。

#### foreignKey的用法

上面显示的是2个表的数据内容,但回到 DB Schema 中,如何在schema中表达这种关联关系?那就是foreignKey,外键。

文章表opendb-news-articles的 DB Schema 中的`user_id`字段的描述如下:
```json
"properties": {
  "_id": {
	"description": "存储文档 ID(用户 ID),系统自动生成"
  },
  "user_id": {
	"bsonType": "string",
	"description": "文章作者ID, 参考`uni-id-users` 表",
	"foreignKey": "uni-id-users._id",
	"defaultValue": {
	  "$env": "uid"
	}
  },
  "title":{},
  "content":{}
}
Q
qiang 已提交
619 620
```

621
上面的重点,就在这个foreignKey,**它的前半部分是另一张表的表名,中间用`.`分割,后半部分是另一张表的字段名**
Q
qiang 已提交
622

623 624 625 626 627 628 629
它代表文章表这个`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](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-news-comments/collection.json)的schema里,
- 评论用户id字段`user_id`的foreignKey需指向`"uni-id-users._id"`
- 文章id字段`article_id`的foreignKey需指向`"opendb-news-articles._id"`
Q
qiang 已提交
630

631
配置foreignKey,除了清晰描述数据关系,它最大的作用是**联表查询**
Q
qiang 已提交
632

633
JQL没有像SQL那样提供了join、leftjoin、innerjoin这些语法,只需要配置好数据关系,配好foreignKey,查询时就可以自动联表查询。
Q
qiang 已提交
634

635
联表查询内容较多,[详见](jql.md#lookup)
Q
qiang 已提交
636

637 638 639 640 641 642 643 644
<!-- 
以简历和招聘系统为例,一个完整系统至少需要如下表:
- 应聘者注册信息表(uni-id-users),包括`_id`、用户名、密码、注册时间
- 应聘者简历表(resume),包括`_id`、应聘者uid和简历中各种信息
- 招聘方注册信息表(uni-id-users),一般还是使用uni-id-users表,但通过不同的appid区分。详见uni-id的多系统登录
- 招聘方岗位表(job),包括`_id`、招聘方uid、职务名称、职务内容要求、薪资范围、职务发布时间
- 简历投递记录表(send-resume),包括`_id`、应聘者uid、岗位jobid、投递时间、简历是否被查阅
 -->
Q
qiang 已提交
645

646
### parentKey树形表
Q
qiang 已提交
647

648
在传统关系型数据库中,tree是很难表达的,只有oracle这种商业数据库提供了tree查询。其他关系型数据库需要开发者通过复杂的代码实现tree查询。
Q
qiang 已提交
649

650
在MongoDB中,虽然自身天然支持tree,但实际业务中并不会使用MongoDB的json嵌套方式来描述tree。
Q
qiang 已提交
651

652
比如部门tree,部门可以动态的新增、删除、改名、挪动层级。实际上每个部门,在部门表里的数据仍然是一条独立的行数据记录,并不是一条记录里无限嵌套下去。
Q
qiang 已提交
653

654 655
如部门表,里面有2条数据,一条数据记录是“总部”,另一条数据记录“一级部门A”
- 错误的做法:
Q
qiang 已提交
656
```json
657 658 659 660 661 662 663 664 665 666 667 668
{
    "_id": "5fe77207974b6900018c6c9c",
    "name": "总部",
	"child": [
		{
		    "_id": "5fe77232974b6900018c6cb1",
		    "name": "一级部门A",
		    "child": [],
		    "status": 0
		}
	],
    "status": 0
Q
qiang 已提交
669 670 671
}
```

672 673 674
除非你的部门就这2个,永远不变。否则就不该使用上面的做法。
1. 如果部门很多嵌套很深,查询写法会很困难
2. 如果需要提供可视化的界面给运维人员,动态增删改部门,很难实现。往往需要改代码而不是直接改数据。
Q
qiang 已提交
675

676 677
- 正确的做法:
每个部门是一条记录,用`parent_id`来描述层级关系。
Q
qiang 已提交
678
```json
679 680 681 682 683 684 685 686 687 688 689 690 691
[{
    "_id": "5fe77207974b6900018c6c9c",
    "name": "总部",
    "parent_id": "",
    "status": 0
},
{
    "_id": "5fe77232974b6900018c6cb1",
    "name": "一级部门A",
    "parent_id": "5fe77207974b6900018c6c9c",
    "status": 0
}]
```
Q
qiang 已提交
692

693
在"一级部门A"的`parent_id`中,值为`5fe77207974b6900018c6c9c`,它其实就是"总部"的`_id`
Q
qiang 已提交
694

695
那么在 DB Schema 中如何表达这种关系呢?就要使用parentKey。
Q
qiang 已提交
696

697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
部门表[opendb-department](https://gitee.com/dcloud/opendb/blob/master/collection/opendb-department/collection.json)的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-禁用"
    }
  }
Q
qiang 已提交
721 722 723
}
```

724 725 726
parentKey将数据表不同记录的父子关系描述了出来。一个字段A的属性设置了parentKey并指向另一个字段B,那么这个A的值,就一定等于B的值。

使用parentKey描述了字段父子关系后,就可以通过JQL的getTree方便的做tree查询了。因内容较多,[详见](jql.md#gettree)
Q
qiang 已提交
727

728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
## 字段值域规则validator@validator

`DB Schema`提供了一套完善的字段值域描述规则,并且自动进行数据入库校验,不符合规则的数据无法写入数据库。

注意只有要对数据库写入内容时(新增记录或修改记录)才涉及字段值域的校验问题。读与删不涉及。

`DB Schema`里的字段值域校验系统由4部分组成:
1. 字段的属性配置:是否必填(required)、数字范围(maximum、minimum)、字符串长度范围(minLength、maxLength)、format、pattern正则表达式、enum、trim等所有属性表格中被分类在值域校验里的属性
2. 字段间的关系约束:fieldRules。在schema一级节点,和properties平级。它比字段的属性配置更强大,可以描述字段之间的关系,比如字段结束时间需大于字段开始时间;可以简单编程
3. 字段的扩展校验函数:validateFunction。当属性配置和fieldRules都不能满足需求,还可以写完整的js编程进行校验。
4. 错误提示:errorMessage。常见错误有默认的错误提示语。开发者也可以自定义错误提示语

### 1. 字段属性配置

#### required必填字段

在schema一级节点的`required`中,可以以数组的方式填入多个字段名称。比如以下示例将name字段设为必填
Q
qiang 已提交
745 746 747 748

```json
{
  "bsonType": "object",
749
  "required": ["name"],
Q
qiang 已提交
750
  "properties": {
751 752 753 754
    "name": {
      "bsonType": "string",
      "title": "姓名",
      "errorMessage": "{title}不能为空"
Q
qiang 已提交
755 756 757 758 759
    }
  }
}
```

760 761 762
一个字段的`required`,和字段的其他规则的关系如下:
- 字段被`required`时,必须传入该字段
- 字段没有被`required`时,如果传入的数据包含该字段则进行其他规则校验,否则忽略该字段的其他校验规则。
Q
qiang 已提交
763

764
以下面的代码为例,如果不传name的值可以通过校验;如果传了name则要求name最小长度为2,否则校验失败
Q
qiang 已提交
765 766 767 768 769 770

```json
{
  "bsonType": "object",
  "required": [],
  "properties": {
771 772 773 774 775 776 777
    "name": {
      "bsonType": "string",
      "title": "姓名",
      "minLength": 2,
      "errorMessage": {
        "required": "{title}不能为空",
        "minLength": "{title}不能小于 {minLength} 个字符"
Q
qiang 已提交
778 779 780 781 782 783
      }
    }
  }
}
```

784
#### format@url
Q
qiang 已提交
785

786
仅对string类型字段生效。
Q
qiang 已提交
787

788
其中format的url格式补充说明如下:
Q
qiang 已提交
789

790
`http://` | `https://` | `ftp://` 开头, `//` 后必须包含一个 `.`(localhost除外)
Q
qiang 已提交
791

792 793 794 795 796 797 798 799 800 801 802
有效格式
- http://dcloud.io
- https://dcloud.io
- http://localhost

无效格式
- http://dcloud
- https://dcloud
- mailto:dcloud@dcloud.io
- file:\\
- file:\\\
Q
qiang 已提交
803

804
**示例**
Q
qiang 已提交
805

806
可以在`resume`表中增加一个email字段,使用format来约束其格式。
Q
qiang 已提交
807 808 809 810

```json
{
  "bsonType": "object",
811
  "required": ["email"],
Q
qiang 已提交
812
  "properties": {
813
    "email": {
Q
qiang 已提交
814
      "bsonType": "string",
815 816 817 818 819 820
      "title": "邮箱",
      "format": "email",
      "errorMessage": {
        "required": "{title}不能为空",
        "format": "{title}格式无效"
      }
Q
qiang 已提交
821 822 823 824 825
    }
  }
}
```

826 827 828
#### pattern正则表达式

例如: 验证手机号 `"pattern": "^\\+?[0-9-]{3,20}$"`
Q
qiang 已提交
829 830 831 832 833


```json
{
  "bsonType": "object",
834
  "required": ["name"],
Q
qiang 已提交
835 836 837
  "properties": {
    "name": {
      "bsonType": "string",
838 839
      "title": "姓名",
      "pattern": "",
Q
qiang 已提交
840
      "errorMessage": {
841 842
        "required": "{title}不能为空",
        "pattern": "{title}格式无效"
Q
qiang 已提交
843 844 845 846 847 848
      }
    }
  }
}
```

849
#### enum枚举控制值域@enum
Q
qiang 已提交
850

851 852 853 854 855 856 857 858 859 860
enum,即枚举。一个字段设定了enum后,该字段的合法内容,只能在enum设定的候选数据项中取值。**enum最多只可以枚举500条**

enum支持3种数据格式来描述候选:
1. 简单数组
2. 支持描述的复杂数组
3. 数据表查询

##### enum格式之简单数组

比如给`resume`表增加一个性别字段,合法值域只能是“0”、“1”、“2”中的一个。
Q
qiang 已提交
861 862 863 864

```json
{
  "bsonType": "object",
865
  "required": [],
Q
qiang 已提交
866
  "properties": {
867 868 869 870 871 872 873 874
    "_id": {
      "description": "存储文档 ID(用户 ID),系统自动生成"
    },
    "gender": {
      "bsonType": "array",
      "title": "性别",
      "description": "用户性别:0 未知 1 男性 2 女性",
      "enum": [0,1,2]
Q
qiang 已提交
875 876 877 878 879
    }
  }
}
```

880
字段gender设成这样后,插入或修改的数据如果不是 0 或 1 或 2,就会报错,无法插入或更新数据。
Q
qiang 已提交
881

882 883 884 885 886
通过schema2code生成前端表单页面时,带有enum的字段会生成 picker 组件。该组件在界面上渲染时会生成“1、2、3”这3个候选的复选框。所以一般不推荐使用简单数组,而是推荐下面的 支持描述的数组。

##### enum格式之支持描述的复杂数组

仍然使用性别字段举例,合法值域只能是“0”、“1”、“2”中的一个。但通过schema2code生成前端表单页面时,该字段会生成uni-data-checkbox组件,该组件在界面上渲染时会生成“未知”、“男”、“女”这3个候选的复选框。
Q
qiang 已提交
887 888 889 890

```json
{
  "bsonType": "object",
891
  "required": [],
Q
qiang 已提交
892
  "properties": {
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
    "_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组件](https://ext.dcloud.net.cn/plugin?id=3456)来表达性别选择。

##### enum格式之数据表查询

一个字段的合法值域,可以是从另一个数据查询而来。也即,在enum中可以配置JQL查询语句。

**这种方式需要搭配foreignKey来使用,也就是需要关联另一个表**

在opendb中有一个民族表[opendb-nation-china](https://gitee.com/dcloud/opendb/tree/master/collection/opendb-nation-china),里面存放了中国的56个民族。

我们要在`resume`表中加一个民族字段,就应该从这个`opendb-nation-china`表取值。

1. 首先新建这个民族表

在项目根目录uniCloud/database点右键,新建schema,选择`opendb-nation-china`

这种opendb表的预置数据,需要上传schema到云端,才会添加到数据库中。所以需要对这个`opendb-nation-china.schema.json`点右键,上传 DB Schema

就可以uniCloud web控制台创建,此时会自动入库数据,但需要对项目根目录uniCloud/database点右键->下载 DB Schema,才可以在本地调试时使用。

2. 建立关联表关系、配置enum的jql查询

设置`nation`字段的外键`"foreignKey": "opendb-nation-china.name"`。民族比较简单,这里我们直接取了民族表的汉字名称为关联key,没有取数据库ID。

然后设置`nation`字段的enum如下:

```json
{
  "bsonType": "object",
  "required": [],
  "properties": {
    "_id": {
      "description": "存储文档 ID(用户 ID),系统自动生成"
    },
    "nation": {
Q
qiang 已提交
956
      "bsonType": "string",
957 958 959 960 961 962
      "title": "民族",
      "foreignKey": "opendb-nation-china.name",
      "enum": {
        "collection": "opendb-nation-china",
        "orderby": "first_letter asc",
        "field": "name as value, name as text"
Q
qiang 已提交
963 964 965 966 967 968
      }
    }
  }
}
```

969
这样客户端如果传上来一个不在`opendb-nation-china`表里的民族名称,是无法入库的。
Q
qiang 已提交
970

971
通过schema2code生成前端表单页面时,该字段会生成 picker 组件,该组件被点击后,会弹出候选项,这些候选项都是从民族表中查询数据并显示的。
Q
qiang 已提交
972

973 974 975
- enum格式之数据表查询之tree型数据

除了普通的二维数据表,enum还支持**tree型数据**。即enumType为tree。
Q
qiang 已提交
976

977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030
在opendb中有一个城市表[opendb-city-china](https://gitee.com/dcloud/opendb/tree/master/collection/opendb-city-china),里面存放了中国的各个城市。城市是按 省、市、区 分三级的树形数据。

`resume`表中,有一个city字段,其合理的字段规则应该是从`opendb-city-china`表取值,

设置`enumType`为"tree",代表enum里的数据为树形结构,比如下面的例子,代表opendb-city-china表以getTree方式查询。在schema2code时,可自动生成多级级联选择组件,[详见](/uniCloud/schema2code?id=schema2picker)
```json
{
  "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"
        }
      }
    }
  }
}

```

**注意**

- enum内对普通的二维数据表枚举时,此表数据不可超过500条。否则违反enum最大500个值域范围的限制。

#### trim去除首尾空白字符@trim

是否将字符串两边空格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,导致这个数据无法被写入数据库。
Q
qiang 已提交
1031 1032 1033 1034 1035 1036 1037 1038

```json
{
  "bsonType": "object",
  "required": ["name"],
  "properties": {
    "name": {
      "bsonType": "string",
1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
      "title": "姓名",
      "minLength": "2",
      "trim" : "both"
    }
  }
}
```


### 2. fieldRules字段间校验@field-rules

`HBuilderX 3.1.0`起,支持schema内配置一级节点fieldRules对字段之间的关系进行约束和校验。当然只校验一个字段也可以。

fieldRules的写法等同JQL的where写法(也可以使用各种数据库运算方法),参考:[JQL where](jql.md#where)

fieldRules内配置如下,数组内可以配置多个rule,每个rule都有rule表达式、错误提示语、运行兼容环境这3部分。

```js
{
  "fieldRules": [{
    "rule": "end_date == null || end_date != null && create_date < end_date", // 校验规则
    "errorMessage": "创建时间和结束时间不匹配", // 错误提示信息(仅在新增时生效,更新数据时不会提示此信息)
    "client": false // schema2code时,当前规则是否带到前端也进行校验。目前此属性暂不生效,fieldRules不会在客户端校验数据,仅会在云端进行校验
  }],
}
```

rule表达式,是一组js,返回值必须为true或false。返回false则触发提示错误,错误提示显示的是errorMessage的内容。

rule表达式里支持:

1. 字段名称
2. 数据库运算方法
3. js语法
4. 另外还支持`new Date()`来获取时间。需要注意的是不同于数据库运算方法,`new Date()`内不可传入数据库字段作为参数

上述配置中,`create_date``end_date`为字段名称。schema内也支持写字段操作方法,如add方法。

例:在todo表内可以使用fieldRules限制`end_date`大于`create_date`

```json
{
  "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"
Q
qiang 已提交
1097
      }
1098 1099 1100 1101
    },
    "end_date": {
      "bsonType": "timestamp",
      "title": "结束时间"
Q
qiang 已提交
1102 1103 1104 1105
    }
  }
}
```
1106 1107 1108 1109
  
上述示例中,`create_date`为必填项,只需限制`end_date`存在时大于`create_date`即可

**注意**
Q
qiang 已提交
1110

1111 1112 1113
- 新增/更新数据时会校验所有新增/更新字段相关联的fieldRules。如上述规则中,如果更新`end_date`字段或者`create_date`字段均会触发校验
- 新增数据时不需要查库进行校验,更新数据时需要进行一次查库校验(有多条fieldRules时也是一次)
- fieldRules内不支持使用正则
Q
qiang 已提交
1114

1115
### 3. validateFunction扩展校验函数@validatefunction
Q
qiang 已提交
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129

扩展校验函数

当属性配置不满足需求,需要写js函数进行校验时,使用本功能。

**注意**

- 扩展校验函数不能有其他依赖
- 尽量不要在扩展校验函数中使用全局变量,如果一定要用请务必确保自己已经阅读并理解了[云函数的启动模式](uniCloud/cf-functions.md?id=launchtype)

如何使用

- 方式一:在uniCloud web控制台创建
1. uniCloud 控制台,选择服务空间,切换到数据库视图
D
DCloud_LXH 已提交
1130
2. 底部 “扩展校验函数” 点击 “+” 增加校验函数 ![](https://web-assets.dcloud.net.cn/unidoc/zh/schema-validate-function.png)
Q
qiang 已提交
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
3. 给函数起个名字,比如叫“checkabc”

- 方式二:在HBuilderX中创建
`HBuilderX 3.0.0`及以上版本,可以在项目下创建扩展校验云函数并上传,使用方法如下:

1. 在左侧项目管理器选择工程,对其下的`uniCloud`目录点右键,选择`创建database目录`(如果已有该目录则忽略本步骤)
2. 在第一步创建的database目录右键选择`创建数据库扩展校验函数目录`
3. 在第二步创建的`validateFunction`目录右键选择`新建数据库扩展校验函数`

`validateFunction`目录右键,还可以上传和下载`validateFunction`,和uniCloud web控制台进行同步。

扩展校验函数示例如下

  ```js
  // 扩展校验函数示例
  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云端。

3. 在需要的字段中引用写好的`validateFunction`

编写`扩展校验函数`后,在表结构 schema 中确定要配置的字段,在该字段的`validateFunction`属性上,配置上面编写的`扩展校验函数`的名称。

如下例中,当name字段的内容要入库前,就会触发执行 "checkabc" 这个 `扩展校验函数` 。如果"checkabc"校验没有返回true,则该次数据库操作会失败。

`validateFunction` 类型为字符串时,云端和客户端同时生效

  ```json
  {
    "bsonType": "object",
    "required": ["name"],
    "properties": {
      "name": {
        "bsonType": "string",
1203
        "title": "姓名",
Q
qiang 已提交
1204 1205
        "validateFunction": "checkabc",
        "errorMessage": {
1206
          "required": "{title}不能为空"
Q
qiang 已提交
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
        }
      }
    }
  }
  ```


`validateFunction` 类型为对象时,可配置客户端同不生效,云端仍然生效

> HBuilder 3.1.0+ 支持

  ```json
  {
    "bsonType": "object",
    "required": ["name"],
    "properties": {
      "name": {
        "bsonType": "string",
1225
        "title": "姓名",
Q
qiang 已提交
1226 1227 1228 1229 1230
        "validateFunction": {
            "name": "checkabc", // 扩展校验函数名
            "client": false //如果不配置默认是 true
        },
        "errorMessage": {
1231
          "required": "{title}不能为空"
Q
qiang 已提交
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
        }
      }
    }
  }
  ```

提示:如果配置了 `"client": false` 客户端也可以在生成的代码中改为自己的校验函数,此时客户端的校验仍然生效,客户端对应的校验文件目录为 `js_sdk/validator/collection``collection`为表名,非固定值


`扩展校验函数`是服务空间级的,一个`扩展校验函数`可以被这个服务空间下的任意表中的任意字段引用。

`扩展校验函数`里的代码是可以联网的。一个常见场景是内容的敏感词过滤,可以将内容提交到三方校验服务里,如果校验通过再入库。

不建议在`扩展校验函数`里编写大量的代码,会影响性能。

**扩展校验函数 的运行环境注意事项**

`扩展校验函数`的默认运行环境与普通云函数的环境相同,可以调用云函数里可用的各种API。
雪洛's avatar
雪洛 已提交
1250 1251
	* 如果要连接外网,要调用[uniCloud.httpclient](cf-functions.md#id=httpclient)
	* 如果要调用数据库,需使用云函数里操作数据库的方式,即不支持JQL,[详见](cf-database.md)
Q
qiang 已提交
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272

但是,在[schema2code](/uniCloud/schema?id=autocode)中,`扩展校验函数`也会被生成到前端页面的校验规则里。

也就是说,如果使用[schema2code](/uniCloud/schema?id=autocode)生成前端页面,那么写`扩展校验函数`需要多一层注意。

比如调用了uniCloud.httpclient这样在前端并不存在的API时,前端的表单校验会出错。

此时就需要在`扩展校验函数`中多写一个if判断,避免undefined的问题。

```js
if (uniCloud.httpclient) {
	console.log("此处运行在云函数环境里。前端没有这个API");
}
// 或者另一种写法
if (uni) {
	console.log("此处运行在前端环境里。云函数没有uni对象,除非你在validateFunction里自己定义了这个对象");
}
```



1273
### 4. errorMessage自定义错误提示@errormessage
Q
qiang 已提交
1274 1275 1276 1277 1278 1279 1280

数据不符合schema配置的规范时,无法入库,此时会报错。

uniCloud有一些基本错误提示语的格式化,如需自定义错误提示语,就需要使用本功能,根据errorMessage的定义报出错误提示。

errorMessage支持字符串,也支持json object。类型为object时,可定义多个校验提示。

1281
{} 为占位符,可在其中引用已有属性,如title、label等。
Q
qiang 已提交
1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296

|属性		|类型	|描述	|
|:-			|:-		|:-		|
|minLength	|string	|消息	|
|maxLength	|string	|消息	|
|...		|...	|...	|

示例

```json
{
  "required": ["name"],
  "properties": {
    "name": {
      "bsonType": "string",
1297
      "title": "姓名",
Q
qiang 已提交
1298 1299 1300
      "minLength": 2,
      "maxLength": 8,
      "errorMessage": {
1301 1302 1303
        "required": "{title}必填",
        "minLength": "{title}不能小于{minLength}个字符",
        "maxLength": "{title}不能大于{maxLength}个字符"
Q
qiang 已提交
1304 1305 1306 1307 1308
      },
      ...
    },
    "age": {
      "bsonType": "int",
1309
      "title": "年龄",
Q
qiang 已提交
1310 1311
      "minimum": 1,
      "maximum": 150,
1312
      "errorMessage": "{title}应该大于 {minimum} 岁,小于 {maximum} 岁"
Q
qiang 已提交
1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328
    }
  }
}
```

从示例可以看出,errorMessage支持配一条,也支持根据不同的错误类型配不同的errorMessage。

- 每个校验相关的属性不通过,可以以属性名为key配置它的错误提示语。
- 如果是扩展校验函数,可以在其内部写callback来自定义错误提示语。

**其他注意事项**

“数据库中某字段值不能在多条记录中重复”,这个需求一般不是在字段值域校验里实现,而是在数据库索引里配置该字段为唯一索引。[详见](/uniCloud/hellodb?id=dbindex)

可以在web控制台配置索引,db_init.json也可以创建索引。注意如果数据库中多条记录中该字段已经有重复内容,那么设该字段为唯一索引时会报错,需先把重复数据去掉。

1329
## 数据权限系统permission@permission
Q
qiang 已提交
1330

1331
### 概述
Q
qiang 已提交
1332

1333
`DB Schema`的数据权限系统,是为`JQL`设计的,尤其`clientDB`强依赖这套权限系统。因为客户端是无法信任的,没有缜密的权限系统,会导致客户端任意改动云数据库内容。
Q
qiang 已提交
1334

1335
在过去,开发者需要在后端写代码来处理权限控制,但实际上有了`DB Schema``uni-id`后,这种权限控制的后台代码就不用再写了。
Q
qiang 已提交
1336

1337
只要配好`DB Schema`的权限,放开让前端写业务即可。配置里声明不能读写的数据,前端就无法读写。
Q
qiang 已提交
1338 1339 1340 1341

`DB Schema`的permission规则,分为两部分,一边是对操作数据的指定,一边是对角色的指定,规则中对两者进行关联,匹配则校验通过。

1. 对数据的指定
1342
- 可以对整个表进行`增删改查或计数`控制
Q
qiang 已提交
1343 1344 1345 1346
- 可以针对字段进行`读写`控制
- 可以配置具体的where规则,对指定的数据记录进行`删改查`控制
- 默认自带一个特殊数据的描述,就是当前请求计划操作的数据(doc),后面会详解用法
2. 对角色的指定
1347 1348 1349 1350
- 未登录,即游客都可以操作数据
- 当前已登录用户(auth.uid)
- `uni-id`定义的其他角色
	* 开发者可以在`uni-id`中自定义各种角色,比如部门管理员,然后在`DB Schema`的permission中配置其可操作的数据。详见[uni-id的角色权限](uni-id-summary.md#rbac)
Q
qiang 已提交
1351 1352 1353 1354 1355

**注意**:如果登录用户是`uni-id`的admin角色,即超级管理员,则不受`DB Schema`的配置限制的,admin角色拥有对所有数据的读写权限。

例如在`uniCloud admin`等管理端系统,只要使用admin用户登录就可以在前端操作数据库。

1356
在更新云端`DB Schema`时,如果发现服务空间下没有`uni-id`公共模块,会自动安装`uni-id`。如果服务空间已经存在`uni-id`,则不会再自动安装。此时需要注意及时升级`uni-id`,避免太老的`uni-id`有兼容问题。(从HBuilderX 3.5起,改为`uni-id-common`公共模块)
Q
qiang 已提交
1357

1358
### 表级权限控制@col-permission
Q
qiang 已提交
1359 1360 1361

表级控制,包括增删改查四种权限,分别称为:create、delete、update、read。(注意这里使用的是行业通用的crud命名,与操作数据库的方法add()、remove()、update()、get()在命名上有差异,但表意是相同的)

1362 1363 1364
HBuilderX 3.1.0起还新增了count权限,即是否有权对该表进行统计计数。

所有的操作的默认值均为false。也就是不配置permission代表不能操作数据库(角色为admin用户例外)。
Q
qiang 已提交
1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382

例如一个user表,里面有_id、name、pwd等字段,该表的`DB Schema`如下,代表前端用户可读(包括游客),但前端非admin用户不可新增、删除、更新数据记录。

```json
// 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":{},
雪洛's avatar
雪洛 已提交
1383
    "age": {},
Q
qiang 已提交
1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394
    "pwd": {}
  }
}
```
  
**关于count权限的说明**

- 在HBuilderX 3.1.0之前,count操作都会使用表级的read权限进行验证。HBuilderX 3.1.0及之后的版本,如果配置了count权限则会使用表级的read+count权限进行校验,两条均满足才可以通过校验
- 如果schema内没有count权限,则只会使用read权限进行校验
- 所有会统计数量的操作均会触发count权限校验

1395
### 字段级权限控制
Q
qiang 已提交
1396 1397 1398 1399 1400

permission的字段级控制,包括读写两种权限,分别称为:read、write。

也就是对于一个指定的字段,可以控制什么样的角色可以读取该字段内容,什么样的角色可以修改写入字段内容。

雪洛's avatar
雪洛 已提交
1401
以上述的user表为例,假如要限制前端禁止读取age字段,那么按如下配置,在字段age下面再写permission节点,设定read为false。
Q
qiang 已提交
1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415

```json
// user表的schema
{
  "bsonType": "object",
  "required": [],
  "permission": {
    "read": true, // 任何用户都可以读
    "create": false, // 禁止新增数据记录(admin权限用户不受限)
    "update": false, // 禁止更新数据(admin权限用户不受限)
    "delete": false // 禁止删除数据(admin权限用户不受限)
  },
  "properties": {
    "_id":{
雪洛's avatar
雪洛 已提交
1416 1417 1418 1419 1420 1421 1422 1423 1424 1425
    },
    "name":{
    },
    "age": {
      "bsonType": "number",
      "title": "年龄",
      "permission": {
        "read": false, // 禁止读取 age 字段的数据(admin权限用户不受限)
        "write": false // 禁止写入 age 字段的数据(admin权限用户不受限)
      }
Q
qiang 已提交
1426 1427 1428 1429 1430
    }
  }
}
```

雪洛's avatar
雪洛 已提交
1431
按上述配置,前端查询数据时,如果不包含age字段,仍然可以查询。但如果查询请求包含age字段,该请求会被拒绝,提示无权访问。
Q
qiang 已提交
1432

雪洛's avatar
雪洛 已提交
1433
子级会继承父级的权限,即需要同时满足父级权限以及本节点权限,方可操作此节点。例如上述schema中如果配置表级read权限为false,在为name设置read权限为true的情况下,name字段仍不可读
Q
qiang 已提交
1434

1435
如果字段的bsonType配置为password,则clientDB完全不可操作此字段(即使是admin用户也不可以在客户端读写)。
雪洛's avatar
雪洛 已提交
1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460

```js
// user表的schema
{
  "bsonType": "object",
  "required": [],
  "permission": {
    "read": true, // 任何用户都可以读
    "create": false, // 禁止新增数据记录(admin权限用户不受限)
    "update": false, // 禁止更新数据(admin权限用户不受限)
    "delete": false // 禁止删除数据(admin权限用户不受限)
  },
  "properties": {
    "_id":{
    },
    "name":{
    },
    "pwd": {
      "bsonType": "password", // 即使不配置权限,此字段也无法在客户端读写
      "title": "密码"
    }
  }
}
```

1461
### 指定数据集权限控制
Q
qiang 已提交
1462 1463 1464 1465 1466

`DB Schema`提供了一个内置变量doc,表示要意图操作的数据记录。并支持用各种表达式来描述指定的记录。

仍然以user表举例,假使该表有个字段叫`status`表示用户是否被禁用。`status`是bool值,true代表用户状态正常,false代表被禁用。

1467
然后有个需求,JQL只能查用户状态正常的用户信息,禁用用户信息无法查。那么schema配置如下,表级控制的read节点的值不再是简单的true/false,而是变成一个表达式:`"doc.status==true"`
Q
qiang 已提交
1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501

```json
// 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代表用户被禁用"
    }
  }
}
```

1502
根据这个配置,如JQL查询user表的所有数据,则会报权限校验失败;如JQL查询里在where条件里声明了只查询status字段为true的数据,则查询会放行。
Q
qiang 已提交
1503

1504
### 权限规则的变量和运算符
Q
qiang 已提交
1505 1506 1507 1508 1509

除了上述例子提到的doc变量,事实上`DB Schema`的权限规则支持很多变量和运算符,可以满足各种配置需求。

**权限规则内可用的全局变量**

雪洛's avatar
雪洛 已提交
1510 1511 1512 1513 1514 1515 1516 1517
|变量名					|说明																																																																							|
|:-:						|:-:																																																																							|
|auth.uid				|用户id																																																																						|
|auth.role			|用户角色数组,参考[uni-id 角色权限](uni-id-summary.md#rbac),注意`admin`为内置的角色,如果用户角色列表里包含`admin`则认为此用户有完全数据访问权限|
|auth.permission|用户权限数组,参考[uni-id 角色权限](uni-id-summary.md#rbac)																																											|
|doc						|数据库中的目标数据记录,用于匹配记录内容/查询条件																																																|
|now						|当前服务器时间戳(单位:毫秒),时间戳可以进行额外运算,如doc.publish\_date > now - 60000表示publish\_date在最近一分钟														|
|action					|数据操作请求同时指定的uni-clientDB-action。用于指定前端的数据操作必须同时附带执行一个action云函数,如未触发该action则权限验证失败								|
Q
qiang 已提交
1518 1519

**注意**
Q
qiang 已提交
1520
- `auth`表示正在执行操作的用户对象
1521
- `auth.xxx`均由uni-id提供,依赖于[uni-id公共模块](uni-id-summary.md)
Q
qiang 已提交
1522
- `doc.xxx`表示将要查询/修改/删除的每条数据(注意并不包括新增数据,新增数据应通过值域校验进行验证),如果将要访问的数据不满足permission规则将会拒绝执行
1523
- `uni-id`的角色和权限,也即auth.role和auth.permission是不一样的概念。注意阅读[uni-id 角色权限](uni-id-summary.md#rbac)
Q
qiang 已提交
1524
- 如果想支持使用多个`action`的用法,可以通过`"'actionRequired' in action"`的形式配置权限,限制客户端使用的action内必须包含名为`actionRequired`的action
雪洛's avatar
雪洛 已提交
1525
- doc可以理解为将要访问的数据,因此create权限内不可使用doc变量。create时建议使用forceDefaultValue或自定义校验函数实现插入数据的值域校验。
Q
qiang 已提交
1526 1527 1528 1529 1530 1531 1532

**权限规则内可以使用的运算符**

|运算符			|说明			|示例									|示例解释(集合查询)														|
|:-:			|:-:			|:-:									|:-:																	|
|==				|等于			|auth.uid == 'abc'						|用户id为abc															|
|!=				|不等于			|auth.uid != null						|用户要处于登录状态											|
雪洛's avatar
雪洛 已提交
1533 1534 1535 1536 1537 1538 1539 1540
|>				|大于			|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							|
|&#124;&#124;	|或				|auth.uid == 'abc'&#124;&#124;doc.age>10|用户Id为abc或者目标数据的 age 属性大于 10								|
Q
qiang 已提交
1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593


我们继续使用user表举例,目前需求如下,前端用户如果登录,那么该用户可以修改自己的name字段。此时需要在schema中配置name字段的permission为`"write":"(doc._id == auth.uid)"`

```json
// 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方法,见下一章节。

1594
forceDefaultValue属于数据校验的范畴,在数据写入时生效,但是如果配置forceDefaultValue为`{"$env": "uid"}`也会进行用户身份的校验,未登录用户不可插入数据。
Q
qiang 已提交
1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607

例如在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中创建权限、角色和用户授权,另见[文档](/uniCloud/admin?id=mutiladmin)

**变量action的说明**

1608
action是`clientDB`的一个配套功能。它的作用是在前端发起数据操作请求时,附带一个action的name,则会同时执行一个`uni-clientDB-action`的云函数。[详见](jql.md#action)
Q
qiang 已提交
1609 1610 1611

有些复杂业务,要求必须同时执行一个action云函数,才能允许前端对特定数据的修改。

1612
以user表为例,假使用户在修改自己的name时,必须要触发一个名为changenamelog的action云函数,在该云函数里会记录一条留痕日志,如果没有记录日志则不允许修改name。
雪洛's avatar
雪洛 已提交
1613
那么在`DB Schema`里要配置`'changenamelog' in action`
Q
qiang 已提交
1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633

```json
// user表的schema
{
  "bsonType": "object",
  "required": [],
  "permission": {
    "read": "doc.status==true", // 任何用户都可以读status字段的值为true的记录,其他记录不可读
    "create": false, // 禁止新增数据记录(admin权限用户不受限)
    "update": false, // 禁止更新数据(admin权限用户不受限)
    "delete": false // 禁止删除数据(admin权限用户不受限)
  },
  "properties": {
    "_id":{
	},
	"name":{
		"bsonType": "string",
		"title": "名称",
		"permission": {
		  "read": true, 
雪洛's avatar
雪洛 已提交
1634
		  "write": "(doc._id == auth.uid) && ('changenamelog' in action)" // 允许登录的用户修改自己的name字段,但必须同时触发执行action云函数changenamelog
Q
qiang 已提交
1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670
		}
	},
    "pwd": {
      "bsonType": "string",
      "title": "密码",
      "permission": {
        "read": false, // 禁止读取 pwd 字段的数据(admin权限用户不受限)
        "write": false // 禁止写入 pwd 字段的数据(admin权限用户不受限)
      }
    },
	"status": {
		"bsonType": "bool",
		"title": "用户状态",
		"description": "true代表用户正常。false代表用户被禁用"
	}
  }
}
```

前端提交代码,必须带上action参数
```js
const db = uniCloud.database();
db.action("changenamelog").collection("user").doc("xxx").update({
	name:"newname"
})
```

action云函数中记录日志的代码,此处省略。

根据上述配置,如前端应用已经登录,且登录的用户发起修改自己的name的请求,且同时前端的改库请求伴随action云函数changenamelog,则允许修改。其他修改数据请求则会被拒绝。

`action`有很多用途,有的权限规则比较复杂,需要写很多js代码,此时也可以在`action`的before中进行校验。

但注意导出`db_init.json`时不会包含`action``action`属于云函数。导出`db_init.json`只会包含schema和validateFunction。


1671
### 权限规则内的数据库查询get方法
Q
qiang 已提交
1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699

权限规则内置了doc变量,但只能用于要操作的数据表的判断,如果要获取其他表的数据做判断就需要get方法了。

权限规则内通过get方法,根据id获取数据库中的数据。get方法接收一个字符串作为参数,字符串形式为`database.表名.记录ID`

例如有个论坛,要求用户积分大于100分才可以发帖。那么帖子表的create权限应该配成:

```json
// 使用模板字符串语法拼接产生`database.表名.记录ID`形式字符串
"create": get(`database.uni-id-users.${auth.uid}`).score > 100"
```

使用get方法时需要注意get方法的参数必须是唯一确定值,例如schema配置的get权限如下:

```json
// 这句的含义是,本次查询where条件内传入的shop_id需要满足以下条件:shop表内_id为此shop_id的记录的owner字段等于当前用户uid
"get(`database.shop.${doc.shop_id}`).owner == auth.uid"
```

前端js如下:
```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()
```

1700
## schema2code代码生成系统@autocode
Q
qiang 已提交
1701 1702 1703

`DB Schema`里有大量的信息,其实有了这些信息,前端将无需自己开发表单维护界面,uniCloud可以自动生成新增、修改、列表、详情的前端页面,以及admin端的列表、新增、修改、删除全套功能。

1704
因内容较长,请另见文档[schema2code](schema2code.md)