Backend Function(三):独立接口 - SQL 复杂查询
上一篇讲了后验函数。本篇是 Backend Function 系列的最后一篇,介绍如何在 Backend Function 中执行自定义 SQL。
前端调用 SQL 适合做数据展示类的报表查询,但有些场景需要在后端处理——比如批量更新数据、把 SQL 结果作为后续业务逻辑的输入、在事务中混合使用 SQL 和数据集 API。
本节你将学到
- Backend Function 中调用 SQL 的方法
- 前端 SDK 和 BFF 中 SQL 返回值的区别
- 在事务中使用 SQL
- SQL 结果与数据集 API 配合使用
需求
在 Backend Function 中使用自定义 SQL,覆盖这些场景:
- 复杂报表统计(多表关联、分组聚合)
- 批量数据更新(按条件批量修改状态)
- 特殊查询(使用数据库特定函数)
- SQL 结果 + 数据集 API 配合使用
核心概念
前端 vs Backend Function 调用 SQL
| 特性 | 前端 SDK | Backend Function |
|---|---|---|
| 调用方式 | client.sql.execute() | context.client.sql.execute() |
| 返回结构 | { execSuccess, execResult } | 直接返回数组 |
| 错误处理 | 检查 execSuccess | try-catch 捕获异常 |
| 用途 | 查询数据展示 | 业务逻辑处理 |
实现步骤
步骤 1:准备自定义 SQL
在 Lovrabet 平台上创建自定义 SQL:
SQL 名称:getUserStats
SELECT
u.id,
u.username,
u.email,
COUNT(o.id) as order_count,
COALESCE(SUM(o.total_amount), 0) as total_amount
FROM dataset_users u
LEFT JOIN dataset_orders o ON u.id = o.user_id
WHERE u.status = 'active'
<if test="userId">
AND u.id = #{userId}
</if>
GROUP BY u.id, u.username, u.email
步骤 2:在 Backend Function 中调用 SQL
查询统计数据
/**
* 获取用户统计信息 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getUserStats
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
* 说明:appCode 为应用编码,创建后可在平台上配置此独立端点
*
* [HTTP 请求体参数]
* { "userId": "用户ID(可选)" }
*
* [返回数据结构]
* ENDPOINT: 返回业务数据对象
* {
* "success": true,
* "data": [...], // 用户统计数组
* "total": 10 // 总数
* }
*
* @param {Object} params - 请求参数
* @param {string} params.userId - 用户ID(可选)
* @param {Object} context - 执行上下文(平台自动注入,调用时无需传递)
* @param {Object} context.userInfo - 当前用户信息
* @param {Object} context.client - 数据库操作入口
* @returns {Object} 返回统计结果
*/
export default async function getUserStats(params, context) {
const { userId } = params;
// 直接返回数组(BFF 中 sql.execute 直接返回结果数组)
const rows = await context.client.sql.execute({
sqlCode: "getUserStats",
params: { userId },
});
// 如果没有指定 userId,返回所有用户的统计
// 如果指定了 userId,返回单个用户的统计
if (userId) {
// 单个用户:返回对象或 null
return rows.length > 0 ? rows[0] : null;
} else {
// 所有用户:返回数组
return {
success: true,
data: rows,
total: rows.length,
};
}
}
前端调用:
import { lovrabetClient } from "./api/client";
// 获取所有用户统计
const result = await lovrabetClient.bff.execute({
scriptName: "getUserStats",
params: {},
});
console.log(result.data); // 用户统计数组
// 获取单个用户统计
const userStats = await lovrabetClient.bff.execute({
scriptName: "getUserStats",
params: { userId: "123" },
});
console.log(userStats.orderCount); // 订单数量
批量更新数据
SQL 名称:batchUpdateStatus
UPDATE dataset_orders
SET status = #{newStatus},
update_time = NOW()
WHERE status = #{oldStatus}
<if test="startDate">
AND create_time >= #{startDate}
</if>
<if test="endDate">
AND create_time <= #{endDate}
</if>
Backend Function:
/**
* 批量更新订单状态 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/batchUpdateOrderStatus
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {
* "oldStatus": "原状态",
* "newStatus": "新状态",
* "startDate": "开始日期(可选)",
* "endDate": "结束日期(可选)"
* }
*
* [返回数据结构]
* { "success": true, "message": "...", "affectedRows": 10 }
*
* @param {Object} params - 请求参数
* @param {string} params.oldStatus - 原状态
* @param {string} params.newStatus - 新状态
* @param {string} params.startDate - 开始日期(可选)
* @param {string} params.endDate - 结束日期(可选)
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回更新结果
*/
export default async function batchUpdateOrderStatus(params, context) {
const { oldStatus, newStatus, startDate, endDate } = params;
// 参数校验
if (!oldStatus || !newStatus) {
throw new Error("oldStatus 和 newStatus 不能为空");
}
if (oldStatus === newStatus) {
throw new Error("新状态不能与原状态相同");
}
// 执行批量更新 SQL
const affectedRows = await context.client.sql.execute({
sqlCode: "batchUpdateStatus",
params: {
oldStatus,
newStatus,
startDate,
endDate,
},
});
// 返回更新结果(affectedRows 通常是一个数组,包含受影响的行数)
const rowCount = affectedRows[0]?.affected_rows || 0;
return {
success: true,
message: `成功更新 ${rowCount} 条订单`,
affectedRows: rowCount,
};
}
SQL 结果 + 数据集操作
SQL 名称:getPendingOrders
SELECT
o.id,
o.order_no,
o.total_amount,
c.customer_name,
c.phone
FROM dataset_orders o
JOIN dataset_customers c ON o.customer_id = c.id
WHERE o.status = 'pending'
ORDER BY o.create_time DESC
LIMIT 50
Backend Function:
/**
* 获取待处理订单并计算优惠 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getPendingOrdersWithDiscount
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {}
*
* [返回数据结构]
* { "success": true, "orders": [...], "summary": {...} }
*
* @param {Object} params - 请求参数
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回订单列表及汇总
*/
export default async function getPendingOrdersWithDiscount(params, context) {
// 数据集编码映射表
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 数据集: 客户 | 数据表: customers(需替换为实际32位编码)
};
const models = context.client.models;
// 1. 调用自定义 SQL 获取待处理订单
const orders = await context.client.sql.execute({
sqlCode: "getPendingOrders",
params: {},
});
// 收集所有客户ID,批量查询
const customerIds = [
...new Set(orders.map((o) => o.customer_id).filter(Boolean)),
];
// 批量查询客户信息
let customerMap = new Map();
if (customerIds.length > 0) {
const customers = await models[TABLES.customers].filter({
where: { id: { $in: customerIds } },
});
customerMap = new Map(customers.tableData.map((c) => [c.id, c]));
}
// 2. 处理每笔订单,计算折扣
const result = orders.map((order) => {
// 获取客户的会员等级(从批量查询结果中获取)
const customer = customerMap.get(order.customer_id);
let discount = 0;
if (customer?.level === "VIP") {
discount = 0.1; // VIP 9折
} else if (customer?.level === "SVIP") {
discount = 0.15; // SVIP 85折
}
const discountAmount = order.total_amount * discount;
const finalAmount = order.total_amount - discountAmount;
return {
...order,
discount: discount * 100, // 转换为百分比
discountAmount,
finalAmount,
};
});
// 3. 计算汇总信息
const totalOriginal = result.reduce((sum, o) => sum + o.total_amount, 0);
const totalDiscount = result.reduce((sum, o) => sum + o.discountAmount, 0);
const totalFinal = result.reduce((sum, o) => sum + o.finalAmount, 0);
return {
success: true,
orders: result,
summary: {
totalOriginal,
totalDiscount,
totalFinal,
},
};
}
在事务中使用 SQL
SQL 名称:decreaseStock
UPDATE dataset_products
SET stock = stock - #{quantity},
update_time = NOW(),
version = version + 1
WHERE id = #{productId}
AND stock >= #{quantity}
AND version = #{version}
Backend Function:
/**
* 提交订单(扣减库存)- Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/submitOrderWithStock
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* {
* "customerName": "客户名称",
* "customerPhone": "客户电话",
* "items": [
* { "productId": "商品ID", "quantity": 1, "price": 100 }
* ]
* }
*
* [返回数据结构]
* { "success": true, "message": "...", "orderId": "...", "totalAmount": 0 }
*
* @param {Object} params - 请求参数
* @param {string} params.customerName - 客户名称
* @param {string} params.customerPhone - 客户电话
* @param {Array} params.items - 订单明细
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Object} 返回提交结果
*/
export default async function submitOrderWithStock(params, context) {
const { items } = params; // items = [{ productId, quantity, price }, ...]
// 数据集编码映射表
const TABLES = {
orders: "dataset_XXXXXXXXXX", // 数据集: 订单 | 数据表: orders(需替换为实际32位编码)
orderItems: "dataset_YYYYYYYYYY", // 数据集: 订单明细 | 数据表: order_items
};
const models = context.client.models;
// 使用事务确保库存扣减和订单创建的原子性
const result = await context.client.db.transaction(async (tx) => {
let totalAmount = 0;
// 1. 检查库存并扣减(使用 SQL)
for (const item of items) {
// 获取当前库存和版本号
const products = await tx.sql.execute({
sqlCode: "getProductStock",
params: { productId: item.productId },
});
if (products.length === 0) {
throw new Error(`商品 ${item.productId} 不存在`);
}
const product = products[0];
const currentStock = product.stock;
const currentVersion = product.version;
if (currentStock < item.quantity) {
throw new Error(
`商品 ${product.name} 库存不足,当前库存:${currentStock}`
);
}
// 扣减库存(使用 version 实现乐观锁)
await tx.sql.execute({
sqlCode: "decreaseStock",
params: {
productId: item.productId,
quantity: item.quantity,
version: currentVersion,
},
});
totalAmount += product.price * item.quantity;
}
// 2. 创建订单(使用 models 而非 tx.models)
const orderId = await models[TABLES.orders].create({
customerName: params.customerName,
customerPhone: params.customerPhone,
totalAmount,
status: "paid",
});
// 3. 创建订单明细
for (const item of items) {
await models[TABLES.orderItems].create({
orderId: orderId,
productId: item.productId,
quantity: item.quantity,
price: item.price,
subtotal: item.price * item.quantity,
});
}
return { orderId, totalAmount };
});
return {
success: true,
message: "订单提交成功",
...result,
};
}
知识点整理
Backend Function 中 SQL 执行语法
// 直接返回数组,无需检查 execSuccess
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {
/* 参数 */
},
});
// rows 就是结果数组
rows.forEach((row) => console.log(row));
与前端 SDK 的区别:
| 环境 | 调用方式 | 返回值 | 数据获取 |
|---|---|---|---|
| 前端 | client.sql.execute() | { execSuccess, execResult } | result.execResult |
| BFF | context.client.sql.execute() | 直接返回数组 | 直接使用 rows |
// ❌ 错误:BFF 中没有 execResult
const result = await context.client.sql.execute({ sqlCode: "xxx" });
const rows = result.execResult; // undefined!
// ✅ 正确:BFF 返回值直接就是数组
const rows = await context.client.sql.execute({ sqlCode: "xxx" });
const firstRow = rows[0];
在事务中调用 SQL
const TABLES = {
orders: "dataset_XXXXXXXXXX", // 数据集: 订单 | 数据表: orders
};
const models = context.client.models;
await context.client.db.transaction(async (tx) => {
// 使用 tx.sql.execute() 确保在事务中执行
await tx.sql.execute({
sqlCode: "yourSqlCode",
params: {
/* ... */
},
});
// 使用 models[TABLES.xxx] 进行数据集操作
await models[TABLES.orders].create({
/* ... */
});
});
SQL 参数传递
// SQL 中使用 #{paramName}
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {
userId: 123,
startDate: "2024-01-01",
status: "active",
},
});
几个好的做法
参数校验
export default async function myFunction(params, context) {
const { userId, startDate, endDate } = params;
// 必填参数校验
if (!userId) {
throw new Error("userId 不能为空");
}
// 业务规则校验
if (startDate && endDate && startDate > endDate) {
throw new Error("开始日期不能大于结束日期");
}
// 业务逻辑...
}
错误处理
export default async function myFunction(params, context) {
try {
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {},
});
return {
success: true,
data: rows,
};
} catch (error) {
// 记录日志
console.error("SQL 执行失败:", error);
return {
success: false,
message: error.message || "查询失败",
};
}
}
添加 JSDoc 注释
给 Backend Function 加完整的 JSDoc 注释,方便后续维护:
/**
* 获取用户统计信息 - Backend Function 独立端点
*
* [接口路径] POST /api/{appCode}/endpoint/getUserStats
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*
* [HTTP 请求体参数]
* { "userId": "用户ID(可选)" }
*
* [返回数据结构]
* { "success": true, "data": [...], "total": 10 }
*
* @param {Object} params - 请求参数
* @param {string} params.userId - 用户ID(可选)
* @param {Object} context - 执行上下文(平台自动注入)
* @returns {Promise<Object>} 返回统计结果
*/
export default async function getUserStats(params, context) {
// ...
}
常见问题
Q: 如何调试 Backend Function 中的 SQL?
A: 使用 console.log 输出调试信息:
/**
* Backend Function 示例 - 调试日志
*
* [接口路径] POST /api/{appCode}/endpoint/myFunction
*
* [平台配置] https://app.lovrabet.com/app/{appCode}/data/backend-function
*/
export default async function myFunction(params, context) {
console.log("输入参数:", JSON.stringify(params));
console.log("当前用户:", context.userInfo?.username);
const rows = await context.client.sql.execute({
sqlCode: "yourSqlCode",
params: {},
});
console.log("查询结果行数:", rows.length);
return rows;
}
然后查看平台的 Backend Function 日志。
Q: SQL 执行超时怎么办?
A:
- 优化 SQL 查询,添加索引
- 使用
LIMIT限制返回行数 - 减少复杂的多表关联
- 检查是否存在死锁
Q: 如何获取受影响的行数?
A:这取决于具体的数据库:
const rows = await context.client.sql.execute({
sqlCode: "batchUpdate",
params: {},
});
// MySQL: 返回 { affected_rows: 数字 }
const affectedRows = rows[0]?.affected_rows || 0;
// PostgreSQL: 使用 RETURNING
// SQL: UPDATE ... RETURNING *
// rows 直接是更新的行数据
const affectedRows = rows.length;
Q: 事务中的 SQL 失败会回滚吗?
A:是的,事务中任何异常都会自动回滚:
await context.client.db.transaction(async (tx) => {
// 执行 SQL
await tx.sql.execute({ sqlCode: "update1" });
// 如果这里抛出异常,上面的 update1 也会回滚
if (someError) {
throw new Error("操作失败");
}
// 只有全部成功,事务才会提交
await tx.sql.execute({ sqlCode: "update2" });
});
本节小结
恭喜你完成了所有开发教程!关键知识点:
| 知识点 | 说明 |
|---|---|
context.client.sql.execute() | BFF 中执行 SQL |
| 直接返回数组 | BFF 中 SQL 返回值不同于前端 |
tx.sql.execute() | 事务中执行 SQL |
| SQL + 数据集 API | 两种方式配合使用 |
前端 vs BFF 的区别
// 前端 SDK:返回 { execSuccess, execResult }
const data = await client.sql.execute({ sqlCode: "xxx" });
if (data.execSuccess) { data.execResult.forEach(...); }
// Backend Function:直接返回数组
const rows = await context.client.sql.execute({ sqlCode: "xxx" });
rows.forEach(...);
🎉 恭喜完成开发阶段!
你已经掌握了 Lovrabet 开发的所有核心技能。接下来是构建与部署,把你的成果上线!
相关阅读
核心文档
- BFF API 参考 — Backend Function 完整使用指南
- SQL API 使用指南 — 自定义 SQL 查询完整说明
- 语法糖 — sqlSafe 等便捷函数
进阶主题
- 销售报表:自定义 SQL — 复杂统计使用 SQL 查询
- 多表关联查询 — filter 多表查询和 SQL 对比
- 主子表单:事务处理 — BF 独立端点处理复杂业务
难度等级:L2 | 预计耗时:30 分钟