jql-schema-ext.md 16.7 KB
Newer Older
W
wanganxp 已提交
1
# DB Schema扩展js
雪洛's avatar
雪洛 已提交
2

W
wanganxp 已提交
3
DB Schema 的json文件无法编程,可编程扩展的js将大大增强schema的控制能力。
雪洛's avatar
雪洛 已提交
4

W
wanganxp 已提交
5 6
过去clientDB里使用action来处理schema.json不足的地方。但action云函数有个安全缺陷,无法禁止客户端发起指定action的调用。

雪洛's avatar
雪洛 已提交
7
从 HBuilderX 3.6.11+,uniCloud提供了可编程schema,每个`${表名}.schema.json`旁边都可以配置一个`${表名}.schema.ext.js`
W
wanganxp 已提交
8 9 10 11

- 在HBuilderX项目下,在目录 uniCloud/database/ 下可以创建`${表名}.schema.ext.js`
- 在uniCloud web控制台的数据库表管理界面,在schema.json旁边也有`${表名}.schema.ext.js`的在线管理。

DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
12 13 14
schema扩展js在规划中可以实现很多事情,目前仅上线数据库触发器功能。

推荐开发者使用JQL数据库触发器来替代action云函数。
W
wanganxp 已提交
15 16 17 18 19 20 21 22

## 数据库触发器@trigger

JQL的数据库触发器,用于在执行一段JQL数据库指令(增删改查等)的同时触发相应的操作。

仅限使用JQL来操作数据库,客户端和云端均可以执行JQL。但使用传统MongoDB写法不支持数据库触发器。

可以使用触发器方便的实现很多功能,例如:
雪洛's avatar
雪洛 已提交
23

W
wanganxp 已提交
24 25 26
1. 更新数据时自动将更新时间修改为当前时间
2. 读取文章详情后阅读量加1
3. 发布一篇文章后自动给文章作者列表文章数量加1
雪洛's avatar
雪洛 已提交
27

W
wanganxp 已提交
28 29
由于数据库触发器是在云端执行的,所以clientDB操作数据库时很多不宜写在前端的代码,就可以挪到数据库触发器中实现。

W
wanganxp 已提交
30
如果把数据库的schema定义好,包括json和ext.js,那么各个业务模块就可以随便安心的调用数据库了,数据一致性逻辑和安全保障将被统一管理,不担心不良业务代码的破坏、不担心哪次调用会漏掉更新时间字段。
W
wanganxp 已提交
31

雪洛's avatar
雪洛 已提交
32
### 触发器配置@config
雪洛's avatar
雪洛 已提交
33

雪洛's avatar
雪洛 已提交
34
在项目的`uniCloud/database`目录下创建`${表名}.schema.ext.js`,内容如下。
雪洛's avatar
雪洛 已提交
35 36 37 38

```js
module.exports = {
  trigger: {
W
wanganxp 已提交
39 40 41 42
	// 注册数据表的read前事件
    beforeRead: async function (
	// 确定要监听的什么样的JQL指令
	{
雪洛's avatar
雪洛 已提交
43 44 45 46 47
      collection,
      operation,
      where,
      field
    } = {}) {
W
wanganxp 已提交
48 49
		// 当上述jql指令被触发时,将执行这里的代码。这里就是普通的uniCloud代码,可以调用uniCloud的各种api。
		console.log("这个触发器被触发了")
雪洛's avatar
雪洛 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62
    },
    afterRead: async function ({
      collection,
      operation,
      where,
      field
    } = {}) {

    }
  }
}
```

W
wanganxp 已提交
63 64
如上配置会在使用jql读取此表内容时触发`beforeRead``afterRead`
除这两个时机外还有`beforeCount``afterCount``beforeCreate``afterCreate``beforeUpdate``afterUpdate``beforeDelete``afterDelete`这些触发时机,后续章节会详细说明
雪洛's avatar
雪洛 已提交
65

W
wanganxp 已提交
66 67 68
ext.js里引入公共模块的机制:
- 在通过clientDB访问时触发器内可以使用包含在clientDB内的公共模块,如何将公共模块引入clientDB请参考:[jql依赖公共模块](jql.md#common-for-jql)
- 在通过云函数/云对象使用jql访问时,触发器可以使用云函数/云对象依赖的公共模块。
雪洛's avatar
雪洛 已提交
69

雪洛's avatar
雪洛 已提交
70
### 触发器入参@trigger-param
雪洛's avatar
雪洛 已提交
71 72 73 74 75

所有触发器均在数据校验、权限校验之后执行,beforeXxx会在实际执行数据库指令之前执行,afterXxx会在实际执行数据库指令之后执行。

触发器的入参有以下几个,不同时机的触发器参数略有不同

雪洛's avatar
雪洛 已提交
76 77 78 79 80 81 82 83 84 85 86 87
|参数名				|类型								|默认值	|是否必备				|说明																																												|
|--						|--									|--			|--							|--																																													|
|collection		|string							|-			|是							|当前表名																																										|
|operation		|string							|-			|是							|当前操作类型:`create``update``delete``read``count`																|
|where				|object							|-			|否							|当前请求使用的查询条件(见下方说明)																												|
|field				|array<string>|-			|read必备				|当前请求访问的字段列表(见下方说明)																												|
|addDataList	|array<object>|-			|create必备			|新增操作传入的数据列表(见下方说明)																												|
|updateData		|object							|-			|update必备			|更新操作传入的数据(见下方说明)																														|
|clientInfo		|object							|-			|是							|客户端信息,包括设备信息、用户token等,详见:[clientInfo](cf-functions.md#get-client-infos)|
|userInfo			|object							|-			|是							|用户信息																																										|
|result				|object							|-			|afterXxx内必备	|本次请求结果																																								|
|isEqualToJql	|function						|-			|是							|用于判断当前执行的jql语句和执行语句是否相等																								|
雪洛's avatar
雪洛 已提交
88
|triggerContext	|object						|-			|是							|用于在before和after内共享数据																								|
雪洛's avatar
雪洛 已提交
89 90

#### where@where
雪洛's avatar
雪洛 已提交
91 92 93 94 95

> read、count、delete、update操作可能有此参数

触发器收到的where参数为转化后的查询条件,可以直接作为参数传给db.collection()和dbJql.collection()的where方法。jql语句使用doc方法时也会转成where,形如:{_id: 'xxx'}

雪洛's avatar
雪洛 已提交
96
#### field@field
雪洛's avatar
雪洛 已提交
97 98 99 100 101

> 仅read操作有此参数

field为所有被访问的字段的组成的数组,嵌套的字段会被摊平。

雪洛's avatar
雪洛 已提交
102
#### addDataList@add-data-list
雪洛's avatar
雪洛 已提交
103 104 105

> 仅create操作有此参数

W
wanganxp 已提交
106 107 108
数据库指令add方法的参数和schema内defaultValue、forceDefaultValue合并后的列表。注意为统一数据结构,add方法内只传递一个对象时此参数也是一个仅有一项的数组。

如果在给数据库插入数据前拦截并修改了addDataList的数据,那么插入数据库的就会是新修改的数据。
雪洛's avatar
雪洛 已提交
109

雪洛's avatar
雪洛 已提交
110
#### updateData@update-data
雪洛's avatar
雪洛 已提交
111 112 113

> 仅update操作有此参数

W
wanganxp 已提交
114 115 116
数据库指令update方法的参数。

如果在给数据库修改数据前拦截并修改了updateData的数据,那么更新进数据库的就会是新修改的数据。
雪洛's avatar
雪洛 已提交
117

雪洛's avatar
雪洛 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
#### userInfo@user-info

> 新增于 HBuilderX 3.6.14

用户信息包含以下字段

|字段名			|类型							|说明															|
|--					|--								|--																|
|uid				|string|null	|用户id,未能获取用户信息时为null	|
|role				|array						|角色列表,默认为空数组						|
|permission	|array						|权限列表,默认为空数组						|

#### result@result

> 新增于 HBuilderX 3.6.14

本次数据库操作的结果,不同操作返回不同的结构。对result对象的修改会应用到最终返回的结果内

**查询**

```js
{
	data: [] // 获取到的数据列表
}
```

**查询带count**

```js
{
	data: [], // 获取到的数据列表
  count: 0 // 符合条件的数据条数
}
```

雪洛's avatar
雪洛 已提交
153 154 155 156 157 158 159 160
**计数**

```js
{
  total: 0 // 符合条件的数据条数
}
```

雪洛's avatar
雪洛 已提交
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
**新增单条**

```js
{
	id: '' // 新增数据的id
}
```

**新增多条**

```js
{
	ids: [], // 新增数据的id列表
	inserted: 3 // 新增成功的条数
}
```

**更新数据**

```js
{
	updated: 1 // 更新的条数,数据更新前后无变化则更新条数为0
}
```

#### isEqualToJql@is-equal-to-jql

> 新增于 HBuilderX 3.6.14

DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
190 191 192 193 194
用于判断触发器当前执行的jql语句和方法传入的语句是否等价的方法。

开发者除了使用field、where等分解后的对象,也可以使用isEqualToJql来判断当前执行的JQL语句是什么。

如果单纯使用字符串比较,开发者会遇到单双引号、换行等原因造成比较失败。所以提供了isEqualToJql方法。
雪洛's avatar
雪洛 已提交
195 196 197 198 199 200 201 202 203 204

**用法**

```js
isEqualToJql(string JQLString)
```

**返回值**

此方法返回一个bool值,true表示当前执行的jql语句和传入的语句相等,否则为不等
雪洛's avatar
雪洛 已提交
205 206 207 208 209 210 211 212 213 214 215

**示例**

```js
// article.schema.ext.js
module.exports {
  trigger: {
    beforeCount: async function({
      isEqualToJql
    } = {}) {
      if(isEqualToJql('db.collection("article").count()')) {
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
216
        console.log('成功匹配了JQL命令:对article表进行count计数且未带条件')
雪洛's avatar
雪洛 已提交
217 218 219 220 221 222 223 224
      } else {
        throw new Error('禁止执行带条件的count')
      }
    }
  }
}
```

雪洛's avatar
雪洛 已提交
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
#### triggerContext@trigger-context

> 新增于 HBuilderX 3.6.16

此参数为一个空对象,仅用于在before内挂载数据并在after内获取使用

**示例**

```js
// article.schema.ext.js
module.exports {
  trigger: {
    beforeUpdate: async function({
      triggerContext
    } = {}) {
      triggerContext.shareVar = 1
    },
    afterUpdate: async function(){
      if (triggerContext.shareVar === 1) {
        console.log('获取到的triggerContext.shareVar为1')
      }
    }
  }
}
```

雪洛's avatar
雪洛 已提交
251
### 触发时机@trigger-timing
雪洛's avatar
雪洛 已提交
252 253 254 255 256 257 258 259 260 261 262 263 264 265

|触发时机			|说明				|
|---					|---				|
|beforeRead		|读取前触发	|
|afterRead		|读取后触发	|
|beforeCount	|计数前触发	|
|afterCount		|计数后触发	|
|beforeCreate	|新增前触发	|
|afterCreate	|新增后触发	|
|beforeUpdate	|更新前触发	|
|afterUpdate	|更新后触发	|
|beforeDelete	|删除前触发	|
|afterDelete	|删除后触发	|

雪洛's avatar
雪洛 已提交
266 267
**注意**

雪洛's avatar
雪洛 已提交
268
- count有两种触发情况一种是在数据库指令使用了count方法,另一种是在get方法内传getCount参数。截至HBuilderX 3.6.14版本,get方法内传getCount参数不会触发count触发器,此问题会在后续版本进行修复。
雪洛's avatar
雪洛 已提交
269

雪洛's avatar
雪洛 已提交
270
### 示例@demo
雪洛's avatar
雪洛 已提交
271 272 273

以下article表为例。

雪洛's avatar
雪洛 已提交
274
为了不增加示例的复杂度,所有权限均设置为true,实际项目中切勿模仿。
雪洛's avatar
雪洛 已提交
275 276

```js
雪洛's avatar
雪洛 已提交
277
// article.schema.ext.js
雪洛's avatar
雪洛 已提交
278 279 280 281 282 283 284 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 313 314 315
{
  "bsonType": "object",
  "required": ["title", "content"],
  "permission": {
    "read": true,
    "create": true,
    "update": true,
    "delete": true
  },
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "title": {
      "bsonType": "string"
    },
    "summary": {
      "bsonType": "string"
    },
    "content": {
      "bsonType": "string"
    },
    "author": {
      "bsonType": "string"
    },
    "view_count": {
      "bsonType": "int"
    },
    "create_date": {
      "bsonType": "timestamp"
    },
    "update_date": {
      "bsonType": "timestamp"
    }
  }
}
```

W
wanganxp 已提交
316 317 318 319 320 321 322
#### 修改文章更新时间

```js
// article.schema.ext.js
module.exports {
  trigger: {
    beforeUpdate: async function({
雪洛's avatar
雪洛 已提交
323 324
      collection,
      operation,
W
wanganxp 已提交
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
      where,
      updateData,
      clientInfo
    } = {}) {
      const id = where && where._id
      if(typeof id === 'string' && (updateData.title || updateData.content)) { //如果字段较多,也可以不列举字段,删掉后半个判断
        if(updateData.content) {
          // updateData.summary = 'xxxx' // 根据content生成summary
        }
        updateData.update_date = Date.now() // 更新数据的update_date字段赋值为当前服务器时间
      }
    }
  }
}
```

雪洛's avatar
雪洛 已提交
341
#### 读取后触发实现阅读量加1
雪洛's avatar
雪洛 已提交
342 343

```js
雪洛's avatar
雪洛 已提交
344
// article.schema.ext.js
雪洛's avatar
雪洛 已提交
345 346 347
module.exports {
  trigger: {
    afterRead: async function({
雪洛's avatar
雪洛 已提交
348 349
      collection,
      operation,
雪洛's avatar
雪洛 已提交
350 351 352 353 354 355 356 357 358
      where,
      field,
      clientInfo
    } = {}) {
      const db = uniCloud.database()
      const id = where && where._id
      // clientInfo.uniIdToken可以解出客户端用户信息,再进行判断是否应该加1。为了让示例简单清晰,此处省略相关逻辑
      if(typeof id === 'string' && field.includes('content')) {
        // 读取了content字段后view_count加1
雪洛's avatar
雪洛 已提交
359
        await db.collection('article').where(where).update({
雪洛's avatar
雪洛 已提交
360 361 362 363 364 365 366 367
          view_count: db.command.inc(1)
        })
      }
    }
  }
}
```

雪洛's avatar
雪洛 已提交
368
#### 删除前备份
雪洛's avatar
雪洛 已提交
369 370

```js
雪洛's avatar
雪洛 已提交
371
// article.schema.ext.js
雪洛's avatar
雪洛 已提交
372 373 374
module.exports {
  trigger: {
    beforeDelete: async function({
雪洛's avatar
雪洛 已提交
375 376
      collection,
      operation,
雪洛's avatar
雪洛 已提交
377 378 379 380 381 382 383 384
      where,
      clientInfo
    } = {}) {
      const db = uniCloud.database()
      const id = where && where._id
      if(typeof id !== 'string') { // 此处也可以加入管理员可以批量删除的逻辑
        throw new Error('禁止批量删除')
      }
雪洛's avatar
雪洛 已提交
385
      const res = await db.collection('article').where(where).get()
雪洛's avatar
雪洛 已提交
386 387 388 389 390 391 392 393 394 395
      const record = res.data[0]
      if(record) {
        await db.collection('article-archived').add(record)
      }
    }
  }
}
```


雪洛's avatar
雪洛 已提交
396
#### 新增文章时自动添加摘要
雪洛's avatar
雪洛 已提交
397 398

```js
雪洛's avatar
雪洛 已提交
399
// article.schema.ext.js
雪洛's avatar
雪洛 已提交
400 401 402
module.exports {
  trigger: {
    beforeCreate: async function({
雪洛's avatar
雪洛 已提交
403 404
      collection,
      operation,
雪洛's avatar
雪洛 已提交
405 406 407 408
      addDataList,
      clientInfo
    } = {}) {
      for(let i = 0; i < addDataList.length; i++) {
雪洛's avatar
雪洛 已提交
409 410 411 412 413
        const addDataItem = addDataList[i]
        if(!addDataItem.content) {
          throw new Error('缺少文章内容')
        }
        addDataItem.summary = addDataItem.content.slice(0, 100)
雪洛's avatar
雪洛 已提交
414 415 416 417
      }
    }
  }
}
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
418 419 420 421 422 423 424
```


### 在触发器内使用jql语法@using-jql-in-schema

jql触发器内可以使用jql语法操作数据库。

W
wanganxp 已提交
425 426 427 428 429 430 431 432 433
注意:在触发器内再使用jql语法操作数据库还会执行触发器,很容易引发死循环!

为此,uniCloud.databaseForJQL方法增加了参数`skipTrigger`,用于指定本次数据库操作跳过触发器的执行。

skipTrigger是一个bool值,true跳过执行触发器,false则继续执行触发器。默认为false。

该参数客户端不生效,仅云端生效。

示例如下:
雪洛's avatar
雪洛 已提交
434 435 436 437

```js
uniCloud.databaseForJQL({
  clientInfo,
W
wanganxp 已提交
438
  skipTrigger: true // true跳过执行触发器,false则继续执行触发器。默认为false
雪洛's avatar
雪洛 已提交
439 440 441
})
```

DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
442
我们现在增加一个阅读记录表,schema如下
雪洛's avatar
雪洛 已提交
443

雪洛's avatar
雪洛 已提交
444
为了不增加示例的复杂度,所有权限均设置为true,实际项目中切勿模仿。
雪洛's avatar
雪洛 已提交
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461

```js
// article.schema.ext.js
{
  "bsonType": "object",
  "required": ["title", "content"],
  "permission": {
    "read": true,
    "create": true,
    "update": true,
    "delete": true
  },
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "article_id": {
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
462
      "bsonType": "string",
雪洛's avatar
雪洛 已提交
463 464 465
      "foreignKey": "article._id"
    },
    "reader_id": {
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
466 467 468 469
      "bsonType": "string",
      "foreignKey": "uni-id-users._id",
      "forceDefaultValue": {
        "$env": "uid"
雪洛's avatar
雪洛 已提交
470 471 472 473
      }
    }
  }
}
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
474 475 476 477
```

使用jql语法可以自动验证客户端身份,仍以上述article表为例,在afterRead触发器内插入阅读记录。此时会将读者id插入到reader_id字段

雪洛's avatar
雪洛 已提交
478 479 480 481
```js
module.exports = {
  trigger: {
    afterRead: async function ({
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
482
      where,
雪洛's avatar
雪洛 已提交
483 484
      field,
      clientInfo
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
485 486 487 488
    } = {}) {
      const id = where && where._id
      if(typeof id !== 'string' || !field.includes('content')) {
        return
雪洛's avatar
雪洛 已提交
489 490
      }
      const dbJQL = uniCloud.databaseForJQL({
雪洛's avatar
雪洛 已提交
491 492
        clientInfo,
        skipTrigger: true
雪洛's avatar
雪洛 已提交
493 494
      })
      await dbJQL.collection('article-view-log')
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
495 496 497
        .add({
          article_id: id,
          reader_id: dbJQL.getCloudEnv('$cloudEnv_uid')
雪洛's avatar
雪洛 已提交
498 499
        })
    }
DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
500
}
雪洛's avatar
雪洛 已提交
501 502
```

雪洛's avatar
雪洛 已提交
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
### 在触发器内使用扩展库和公共模块@module-and-extension

schema扩展依赖的公共模块和扩展库同样可以被action、validateFunction使用。

内置依赖:目前schema扩展依赖了`uni-id`[uni-id-common](uni-id-common.md),uni-id 3.0.7及以上版本又依赖了[uni-config-center](uni-config-center.md),这两个公共模块是可以在触发器内直接使用的。如果所在服务空间开通了redis,schema扩展内可直接使用redis扩展。

`HBuilderX 3.6.20`起,可以在项目的`uniCloud/database`目录上右键管理schema扩展依赖的公共模块和扩展库。同样在此目录右键选择`上传schema扩展Js的配置`将配置的依赖同步到云端。

![](https://web-assets.dcloud.net.cn/unidoc/zh/deps-of-jql.jpg)

`HBuilderX 3.2.7``HBuilderX 3.6.20`之间的版本,可通过在要使用的公共模块的package.json内配置`"includeInClientDB":true`,可以将公共模块和schema扩展关联,`HBuilderX 3.6.20`及之后的版本不推荐使用此用法

一个在JQL内使用的公共模块的package.json示例如下。

```js
{
  "name": "test-common",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "includeInClientDB": true
}
```

通过上述步骤建立起关联关系后,可正常在数据库触发器或action云函数中使用公共模块。

**注意**

- 尽量不要依赖体积过大的公共模块,会延长冷启动时间


雪洛's avatar
雪洛 已提交
537
### 注意事项
雪洛's avatar
雪洛 已提交
538 539 540 541 542

- 非getTemp联表查询(不推荐的用法)在触发器内获取的where为null、field为当前表的所有字段。
- 联表查询时只会触发主表触发器,不会触发副表触发器
- getTemp联表时主表所在的getTemp内的where和field会传递给触发器,虚拟联表的where和field不会传给触发器
- 通过jql的redis缓存读取的内容不会触发读触发器
雪洛's avatar
雪洛 已提交
543
- HBuilderX内使用jql数据管理功能执行jql语句时不会触发任何触发器
雪洛's avatar
雪洛 已提交
544

DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
545
#### 和action云函数的关系
雪洛's avatar
雪洛 已提交
546

DCloud_Heavensoft's avatar
DCloud_Heavensoft 已提交
547 548 549 550
- 数据库触发器比action云函数更安全,不会被前端错误指定。
- 数据库触发器支持JQL语法。action云函数只支持使用传统MongoDB方式。
- 数据库触发器能实现很多常见的action云函数功能,并且无需修改schema和数据库指令。
- 如果同时使用数据库触发器和action云函数,注意触发器的before会在所有action的before执行之前再执行,after会在所有action的after执行之后再执行。action无法捕获触发器抛出的错误。