跳到主要内容

6. BFF SQL

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 捕获异常

用途

查询数据展示

业务逻辑处理


实现步骤

AI 辅助开发(推荐)

**为什么 AI 能在 BFF 中帮你调用 SQL?**没有 rabetbase,在服务端执行 SQL = 搭一个后端服务。后端同学选框架(Spring Boot?Laravel?Express?)、配数据库连接池(连接数?超时?)、写 SQL 执行逻辑、加认证中间件、处理参数绑定、错误处理、日志记录... SQL 本身 10 行,周边代码 200 行。还得部署到服务器:买机器、装环境、配 nginx 反向代理、加 SSL。联调时更痛苦:前端调 API 返回的格式 \{ code: 200, data: \{ success: true, result: \[\.\.\.\] \} \} 跟预期的不一样——多包了两层。参数传递前端发 JSON,后端的 SQL 参数绑定用的 ? 占位符,JSON 到 SQL 参数的转换又写了一堆中间代码。想在同一个请求里先查 SQL 再用代码更新数据?事务管理全靠手写:connection\.beginTransaction\(\) → 查 SQL → 处理结果 → UPDATE → commit → rollback → finally 释放连接。有一次忘了 connection\.release\(\),线上跑了一夜连接泄漏,第二天整个系统挂了。一个"查 SQL 再更新",从搭服务到调通用了 3 天。有了 rabetbase:不用搭后端服务。BFF 运行时内置 SQL 执行和数据集 API,同一个事务里可以混用。CLI 的 sql save 保存 SQL,bff new \-\-type ENDPOINT 创建端点脚手架(事务 API 就绪),Skill 指导 AI 区分前端和 BFF 不同的调用方式,以及在事务中正确混用 SQL 和数据集 API。不用搭服务器、不用配连接池、不用猜 API 格式。

在 Claude Code 中输入:

用 rabetbase CLI 帮我创建一个自定义 SQL getUserStats,查询用户统计信息(订单数、总金额),支持按 userId 筛选。然后创建一个 ENDPOINT 类型的 BFF 函数 getUserStats 调用这个 SQL,返回单个用户或全部用户的统计。

AI 会做什么

AI 会使用 rabetbase CLI 自动完成以下工作:

  1. 查询用户和订单数据集结构(rabetbase dataset detail

  2. 创建并保存自定义 SQL(rabetbase sql validate + rabetbase sql save

  3. 创建 BFF 端点脚手架(rabetbase bff new \-\-type ENDPOINT

  4. 编写业务逻辑(SQL 调用 + 结果处理)

  5. 推送到平台生效(rabetbase bff push)完成后前端即可通过 bff\.execute\(\) 调用。下方是 AI 生成的完整代码。


🔧 手动操作(备选)

方式一:手动 CLI

# 创建 SQL 文件
mkdir -p .rabetbase/sql
# 编辑 .rabetbase/sql/getUserStats.sql(参考下方代码)

# 验证并保存
rabetbase sql validate --file .rabetbase/sql/getUserStats.sql --format json
rabetbase sql save --file .rabetbase/sql/getUserStats.sql --db <dbId> --format json

# 创建 BFF 端点
rabetbase bff new --type ENDPOINT --name getUserStats
# 编辑生成的文件(参考下方代码)
rabetbase bff push --yes --type ENDPOINT --name getUserStats


方式二:平台 UI

  1. 创建自定义 SQL:https://app\.lovrabet\.com/app/\{appCode\}/data/custom\-sql

  2. 创建 BFF 独立端点:https://app\.lovrabet\.com/app/\{appCode\}/data/backend\-function


步骤 1:SQL 代码参考

\.rabetbase/sql/getUserStats\.sql

-- @lovrabet sqlName=getUserStats description=用户统计查询
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:BFF 端点代码参考

查询统计数据

/**
* 获取用户统计信息 - 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,
};
}
}


编辑完后推送到平台:

rabetbase bff status --format json
rabetbase bff push --yes --type ENDPOINT --name getUserStats


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本地创建 SQL 文件 \.rabetbase/sql/batchUpdateStatus\.sql

-- @lovrabet sqlName=batchUpdateStatus description=批量更新订单状态
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>


保存并验证 SQL:

rabetbase sql validate --file .rabetbase/sql/batchUpdateStatus.sql --format json
rabetbase sql save --file .rabetbase/sql/batchUpdateStatus.sql --db <dbId> --format json


Backend Function(用 rabetbase bff new \-\-type ENDPOINT \-\-name batchUpdateOrderStatus 创建后编辑):

/**
* 批量更新订单状态 - 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

-- @lovrabet sqlName=getPendingOrders description=获取待处理订单
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


保存并验证 SQL:

rabetbase sql validate --file .rabetbase/sql/batchUpdateStatus.sql --format json
rabetbase sql save --file .rabetbase/sql/batchUpdateStatus.sql --db <dbId> --format json


Backend Function(用 rabetbase bff new \-\-type ENDPOINT \-\-name batchUpdateOrderStatus 创建后编辑):

/**
* 获取待处理订单并计算优惠 - 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

-- @lovrabet sqlName=decreaseStock description=扣减库存(乐观锁)
UPDATE dataset_products
SET stock = stock - #{quantity},
update_time = NOW(),
version = version + 1
WHERE id = #{productId}
AND stock >= #{quantity}
AND version = #{version}


保存并验证 SQL:

rabetbase sql validate --file .rabetbase/sql/batchUpdateStatus.sql --format json
rabetbase sql save --file .rabetbase/sql/batchUpdateStatus.sql --db <dbId> --format json


Backend Function(用 rabetbase bff new \-\-type ENDPOINT \-\-name batchUpdateOrderStatus 创建后编辑):

/**
* 提交订单(扣减库存)- 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(
`商品 <equation>{product.name} 库存不足,当前库存:</equation>{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

  1. 优化 SQL 查询,添加索引

  2. 使用 LIMIT 限制返回行数

  3. 减少复杂的多表关联

  4. 检查是否存在死锁

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 等便捷函数

进阶主题


难度等级:L2 | 预计耗时:30 分钟