简体中文
DB Schema 的json文件无法编程,可编程扩展的js将大大增强schema的控制能力。
过去clientDB里使用action来处理schema.json不足的地方。但action云函数有个安全缺陷,无法禁止客户端发起指定action的调用。
从 HBuilderX 3.6.11+,uniCloud提供了可编程schema,每个${表名}.schema.json
旁边都可以配置一个${表名}.schema.ext.js
。
${表名}.schema.ext.js
。${表名}.schema.ext.js
的在线管理。schema扩展js在规划中可以实现很多事情,目前仅上线数据库触发器功能。
推荐开发者使用JQL数据库触发器来替代action云函数。
JQL的数据库触发器,用于在执行一段JQL数据库指令(增删改查等)的同时触发相应的操作。
仅限使用JQL来操作数据库,客户端和云端均可以执行JQL。但使用传统MongoDB写法不支持数据库触发器。
可以使用触发器方便的实现很多功能,例如:
由于数据库触发器是在云端执行的,所以clientDB操作数据库时很多不宜写在前端的代码,就可以挪到数据库触发器中实现。
如果把数据库的schema定义好,包括json和ext.js,那么各个业务模块就可以随便安心的调用数据库了,数据一致性逻辑和安全保障将被统一管理,不担心不良业务代码的破坏、不担心哪次调用会漏掉更新时间字段。
在项目的uniCloud/database
目录下创建${表名}.schema.ext.js
,内容如下。
module.exports = {
trigger: {
// 注册数据表的read前事件
beforeRead: async function (
// 确定要监听的什么样的JQL指令
{
collection,
operation,
where,
field
} = {}) {
// 当上述jql指令被触发时,将执行这里的代码。这里就是普通的uniCloud代码,可以调用uniCloud的各种api。
console.log("这个触发器被触发了")
},
afterRead: async function ({
collection,
operation,
where,
field
} = {}) {
}
}
}
如上配置会在使用jql读取此表内容时触发beforeRead
和afterRead
。
除这两个时机外还有beforeCount
、afterCount
、beforeCreate
、afterCreate
、beforeUpdate
、afterUpdate
、beforeDelete
、afterDelete
这些触发时机,后续章节会详细说明
ext.js里引入公共模块的机制:
所有触发器均在数据校验、权限校验之后执行,beforeXxx会在实际执行数据库指令之前执行,afterXxx会在实际执行数据库指令之后执行。
触发器的入参有以下几个,不同时机的触发器参数略有不同
参数名 | 类型 | 默认值 | 是否必备 | 说明 |
---|---|---|---|---|
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 |
userInfo | object | - | 是 | 用户信息 |
result | object | - | afterXxx内必备 | 本次请求结果 |
isEqualToJql | function | - | 是 | 用于判断当前执行的jql语句和执行语句是否相等 |
triggerContext | object | - | 是 | 用于在before和after内共享数据,新增于3.6.16 |
array | - | 否 | 请使用secondaryCollection替代此参数,此参数仍可访问只是会给出警告 | |
secondaryCollection | array | - | 否 | 获取联表查询的副表列表,新增于3.7.1 |
rawWhere | object|string | - | 否 | 未经转化的原始查询条件,新增于3.7.0 |
rawGeoNear | object | - | 否 | 未经转化的原始geoNear参数,新增于3.7.0 |
skip | number | - | 否 | 跳过记录条数,新增于3.7.0 |
limit | number | - | 否 | 返回的结果集(文档数量)的限制,新增于3.7.0 |
sample | object | - | 否 | sample(随机选取)方法的参数,新增于3.7.0 |
docId | string | - | 否 | doc方法的参数,数据库记录的_id,新增于3.7.0 |
isGetTempLookup | boolean | - | 联表触发时必备 | 联表查询时用于标识,本次查询是否使用了getTemp,新增于3.7.1 |
primaryCollection | string | - | 副表read必备 | 联表查询时主表的表名,新增于3.7.13 |
仅read操作联表有此参数,新增于 3.7.1
联表查询副表组成的列表
// article.schema.ext.js
module.exports = {
trigger: {
beforeRead: async function({
secondaryCollection
} = {}) {
if(secondaryCollection && secondaryCollection.length > 1) {
throw new Error('仅允许关联一个副表')
}
}
}
}
read、count、delete、update操作可能有此参数
触发器收到的where参数为转化后的查询条件,可以直接作为参数传给db.collection()和dbJql.collection()的where方法。jql语句使用doc方法时也会转成where,形如:{_id: 'xxx'}
read、delete、update操作可能有此参数,新增于 3.7.0
触发器收到的doc方法内传递的文档_id
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeDelete: async function({
docId
} = {}) {
if(!docId) {
throw new Error('仅能指定文档id删除')
}
}
}
}
read、delete、update操作可能有此参数,新增于 3.7.0
where或match方法的原始参数,未经jql转化。如果是字符串或使用了数据库方法,则仅能传递给databaseForJQL返回的数据库实例使用,不能传递给database方法返回的数据库实例使用。
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeDelete: async function({
rawWhere
} = {}) {
if(rawWhere && typeof rawWhere !== string) {
throw new Error('仅能使用字符串作为查询条件')
}
}
}
}
仅read操作有此参数,新增于 3.7.0
geoNear方法的原始参数,未经jql转化。如果其中query是字符串或使用了数据库方法,则仅能传递给databaseForJQL返回的数据库实例使用,不能传递给database方法返回的数据库实例使用。
仅read操作有此参数,新增于 3.7.0
跳过的文档数量
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeRead: async function({
skip
} = {}) {
if(skip && skip > 10000) {
throw new Error('无法访问10000条以后的数据')
}
}
}
}
仅read操作有此参数,新增于 3.7.0
返回的结果集(文档数量)的限制
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeRead: async function({
limit
} = {}) {
if(limit && limit > 100) {
throw new Error('每次最多访问100条数据')
}
}
}
}
仅read操作有此参数,新增于 3.7.0
随机筛选方法的参数
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeRead: async function({
sample
} = {}) {
if(sample && sample.size > 100) {
throw new Error('每次最多随机筛选100条数据')
}
}
}
}
仅read操作有此参数
field为所有被访问的字段的组成的数组,嵌套的字段会被摊平。未传递field时会返回所有字段
仅create操作有此参数
数据库指令add方法的参数和schema内defaultValue、forceDefaultValue合并后的列表。注意为统一数据结构,add方法内只传递一个对象时此参数也是一个仅有一项的数组。
如果在给数据库插入数据前拦截并修改了addDataList的数据,那么插入数据库的就会是新修改的数据。
仅update操作有此参数
数据库指令update方法的参数。
如果在给数据库修改数据前拦截并修改了updateData的数据,那么更新进数据库的就会是新修改的数据。
新增于 HBuilderX 3.6.14
用户信息包含以下字段
字段名 | 类型 | 说明 |
---|---|---|
uid | string|null | 用户id,未能获取用户信息时为null |
role | array | 角色列表,默认为空数组 |
permission | array | 权限列表,默认为空数组 |
新增于 HBuilderX 3.6.14
本次数据库操作的结果,不同操作返回不同的结构。对result对象的修改会应用到最终返回的结果内
查询
{
data: [] // 获取到的数据列表
}
查询带count
{
data: [], // 获取到的数据列表
count: 0 // 符合条件的数据条数
}
计数
{
total: 0 // 符合条件的数据条数
}
新增单条
{
id: '' // 新增数据的id
}
新增多条
{
ids: [], // 新增数据的id列表
inserted: 3 // 新增成功的条数
}
更新数据
{
updated: 1 // 更新的条数,数据更新前后无变化则更新条数为0
}
删除数据
{
deleted: 1 // 删除的条数
}
新增于 HBuilderX 3.6.14
用于判断触发器当前执行的jql语句和方法传入的语句是否等价的方法。
开发者除了使用field、where等分解后的对象,也可以使用isEqualToJql来判断当前执行的JQL语句是什么。
如果单纯使用字符串比较,开发者会遇到单双引号、换行等原因造成比较失败。所以提供了isEqualToJql方法。
用法
isEqualToJql(string JQLString)
返回值
此方法返回一个bool值,true表示当前执行的jql语句和传入的语句相等,否则为不等
示例
// article.schema.ext.js
module.exports = {
trigger: {
beforeCount: async function({
isEqualToJql
} = {}) {
if(isEqualToJql('db.collection("article").count()')) {
console.log('成功匹配了JQL命令:对article表进行count计数且未带条件')
} else {
throw new Error('禁止执行带条件的count')
}
}
}
}
新增于 HBuilderX 3.6.16
此参数为一个空对象,仅用于在before内挂载数据并在after内获取使用
示例
// 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')
}
}
}
}
触发时机 | 说明 |
---|---|
beforeRead | 读取前触发 |
afterRead | 读取后触发 |
beforeCount | 计数前触发 |
afterCount | 计数后触发 |
beforeCreate | 新增前触发 |
afterCreate | 新增后触发 |
beforeUpdate | 更新前触发 |
afterUpdate | 更新后触发 |
beforeDelete | 删除前触发 |
afterDelete | 删除后触发 |
beforeReadAsSecondaryCollection | 集合作为副表被读取前触发,仅使用了getTemp的联表查询才会触发 |
afterReadAsSecondaryCollection | 集合作为副表被读取后触发,仅使用了getTemp的联表查询才会触发 |
注意
以下article表为例。
为了不增加示例的复杂度,所有权限均设置为true,实际项目中切勿模仿。
// article.schema.ext.js
{
"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"
}
}
}
// article.schema.ext.js
module.exports = {
trigger: {
beforeUpdate: async function({
collection,
operation,
docId,
updateData,
clientInfo
} = {}) {
if(typeof docId === 'string' && (updateData.title || updateData.content)) { //如果字段较多,也可以不列举字段,删掉后半个判断
if(updateData.content) {
// updateData.summary = 'xxxx' // 根据content生成summary
}
updateData.update_date = Date.now() // 更新数据的update_date字段赋值为当前服务器时间
}
}
}
}
// article.schema.ext.js
module.exports = {
trigger: {
afterRead: async function({
collection,
operation,
docId,
field,
clientInfo
} = {}) {
const db = uniCloud.database()
// clientInfo.uniIdToken可以解出客户端用户信息,再进行判断是否应该加1。为了让示例简单清晰,此处省略相关逻辑
if(typeof docId === 'string' && field.includes('content')) {
// 读取了content字段后view_count加1
await db.collection('article').doc(docId).update({
view_count: db.command.inc(1)
})
}
}
}
}
// article.schema.ext.js
module.exports = {
trigger: {
beforeDelete: async function({
collection,
operation,
docId,
clientInfo
} = {}) {
const db = uniCloud.database()
if(typeof docId !== 'string') { // 此处也可以加入管理员可以批量删除的逻辑
throw new Error('禁止批量删除')
}
const res = await db.collection('article').doc(docId).get()
const record = res.data[0]
if(record) {
await db.collection('article-archived').add(record)
}
}
}
}
// article.schema.ext.js
module.exports = {
trigger: {
beforeCreate: async function({
collection,
operation,
addDataList,
clientInfo
} = {}) {
for(let i = 0; i < addDataList.length; i++) {
const addDataItem = addDataList[i]
if(!addDataItem.content) {
throw new Error('缺少文章内容')
}
addDataItem.summary = addDataItem.content.slice(0, 100)
}
}
}
}
jql触发器内可以使用jql语法操作数据库。
注意:在触发器内再使用jql语法操作数据库还会执行触发器,很容易引发死循环!
为此,uniCloud.databaseForJQL方法增加了参数skipTrigger
,用于指定本次数据库操作跳过触发器的执行。
skipTrigger是一个bool值,true跳过执行触发器,false则继续执行触发器。默认为false。
该参数客户端不生效,仅云端生效。
示例如下:
uniCloud.databaseForJQL({
clientInfo,
skipTrigger: true // true跳过执行触发器,false则继续执行触发器。默认为false
})
我们现在增加一个阅读记录表,schema如下
为了不增加示例的复杂度,所有权限均设置为true,实际项目中切勿模仿。
// article.schema.ext.js
{
"bsonType": "object",
"required": ["title", "content"],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"bsonType": "string"
},
"article_id": {
"bsonType": "string",
"foreignKey": "article._id"
},
"reader_id": {
"bsonType": "string",
"foreignKey": "uni-id-users._id",
"forceDefaultValue": {
"$env": "uid"
}
}
}
}
使用jql语法可以自动验证客户端身份,仍以上述article表为例,在afterRead触发器内插入阅读记录。此时会将读者id插入到reader_id字段
module.exports = {
trigger: {
afterRead: async function ({
docId,
field,
clientInfo
} = {}) {
if(typeof docId !== 'string' || !field.includes('content')) {
return
}
const dbJQL = uniCloud.databaseForJQL({
clientInfo,
skipTrigger: true
})
await dbJQL.collection('article-view-log')
.add({
article_id: docId,
reader_id: dbJQL.getCloudEnv('$cloudEnv_uid')
})
}
}
schema扩展依赖的公共模块和扩展库同样可以被action、validateFunction使用。
内置依赖:目前schema扩展依赖了uni-id
或uni-id-common,uni-id 3.0.7及以上版本又依赖了uni-config-center,这两个公共模块是可以在触发器内直接使用的。如果所在服务空间开通了redis,schema扩展内可直接使用redis扩展。
自HBuilderX 3.7.0
起,可以在项目的uniCloud/database
目录上右键管理schema扩展依赖的公共模块和扩展库。同样在此目录右键选择上传schema扩展Js的配置
将配置的依赖同步到云端。
HBuilderX 3.2.7
到HBuilderX 3.7.0
之间的版本,可通过在要使用的公共模块的package.json内配置"includeInClientDB":true
,可以将公共模块和schema扩展关联,HBuilderX 3.7.0
及之后的版本不推荐使用此用法
一个在JQL内使用的公共模块的package.json示例如下。
{
"name": "test-common",
"version": "1.0.0",
"description": "",
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"includeInClientDB": true
}
通过上述步骤建立起关联关系后,可正常在数据库触发器或action云函数中使用公共模块。
注意