在 uniCloud 的世界里,与数据库交互主要通过两条路径:uniCloud.database()
和 uniCloud.databaseForJQL()
。这二者的选择并非小事,而是关乎应用架构、安全性和开发效率的重大决策。它们的核心差异可以精辟地总结为三点:
- 权限 (Permissions) : 这是最根本的区别。
databaseForJQL()
与 uniCloud 的灵魂------DB Schema 深度绑定,会自动对每一次读写操作进行严格的权限校验 。而database()
在服务端使用时,更像是通往原生 MongoDB 的一个"裸连接",它不会自动执行 DB Schema 中定义的权限规则。 - 返回结构 (Return Structure) :
databaseForJQL()
致力于提供统一且简化的数据结构 。无论在客户端还是服务端,它都返回扁平的{ data: [...] }
结构。 - 查询语句 (Query Syntax) :
databaseForJQL()
采用 JQL (JavaScript Query Language),这是一种为前端开发者设计的、更简洁、更直观的查询语言。
本指南将围绕这三大差异,为您提供一份详尽的、专家级的分析。
1. 场景选择:公共数据 vs 敏感数据
在深入 API 细节之前,我们首先要理解一个关键的架构决策:数据请求应该由前端发起,还是由后端(云函数)发起? 对于不同的数据类型,答案截然不同。
1.1 公共数据的最佳实践:ClientDB 直连
对于所有人都可以访问的公共数据(如新闻列表、产品目录),最佳实践是 使用 ClientDB 从前端直接请求,而不是通过云函数中转。
这是最能体现 uniCloud 架构优势的方式,原因如下:
- 性能最高:请求路径最短,用户获取数据的延迟最低。
- 成本最低:您只需要支付数据库的读取费用,完全节省了云函数的计算资源费用。
- 开发最简单:您不需要编写、上传和维护一个仅用于数据中转的云函数。
深入剖析:为什么 ClientDB 比云函数更快?
这个性能优势源于两者截然不同的请求处理路径。
- 云函数的请求路径(更长):
当客户端通过云函数获取数据时,流程如下:
客户端 → uniCloud 网关 → 云函数容器 → 云函数执行(包含冷启动耗时) → 数据库 → 云函数接收 → 客户端接收
这个路径中, "云函数容器" 和 "云函数执行" 是额外的步骤,尤其是云函数的冷启动会显著增加首次请求的耗时。
- ClientDB 的请求路径(更短):
当客户端使用 ClientDB 直接请求数据时,流程被大大简化:
客户端 → uniCloud 网关 (直接校验 DB Schema 权限) → 数据库 → 客户端接收
路径对比一目了然 :ClientDB 的方式跳过了整个"启动并执行云函数"的中间环节 。uniCloud 网关直接根据您在 DB Schema 中定义的 permission
规则来判断请求是否合法,如果合法,就直接从数据库获取数据返回给客户端。因为中间环节更少,所以它的网络延迟和处理耗时自然是最低的。
如何实现?
实现 ClientDB 直连的关键,在于正确配置 DB Schema 权限。
- 找到对应数据表的
schema.json
文件。 - 在
permission
对象中,将"read"
权限设置为true
。 - 严格限制写权限,如将
"create"
,"update"
,"delete"
设置为false
。
示例 articles.schema.json
:
json
{
"bsonType": "object",
"permission": {
"read": true, // 允许任何人读取
"create": false, // 禁止从客户端创建
"update": false, // 禁止从客户端更新
"delete": false // 禁止从客户端删除
},
"properties": { /* ... */ }
}
进阶优化:结合 Redis 缓存实现极致性能
对于访问极其频繁的公共数据(如首页新闻、热门商品),即使是直连数据库,也可能造成不必要的数据库负载和费用。此时,可以引入 Redis 缓存来进一步优化。
uniCloud 的 JQL 缓存机制非常强大,它与 JQL 查询语句本身绑定,而不是与某个 API 调用绑定。这意味着您无需修改任何前端代码,仅通过服务端配置即可启用缓存。
实现步骤:
-
权限不变 (DB Schema) :继续沿用上述将
read
权限设为true
的配置。这是安全的前提。 -
配置 JQL 查询缓存 (
uni-config-center
) :在uni-config-center
公共模块中,找到 JQL 缓存的配置文件(通常是uni-cloud/jql-cache.json
),添加缓存规则。
json
// 在 cloudfunctions/common/uni-config-center/uni-jql-cache-redis.json 中配置
[{
"id": "test-get", // 缓存id,不可重复,必填
"jql": "db.collection('test').limit(10).get()", // 要缓存的数据库指令,必填
"expiresIn": 3600 // 缓存有效期,单位秒,非必填
}]
JQL Cache Redis
会将缓存配置对应的查询结果缓存到key为unicloud:jql-cache:${id}:string
的redis缓存内。可以在uniCloud web控制台的redis视图中找到。
- 前端代码不变 :前端的查询代码完全不需要改动。当 uniCloud 网关收到一个 JQL 查询请求时,它会自动检查此查询是否命中了
jql-cache.json
中的规则。如果命中且缓存未过期,数据将直接从 Redis 返回,完全不会触达 MongoDB 数据库。
通过 ClientDB 直连
+ DB Schema 公开读权限
+ uni-config-center JQL 缓存
的组合拳,可以将公共数据的读取性能和成本效益发挥到极致。
1.2 敏感数据的必要选择:云函数/云对象
当业务逻辑敏感(如支付、抽奖、修改用户积分)或需要操作受保护数据时,必须在服务端(云函数/云对象)中执行。这是为了确保核心业务逻辑的安全,防止客户端绕过校验。
2. API 详解:客户端 (ClientDB)
2.1. uniCloud.database()
:传统方式
这是早期的 API,其查询结果被包裹在一个额外的 result
对象中 (res.result.data
),会破坏代码在不同数据来源之间的一致性。
2.2. uniCloud.databaseForJQL()
:现代标准
这是官方强烈推荐 的新标准。它返回扁平的数据结构 (res.data
),与服务端 JQL 的返回结构完全一致,极大增强了代码的可维护性和复用性。
前端代码示例 (获取公共文章):
javascript
// 在你的 Vue/nvue 页面中
const db = uniCloud.databaseForJQL();
async function getPublicArticles() {
try {
// 直接从前端查询,因为 schema 允许,这个请求是安全的
// 如果服务端配置了缓存,此请求可能会从 Redis 返回数据
const res = await db.collection('articles').get();
// res.data 就是你的公共数据
console.log(res.data);
} catch (e) {
console.error(e);
}
}
3. API 详解:服务端 (云函数/云对象)
3.1. uniCloud.database()
:原生 MongoDB 网关 (无自动权限)
在云函数/云对象中,此 API 返回一个原生 MongoDB 驱动实例 ,它不会 自动加载和执行 DB Schema 的 permission
规则。你必须手动负责所有的安全校验,适用于需要执行 JQL 不支持的底层指令的高级场景。
3.2. uniCloud.databaseForJQL()
:现代标准 (内置权限)
在服务端使用 JQL 是官方推荐的做法。它能复用 DB Schema 的权限和数据校验能力 。关键在于必须传递上下文信息,JQL 引擎才能知道是谁在请求,从而正确应用权限规则。
- 云函数中: 传入 event 和 context。
js
// 在云函数 main 方法内
'use strict';
exports.main = async (event, context) => {
// 必须传入 event 和 context
const dbJQL = uniCloud.databaseForJQL({
event,
context
});
//...后续操作
};
- 云对象中: 传入 this.getClientInfo()。
js
// 在云对象的方法内
module.exports = {
myMethod() {
// 必须传入 clientInfo
const dbJQL = uniCloud.databaseForJQL({
clientInfo: this.getClientInfo()
});
//...后续操作
}
}
这个上下文传递的设计并非多此一举。它恰恰是 uniCloud 安全模型强大之处的体现。正是通过 event
、context
或 clientInfo
,服务端 JQL 引擎得以 继承并发起请求的客户端的身份信息 。这使得像 "doc.uid == auth.uid"
这样的权限规则,即使是在云函数中代表用户执行查询,也依然能够被正确地、安全地评估和执行。
3.3 使用 setUser
进行受控的权限提升
setUser
方法是云函数中的一项"受控的超级能力"。它允许云函数在执行后续数据库操作时,临时扮演成另一个用户或拥有特定角色/权限的虚拟身份 。
这个功能对于实现安全的管理操作至关重要。例如,一个普通用户不能修改自己的角色为"管理员",uni-id-users
表的 update
权限应该对此进行严格限制。但是,一个已有的管理员应当有权限去提升另一个普通用户为管理员。
这个过程可以通过一个名为 promoteUser(userId)
的云函数实现:
- 首先,为这个云函数本身设置调用权限,例如在
package.json
或通过其他方式确保只有管理员角色才能调用。 - 在云函数内部,通过
uniCloud.databaseForJQL({ event, context })
获取的dbJQL
实例,其身份与调用者(管理员)一致。 - 函数内部可以执行更新目标用户角色的操作。为了确保操作的原子性和权限的明确性,可以使用
setUser
来指定操作上下文。例如,dbJQL.setUser({ role: ['admin'] })
可以确保后续操作是以管理员身份执行的,从而能够成功修改uni-id-users
表中其他用户的角色字段。
JavaScript
csharp
// 在一个仅限管理员调用的云函数中
exports.main = async (event, context) => {
const { targetUid } = event;
const dbJQL = uniCloud.databaseForJQL({ event, context });
// 可选:使用 setUser 明确指定操作身份为管理员
// 在某些复杂场景下,这可以使权限逻辑更清晰
dbJQL.setUser({
role: ['admin']
});
// 以管理员身份更新目标用户的角色
await dbJQL.collection('uni-id-users').doc(targetUid).update({
role: ['editor'] // 将目标用户角色更新为 editor
});
return { success: true };
};
4. 释放 JQL 的全部潜能
选择 JQL,意味着选择了一个强大的数据处理生态,包括简化的联表查询和强大的数据聚合分析能力。
示例:按班级统计总分和平均分
rust
const res = await db.collection('score')
.groupBy('class')
.groupField('sum(score) as totalScore, avg(score) as avgScore')
.get();
5. 安全基石:精通 DB Schema 权限
无论使用哪个 API,最终的安全都离不开对 DB Schema 的深刻理解。databaseForJQL()
的巨大优势在于它能自动利用这套体系。
场景描述 | 权限类型 | 权限表达式 |
---|---|---|
数据公开可读 | read |
true |
仅登录用户可读 | read |
"auth.uid != null" |
仅所有者可读写 | read , update |
"doc.uid == auth.uid" |
管理员完全控制 | read , update |
"auth.role.includes('admin')" |
6. 综合与最终建议
黄金法则
综合以上所有分析,我们得出最终的"黄金法则":
优先考虑 ClientDB 直连。只有当业务逻辑必须在服务端执行时,才使用云函数/云对象。在任何情况下,都优先使用 uniCloud.databaseForJQL()
API。将你的时间精力投入到设计和编写 DB Schema 权限规则上,这才是构建一个安全、高效、可维护的 uniCloud 应用的真正基石。