uniCloud JQL语法(二)

查询树形数据gettree

数据存储:tree上的每个节点都是一条单独的数据表记录,然后通过类似parent_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-禁用"
    }
  }
}

定义json数据

{
    "_id": "5fe77207974b6900018c6c9c",
    "name": "总部",
    "parent_id": "",
    "status": 0
}

{
    "_id": "5fe77232974b6900018c6cb1",
    "name": "一级部门A",
    "parent_id": "5fe77207974b6900018c6c9c",
    "status": 0
}

注意:一个表的一次查询中只能有一个父子关系。如果一个表的schema里多个字段均设为了parentKey,那么需要在JQL中通过parentKey()方法指定某个要使用的parentKey字段。

查询所有的节点

get({
  getTree: {
    limitLevel: 10, // 最大查询层级(不包含当前层级),可以省略默认10级,最大15,最小1
    startWith: "parent_id=='' || parent_id==null"  // 第一层级条件,此初始条件可以省略,不传startWith时默认从最顶级开始查询
  }
})

limitLevel表示查询返回的树的最大层级。超过设定层级的节点不会返回。如果数据实际层级超过15层,请分层懒加载查询。
startWith用于描述从哪个或哪些节点开始查询树。不填时,默认的条件是 'parent_id==null||parent_id==""'

完整代码

db.collection("department").get({
        getTree: {}
    })
    .then((res) => {
        const resdata = res.result.data
        console.log("resdata", resdata);
    }).catch((err) => {
        uni.showModal({
            content: err.message || '请求服务失败',
            showCancel: false
        })
    }).finally(() => {
        
    })

返回的结果

"data": [{
    "_id": "5fe77207974b6900018c6c9c",
    "name": "总部",
    "parent_id": "",
    "status": 0,
    "children": [{
        "_id": "5fe77232974b6900018c6cb1",
        "name": "一级部门A",
        "parent_id": "5fe77207974b6900018c6c9c",
        "status": 0,
        "children": []
    }]
}]

结合where语句
注意的是where内的条件也会对第一级数据生效

db.collection("department")
  .where('status==0')
  .get({
    getTree: {
      startWith: '_id=="1"'
    }
    })
    .then((res) => {
        const resdata = res.result.data
        console.log("resdata", resdata);
    }).catch((err) => {
        uni.showModal({
            content: err.message || '请求服务失败',
            showCancel: false
        })
    }).finally(() => {
        
    })

查询树形结构父节点路径

get({
  getTreePath: {
    limitLevel: 10, // 最大查询层级(不包含当前层级),可以省略默认10级,最大15,最小1
    startWith: 'name=="一级部门A"'  // 末级节点的条件,此初始条件不可以省略
  }
})

返回结果只包括“一级部门A”的直系父,其父节点的兄弟节点不会返回。所以每一层数据均只有一个节点。

注意

  • 暂不支持使用getTreePath的同时使用其他联表查询语法
  • 如果使用了where条件会对所有查询的节点生效

分组统计groupby

数据分组统计,即根据某个字段进行分组(groupBy),然后对其他字段分组后的值进行求和、求数量、求均值。
JQL的groupField里不能直接写field字段,只能使用分组运算方法来处理字段,常见的累积器计算符包括:count(*)、sum(字段名称)、avg(字段名称)。
开发者也不应该在groupBy和groupField里使用_id字段,_id是唯一的,没有统一意义。
示例: 如果数据库score表为某次比赛统计的分数数据,每条记录为一个学生的分数。学生有所在的年级(grade)、班级(class)、姓名(name)、分数(score)等字段属性。

{
  _id: "1",
  grade: "1",
  class: "A",
  name: "zhao",
  score: 5
}
{
  _id: "2",
  grade: "1",
  class: "A",
  name: "qian",
  score: 15
}
{
  _id: "3",
  grade: "1",
  class: "B",
  name: "li",
  score: 15
}
{
  _id: "4",
  grade: "1",
  class: "B",
  name: "zhou",
  score: 25
}
{
  _id: "5",
  grade: "2",
  class: "A",
  name: "wu",
  score: 25
}
{
  _id: "6",
  grade: "2",
  class: "A",
  name: "zheng",
  score: 35
}

求和

const res = await db.collection('score')
.groupBy('grade,class')
.groupField('sum(score) as totalScore')
.get()

求平均值

const res = await db.collection('score')
.groupBy('grade,class')
.groupField('avg(score) as avgScore')
.get()

统计数量

const res = await db.collection('score')
.groupBy('grade,class')
.groupField('count(*) as totalStudents')
.get()

注意:count(*)为固定写法,括号里的*可以省略

按日分组
假设要统计uni-id-users表的每日新增注册用户数量。表内有以下数据:

{"_id":"1","username":"name1","register_date":1611367810000// 2021-01-23 10:10:10}{"_id":"2","username":"name2","register_date":1611367810000// 2021-01-23 10:10:10}{"_id":"3","username":"name3","register_date":1611367810000// 2021-01-23 10:10:10}{"_id":"4","username":"name4","register_date":1611281410000// 2021-01-22 10:10:10}{"_id":"5","username":"name5","register_date":1611281410000// 2021-01-22 10:10:10}{"_id":"6","username":"name6","register_date":1611195010000// 2021-01-21 10:10:10}
const res = await db.collection('uni-id-users')
.groupBy('dateToString(add(new Date(0),register_date),"%Y-%m-%d","+0800") as date')
.groupField('count(*) as newusercount')
.get()

其中:
add操作符的用法为add(值1,值2)。add(new Date(0),register_date)表示给字段register_date + 0,这个运算没有改变具体的时间,但把register_date的格式从时间戳转为了日期类型。
dateToString操作符的用法为dateToString(日期对象,格式化字符串,时区)

查询返回结果

res = {
  result: {
    data: [{
      date: '2021-01-23',
      newusercount: 3
    },{
      date: '2021-01-22',
      newusercount: 2
    },{
      date: '2021-01-21',
      newusercount: 1
    }]
  }
}

同时发送多条数据库请求

使用multiSend可以将多个数据库请求合并成一个发送。

const bannerQuery = db.collection('banner').field('url,image').getTemp() // 这里使用getTemp不直接发送get请求,等到multiSend时再发送
const noticeQuery = db.collection('notice').field('text,url,level').getTemp()
const res = await db.multiSend(bannerQuery,noticeQuery)

返回值

// 上述请求返回以下结构
res = {
  code: 0, // 请求整体执行错误码,注意如果多条查询执行失败,这里的code依然是0,只有出现网络错误等问题时这里才会出现错误
  message: '', // 错误信息
  dataList: [{
    code: 0, // bannerQuery 对应的错误码
    message: '', // bannerQuery 对应的错误信息
    data: [] // bannerQuery 查询到的数据
  }, {
    code: 0, // noticeQuery 对应的错误码
    message: '', // noticeQuery 对应的错误信息
    data: [] // noticeQuery 查询到的数据
  }]
}

action

从HBuilderX 3.6.11开始,推荐使用数据库触发器替代action云函数。以下内容仅为向下兼容保留
action的作用是在执行前端发起的数据库操作时,额外触发一段云函数逻辑。它是一个可选模块。action是运行于云函数内的,可以使用云函数内的所有接口。

action支持一次使用多个,比如使用db.action("action-a,action-b"),其执行流程为action-a.before->action-b.before->执行数据库操作->action-b.after->action-a.after。在任一before环节抛出错误直接进入after流程,在after流程内抛出的错误会被传到下一个after流程。
action是一种特殊的云函数,它不占用服务空间的云函数数量。

注意action方法是db对象的方法,只能跟在db后面,不能跟在collection()后面
db.action("someactionname").collection('table1')

action创建
项目-->uniCloud-->cloudfunctions-->uni-clientDB-actions(如果没有该文件夹,右键cloudfunctions创建该文件夹)--> 右键新建action

使用

// 客户端发起请求,给todo表新增一行数据,同时指定action为add-todo
const db = uniCloud.database()
db.action('add-todo') //注意action方法是db的方法,只能跟在db后面,不能跟在collection()后面
  .collection('todo')
  .add({
    title: 'todo title'
  })
  .then(res => {
    console.log(res)
  }).catch(err => {
    console.error(err)
  })

定义add-todo action

// 一个action文件示例 uni-clientDB-actions/add-todo.js
module.exports = {
  // 在数据库操作之前执行
  before: async(state,event)=>{
    // state为当前数据库操作状态其格式见下方说明
    // event为传入云函数的event对象
    
    // before内可以操作state上的newData对象对数据进行修改,比如:
    state.newData.create_time = Date.now()
    // 指定插入或修改的数据内的create_time为Date.now()
    // 执行了此操作之后实际插入的数据会变成 {title: 'todo title', create_time: xxxx}
    // 实际上,这个场景,有更简单的实现方案:在db schema内配置defaultValue或者forceDefaultValue,即可自动处理新增记录使用当前服务器时间
  },
  // 在数据库操作之后执行
  after:async (state,event,error,result)=>{
    // state为当前数据库操作状态其格式见下方说明
    // event为传入云函数的event对象
    // error为执行操作的错误对象,如果没有错误error的值为null
    // result为执行command返回的结果
    
    if(error) {
      throw error
    }
    
    // after内可以对result进行额外处理并返回
    result.msg = 'hello'
    return result
  }
}

state参数说明

// state参数格式如下
{
  command: {
    // getMethod('where') 获取所有的where方法,返回结果为[{$method:'where',$param: [{a:1}]}]
    getMethod,
    // getParam({name:'where',index: 0}) 获取第1个where方法的参数,结果为数组形式,例:[{a:1}]
    getParam,
    // setParam({name:'where',index: 0, param: [{a:1}]}) 设置第1个where方法的参数,调用之后where方法实际形式为:where({a:1})
    setParam
  },
  auth: {
    uid, // 用户ID,如果未获取或者获取失败uid值为null
    role, // 通过uni-id获取的用户角色,需要使用1.1.9以上版本的uni-id,如果未获取或者获取失败role值为[]
    permission // 通过uni-id获取的用户权限,需要使用1.1.9以上版本的uni-id,如果未获取或者获取失败permission值为[],注意登录时传入needPermission才可以获取permission,请参考 https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=rbac
  },
  // 事务对象,如果需要用到事务可以在action的before内使用state.transaction = await db.startTransaction()传入
  transaction,
  // 更新或新增的数据
  newData,
  // 访问的集合
  collection,
  // 操作类型,可能的值'read'、'create'、'update'、'delete'
  type
}

JQL Cache Redis

适用场景
需要频繁访问,但不会频繁修改的数据。比如更新不频繁的首页列表、banner、热搜、top排行等公共数据。
这些查询请求频次高,如果每次都去MongoDB读取,又慢又贵,还可能超时或超并发。

配置方法
在uniCloud/cloudfunction/common/uni-config-center下创建uni-jql-cache-redis.json文件
uni-jql-cache-redis.json文件是一个数组,其中每一项是一个缓存配置。示例内容如下

[{
    "id": "test-get", // 缓存id,不可重复,必填
    "jql": "db.collection('test').limit(10).get()", // 要缓存的数据库指令,必填
    "expiresIn": 3600 // 缓存有效期,单位秒,非必填 如果不填,就意味着不会根据时间失效
}]

由于需要将查询语句放在json文件内,而json内字符串又需要引号包围,所以对于查询语句内使用双引号的场景需转义后再放到配置内。
// jql
db.collection('book').where('author=="caoxueqin"').get()
// 转义后
"db.collection('book').where('author==\"caoxueqin\"').get()"

联表查询

const book = db.collection('book').limit(10).getTemp()
const author = db.collection('author').getTemp()
db.collection(book, author).get()

uni-jql-cache-redis.json缓存配置如下

[{
    "id": "book-author-lookup",
    "jql": "const book = db.collection('book').limit(10).getTemp();const author = db.collection('author').getTemp();db.collection(book, author).get()",
    "expiresIn": 3600
}]

删除redis缓存
数据变更时,应该主动删除Redis中的缓存。
const redis = uniCloud.redis()
const id = 'banner' // 这个id就是uni-jql-cache-redis.json中配置的id
const cacheKey = unicloud:jql-cache:${id}:string
await redis.del(cacheKey)

注意事项

  • 仅可缓存查询的JQL指令,增删改JQL指令无效
  • 不可缓存使用了db.getCloudEnv()或$cloudEnv_开头的云端环境变量的查询
  • 不可缓存使用了action的查询
  • 启用JQL扩展库的云函数/云对象同时也要启用Redis扩展库才可以正常使用缓存功能,否则依然从数据库查询
  • 配置后或设置缓存失效后,开发者可自己请求一次JQL查询,让cache生效。避免用户第一次访问时仍然访问MongoDB
    在查询语句内如果有字符串使用引号,请务必保证单双引号和配置的缓存一致
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容