自定义 SQL 使用教程
本教程将手把手教你如何在 Lovrabet 平台上配置和使用自定义 SQL 查询,从零开始完成一个完整的数据查询功能。
通过本教程,你将学会:
- 在平台上创建和配置自定义 SQL
- 使用 SDK 调用自定义 SQL
- 处理查询结果和错误
- 实现常见的业务场景
📋 前提条件
在开始之前,请确保:
- ✅ 已注册 Lovrabet 平台账号
- ✅ 已创建应用并获取 AppCode
- ✅ 已安装
@lovrabet/sdk(>= 1.1.19) - ✅ 已获取 AccessKey(服务端)或已登录平台(浏览器)
如果还没有完成上述准备,请先查看 快速开始指南。
第一步:在平台上创建自定义 SQL
1.1 进入 SQL 管理页面
- 登录 Lovrabet 平台
- 选择你的应用
- 点击进入 SQL 管理页面: 【应用配置】 → 【应用资产】 → 【自定义 SQL 管理】
1.2 创建新的 SQL 查询
点击右上角的 "新建 SQL" 按钮,填写以下信息:
基础信息
| 字段 | 说明 | 示例 |
|---|---|---|
| SQL 名称 | 查询的名称(必填) | 查询活跃用户统计 |
| SQL 描述 | 查询的用途说明(可选) | 统计最近7天活跃用户的登录次数和操作次数 |
| SQL 分组 | 便于管理(可选) | 用户统计 |
SQL 语句
在 SQL 编辑器 中输入你的 SQL 语句。
入门示例:
-- 简单查询:查询所有活跃用户
SELECT
id,
name,
email,
created_at
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 100
进阶示例:
-- 复杂统计:查询最近7天活跃用户统计
SELECT
u.id,
u.name,
u.email,
COUNT(DISTINCT l.id) AS login_count,
COUNT(DISTINCT a.id) AS action_count,
MAX(l.login_time) AS last_login_time
FROM users u
LEFT JOIN user_logins l ON u.id = l.user_id
AND l.login_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
LEFT JOIN user_actions a ON u.id = a.user_id
AND a.action_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
WHERE u.status = 'active'
GROUP BY u.id, u.name, u.email
ORDER BY login_count DESC
LIMIT 100
基础规范:
- ✅ 指定字段名:避免
SELECT *,明确需要的字段 - ✅ 添加注释:使用
-- 注释说明 SQL 用途 - ✅ 使用别名:为表和字段添加有意义的别名(AS)
- ✅ 格式化代码:每个子句单独一行,保持缩进
性能考虑:
- ✅ 添加 LIMIT:限制返回数量,避免一次性返回大量数据
- ✅ WHERE 条件:合理使用索引字段作为筛选条件
- ✅ 避免子查询:尽量使用 JOIN 代替子查询
- ✅ 使用 EXPLAIN:在平台上用 EXPLAIN 分析查询性能
安全建议:
- ✅ 使用参数:动态值使用
#{paramName}而非拼接 - ✅ 权限控制:只查询用户有权限访问的数据
- ❌ 避免删除:不要编写 DELETE 语句(使用 Dataset API)
- ❌ 避免修改:UPDATE 操作需谨慎(建议使用 Dataset API)
常见 SQL 模式:
| 场景 | SQL 关键点 |
|---|---|
| 分页查询 | LIMIT #{limit} OFFSET #{offset} |
| 模糊搜索 | WHERE name LIKE CONCAT('%', #{keyword}, '%') |
| 日期范围 | WHERE date >= #{startDate} AND date <= #{endDate} |
| IN 查询 | WHERE id IN (#{ids}) |
| 聚合统计 | SELECT COUNT(*), SUM(amount), AVG(score) ... |
| 分组统计 | GROUP BY ... HAVING COUNT(*) > 10 |
| 排序 | ORDER BY create_time DESC, name ASC |
1.3 配置 SQL 参数(可选但推荐)
如果你的 SQL 需要动态参数,可以使用参数化查询:
SQL 语句:
SELECT
u.id,
u.name,
COUNT(o.id) as order_count,
SUM(o.amount) as total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
AND o.order_date >= #{startDate}
AND o.order_date <= #{endDate}
WHERE u.status = #{userStatus}
GROUP BY u.id, u.name
ORDER BY total_amount DESC
参数配置:
在平台的参数配置区域,定义以下参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
startDate | 日期 | 2025-01-01 | 开始日期 |
endDate | 日期 | 2025-12-31 | 结束日期 |
userStatus | 字符串 | active | 用户状态 |
- 使用
#{paramName}的格式在 SQL 中标记参数 - 参数名只能包含字母、数字和下划线
- 参数名区分大小写
- 推荐使用驼峰命名:
startDate、userId、orderStatus - 参数会自动进行类型转换和 SQL 注入防护
参数类型支持:
| 类型 | SQL 示例 | 调用示例 |
|---|---|---|
| 字符串 | WHERE name = #{userName} | { userName: 'Alice' } |
| 数字 | WHERE age > #{minAge} | { minAge: 18 } |
| 日期 | WHERE created_at >= #{startDate} | { startDate: '2025-01-01' } |
| 布尔 | WHERE is_active = #{active} | { active: true } |
| 数组 | WHERE id IN (#{userIds}) | { userIds: [1, 2, 3] } |
参数化查询的优势:
- ✅ 安全性:自动防止 SQL 注入攻击
- ✅ 灵活性:同一 SQL 可用于不同参数场景
- ✅ 可维护性:参数与 SQL 逻辑分离
- ✅ 类型安全:平台会进行类型检查和转换
示例对比:
-- ❌ 不安全:容易受到 SQL 注入攻击
SELECT * FROM users WHERE id = 123
-- ✅ 安全:使用参数化查询
SELECT * FROM users WHERE id = #{userId}
性能优化:
- 使用具体字段名:避免
SELECT *,明确指定需要的字段 - 添加合适的索引:为 WHERE 和 JOIN 条件中的字段添加索引
- 限制返回数量:使用
LIMIT避免返回过多数据 - 避免子查询:尽量使用 JOIN 代替子查询
可读性:
- 使用别名:为表和字段添加清晰的别名
- 分行书写:每个子句单独一行
- 添加注释:为复杂逻辑添加
-- 注释 - 统一大小写:建议关键字大写,字段名小写
示例:
-- 查询用户订单统计(最近30天)
SELECT
u.id AS user_id,
u.name AS user_name,
u.email,
COUNT(DISTINCT o.id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_amount,
AVG(o.amount) AS avg_amount,
MAX(o.created_at) AS last_order_time
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
AND o.created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND o.status IN ('paid', 'completed')
WHERE u.status = #{userStatus}
AND u.created_at >= #{startDate}
GROUP BY u.id, u.name, u.email
HAVING order_count > #{minOrderCount}
ORDER BY total_amount DESC
LIMIT #{limit};
1.4 测试 SQL
在保存之前,务必测试 SQL 是否能正常执行:
- 点击 "测试运行" 按钮
- 如果有参数,填写测试参数值
- 查看执行结果和耗时
- 确认返回的数据格式正确
测试结果示例:
{
"success": true,
"data": [
{
"id": 1001,
"name": "张三",
"email": "zhangsan@example.com",
"login_count": 15,
"action_count": 127,
"last_login_time": "2025-11-12 10:30:00"
},
{
"id": 1002,
"name": "李四",
"email": "lisi@example.com",
"login_count": 12,
"action_count": 98,
"last_login_time": "2025-11-11 16:45:00"
}
],
"executeTime": "245ms"
}
- ✅ 检查返回的字段名是否符合预期
- ✅ 检查数据类型是否正确
- ✅ 检查执行时间是否在可接受范围内(建议 < 3 秒)
- ✅ 测试边界情况(空结果、大数据量等)
1.5 保存并获取 SQL Code
- 点击 "保存" 按钮
- 保存成功后,系统会生成一个唯一的 SQL Code
- 复制并保存这个 SQL Code,后续调用时需要用到
SQL Code 格式:xxxxx-xxxxx(例如:12345-67890)
第二步:在代码中调用 SQL
2.1 安装和配置 SDK
安装 SDK:
npm install @lovrabet/sdk
配置客户端:
import { LovrabetClient, AuthMode } from "@lovrabet/sdk";
// 方式一:服务端(Node.js)- 使用 AccessKey
const client = new LovrabetClient({
authMode: AuthMode.AccessKey,
accessKey: process.env.LOVRABET_ACCESS_KEY,
appCode: "your-app-code",
env: "production", // 或 'dev'
});
// 方式二:浏览器 - 使用 Cookie(用户已登录平台)
const client = new LovrabetClient({
authMode: AuthMode.Cookie,
appCode: "your-app-code",
});
// 方式三:浏览器 - 使用 Token
const client = new LovrabetClient({
authMode: AuthMode.Token,
token: "your-token",
timestamp: 1678901234567,
appCode: "your-app-code",
});
2.2 执行基础查询
最简单的调用:
// 执行 SQL(不带参数)
const result = await client.sql.execute("12345-67890");
// 检查执行结果
if (result.execSuccess && result.execResult) {
console.log(`查询成功,返回 ${result.execResult.length} 条数据`);
// 处理数据
result.execResult.forEach((row) => {
console.log(row);
});
} else {
console.error("SQL 执行失败");
}
输出示例:
查询成功,返回 2 条数据
{
id: 1001,
name: '张三',
email: 'zhangsan@example.com',
login_count: 15,
action_count: 127,
last_login_time: '2025-11-12 10:30:00'
}
{
id: 1002,
name: '李四',
email: 'lisi@example.com',
login_count: 12,
action_count: 98,
last_login_time: '2025-11-11 16:45:00'
}
2.3 带参数的查询
传递参数:
const result = await client.sql.execute("12345-67890", {
startDate: "2025-01-01",
endDate: "2025-01-31",
userStatus: "active",
});
if (result.execSuccess && result.execResult) {
console.log("查询结果:", result.execResult);
}
动态参数示例:
// 从用户输入获取参数
function getDateRange() {
const today = new Date();
const sevenDaysAgo = new Date(today);
sevenDaysAgo.setDate(today.getDate() - 7);
return {
startDate: sevenDaysAgo.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
};
}
const { startDate, endDate } = getDateRange();
const result = await client.sql.execute("12345-67890", {
startDate,
endDate,
status: "active",
});
2.4 添加类型定义(TypeScript)
定义结果类型:
// 定义查询结果的数据结构
interface ActiveUserStat {
id: number;
name: string;
email: string;
login_count: number;
action_count: number;
last_login_time: string;
}
// 使用泛型获得类型提示
const result = await client.sql.execute<ActiveUserStat>("12345-67890");
if (result.execSuccess && result.execResult) {
result.execResult.forEach((user) => {
// TypeScript 会自动提示字段
console.log(`用户: ${user.name}`);
console.log(`登录次数: ${user.login_count}`);
console.log(`操作次数: ${user.action_count}`);
});
}
第三步:处理查询结果
3.1 基础数据处理
遍历结果:
const result = await client.sql.execute<ActiveUserStat>("12345-67890");
if (result.execSuccess && result.execResult) {
const users = result.execResult;
// 遍历所有数据
users.forEach((user) => {
console.log(`${user.name} - 登录${user.login_count}次`);
});
// 使用 map 转换数据
const userNames = users.map((user) => user.name);
console.log("用户列表:", userNames);
// 过滤数据
const activeUsers = users.filter((user) => user.login_count > 10);
console.log(`活跃用户数: ${activeUsers.length}`);
}
3.2 数据聚合统计
interface UserOrderStat {
user_id: number;
order_count: number;
total_amount: number;
}
const result = await client.sql.execute<UserOrderStat>("12345-67890");
if (result.execSuccess && result.execResult) {
const stats = result.execResult;
// 计算总订单数
const totalOrders = stats.reduce((sum, stat) => sum + stat.order_count, 0);
console.log(`总订单数: ${totalOrders}`);
// 计算总金额
const totalRevenue = stats.reduce((sum, stat) => sum + stat.total_amount, 0);
console.log(`总销售额: ${totalRevenue.toFixed(2)}`);
// 找出最大值
const topUser = stats.reduce((max, stat) =>
stat.total_amount > max.total_amount ? stat : max
);
console.log(`销售冠军: 用户${topUser.user_id}, 金额${topUser.total_amount}`);
// 计算平均值
const avgAmount = totalRevenue / stats.length;
console.log(`平均销售额: ${avgAmount.toFixed(2)}`);
}
3.3 数据转换和格式化
interface RawOrderData {
order_date: string;
product_name: string;
quantity: number;
price: number;
}
const result = await client.sql.execute<RawOrderData>("12345-67890");
if (result.execSuccess && result.execResult) {
// 转换为前端需要的格式
const formattedData = result.execResult.map((order) => ({
日期: order.order_date,
产品: order.product_name,
数量: order.quantity,
单价: `¥${order.price.toFixed(2)}`,
小计: `¥${(order.quantity * order.price).toFixed(2)}`,
}));
console.table(formattedData);
}
输出示例:
┌─────────┬────────────┬────────────┬────────┬─────────┬──────────┐
│ (index) │ 日期 │ 产品 │ 数量 │ 单价 │ 小计 │
├─────────┼────────────┼────────────┼────────┼─────────┼──────────┤
│ 0 │ 2025-01-15 │ iPhone 15 │ 2 │ ¥5999.00│ ¥11998.00│
│ 1 │ 2025-01-16 │ iPad Pro │ 1 │ ¥7999.00│ ¥7999.00 │
└─────────┴────────────┴────────────┴────────┴─────────┴──────────┘
3.4 导出为 CSV
interface ReportData {
customer_name: string;
order_date: string;
product_name: string;
quantity: number;
amount: number;
}
async function exportToCSV(sqlCode: string) {
const result = await client.sql.execute<ReportData>(sqlCode);
if (!result.execSuccess || !result.execResult) {
throw new Error("查询失败");
}
// 生成 CSV 表头
const headers = ["客户名称", "订单日期", "产品名称", "数量", "金额"];
const csvHeader = headers.join(",") + "\n";
// 生成 CSV 行
const csvRows = result.execResult
.map(
(row) =>
`${row.customer_name},${row.order_date},${row.product_name},${row.quantity},${row.amount}`
)
.join("\n");
const csvContent = csvHeader + csvRows;
// 下载文件
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `report_${Date.now()}.csv`;
link.click();
console.log("导出成功");
}
// 使用
await exportToCSV("12345-67890");
第四步:错误处理
4.1 完整的错误处理
import { LovrabetError } from "@lovrabet/sdk";
async function fetchUserStats(sqlCode: string, params?: Record<string, any>) {
try {
// 执行 SQL 查询
const result = await client.sql.execute(sqlCode, params);
// 检查业务逻辑是否成功
if (!result.execSuccess) {
console.error("SQL 执行失败");
return null;
}
// 检查是否有结果
if (!result.execResult || result.execResult.length === 0) {
console.warn("查询结果为空");
return [];
}
// 返回成功结果
return result.execResult;
} catch (error) {
// 处理 HTTP 请求错误
if (error instanceof LovrabetError) {
console.error("API 请求失败:", error.message);
console.error("错误代码:", error.code);
// 根据错误码做不同处理
if (error.code === 401) {
console.error("认证失败,请检查 AccessKey 或重新登录");
} else if (error.code === 404) {
console.error("SQL 不存在,请检查 SQL Code");
} else if (error.code === 500) {
console.error("服务器错误,请稍后重试");
}
} else {
console.error("未知错误:", error);
}
return null;
}
}
// 使用
const stats = await fetchUserStats("12345-67890", { status: "active" });
if (stats) {
console.log("查询成功:", stats);
} else {
console.log("查询失败,请查看错误日志");
}
4.2 处理常见错误
错误类型对照表:
| 错误场景 | 表现 | 解决方案 |
|---|---|---|
| SQL 不存在 | HTTP 404 | 检查 SQL Code 是否正确 |
| 认证失败 | HTTP 401 | 检查 AccessKey 或重新登录 |
| 权限不足 | HTTP 403 | 联系管理员授予权限 |
| SQL 语法错误 | execSuccess: false | 在平台上测试 SQL |
| 参数缺失 | execSuccess: false | 检查参数名和值是否匹配 |
| 数据库错误 | execSuccess: false | 检查表/字段是否存在 |
| 超时 | Network timeout | 优化 SQL 或增加超时时间 |
4.3 用户友好的错误提示
async function showUserStatsWithError(sqlCode: string) {
try {
const result = await client.sql.execute<UserStat>(sqlCode);
if (!result.execSuccess) {
// 业务逻辑错误
showNotification({
type: "error",
title: "查询失败",
message: "SQL 执行出错,请联系管理员或稍后重试",
});
return;
}
if (!result.execResult || result.execResult.length === 0) {
// 空结果
showNotification({
type: "info",
title: "无数据",
message: "当前没有符合条件的数据",
});
return;
}
// 成功
showNotification({
type: "success",
title: "查询成功",
message: `查询到 ${result.execResult.length} 条数据`,
});
displayData(result.execResult);
} catch (error) {
if (error instanceof LovrabetError) {
// HTTP 错误
if (error.code === 401) {
showNotification({
type: "error",
title: "认证失败",
message: "请重新登录后再试",
action: "去登录",
});
} else {
showNotification({
type: "error",
title: "网络错误",
message: "请检查网络连接后重试",
});
}
}
}
}
第五步:实战案例
案例 1:数据统计看板
需求:实现一个显示最近 7 天用户活跃度的看板
1. 在平台创建 SQL:
-- 用户活跃度统计
-- 参数:days - 统计天数
SELECT
DATE(l.login_time) as login_date,
COUNT(DISTINCT l.user_id) as active_users,
COUNT(l.id) as total_logins,
AVG(TIMESTAMPDIFF(MINUTE, l.login_time, l.logout_time)) as avg_duration
FROM user_logins l
WHERE l.login_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
GROUP BY DATE(l.login_time)
ORDER BY login_date DESC
2. 实现前端代码:
interface DailyActivity {
login_date: string;
active_users: number;
total_logins: number;
avg_duration: number;
}
async function loadActivityDashboard() {
const result = await client.sql.execute<DailyActivity>("12345-67890", {
days: 7,
});
if (!result.execSuccess || !result.execResult) {
console.error("加载失败");
return;
}
const data = result.execResult;
// 渲染图表
renderChart({
labels: data.map((d) => d.login_date),
datasets: [
{
label: "活跃用户数",
data: data.map((d) => d.active_users),
borderColor: "rgb(75, 192, 192)",
},
{
label: "登录次数",
data: data.map((d) => d.total_logins),
borderColor: "rgb(255, 99, 132)",
},
],
});
// 显示统计
const totalUsers = data.reduce((sum, d) => sum + d.active_users, 0);
const avgDuration =
data.reduce((sum, d) => sum + d.avg_duration, 0) / data.length;
console.log(`7天内活跃用户总数: ${totalUsers}`);
console.log(`平均在线时长: ${avgDuration.toFixed(1)} 分钟`);
}
案例 2:订单报表生成
需求:生成指定日期范围的销售报表
1. 平台 SQL:
-- 销售订单报表
-- 参数:startDate, endDate - 日期范围
-- 参数:status - 订单状态('all' 表示所有状态)
SELECT
o.order_date,
o.order_no,
c.customer_name,
p.product_name,
o.quantity,
o.unit_price,
(o.quantity * o.unit_price) AS subtotal,
o.status
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
WHERE o.order_date >= #{startDate}
AND o.order_date <= #{endDate}
AND (#{status} = 'all' OR o.status = #{status})
ORDER BY o.order_date DESC, o.order_no
2. 实现导出功能:
interface OrderReport {
order_date: string;
order_no: string;
customer_name: string;
product_name: string;
quantity: number;
unit_price: number;
subtotal: number;
status: string;
}
async function generateSalesReport(
startDate: string,
endDate: string,
status: string = "all"
) {
// 查询数据
const result = await client.sql.execute<OrderReport>("12345-67890", {
startDate,
endDate,
status,
});
if (!result.execSuccess || !result.execResult) {
throw new Error("查询失败");
}
const orders = result.execResult;
// 计算汇总
const summary = {
totalOrders: orders.length,
totalQuantity: orders.reduce((sum, o) => sum + o.quantity, 0),
totalAmount: orders.reduce((sum, o) => sum + o.subtotal, 0),
avgOrderAmount: 0,
};
summary.avgOrderAmount = summary.totalAmount / summary.totalOrders;
// 生成 Excel 数据
const excelData = [
// 表头
["日期", "订单号", "客户", "产品", "数量", "单价", "小计", "状态"],
// 数据行
...orders.map((o) => [
o.order_date,
o.order_no,
o.customer_name,
o.product_name,
o.quantity,
o.unit_price,
o.subtotal,
o.status,
]),
// 汇总行
[],
["汇总", "", "", "", summary.totalQuantity, "", summary.totalAmount, ""],
["订单数", summary.totalOrders],
["平均金额", summary.avgOrderAmount.toFixed(2)],
];
// 导出(使用 SheetJS 等库)
exportToExcel(excelData, `销售报表_${startDate}_${endDate}.xlsx`);
return summary;
}
// 使用
const summary = await generateSalesReport(
"2025-01-01",
"2025-01-31",
"completed"
);
console.log("报表生成成功:", summary);
案例 3:实时搜索建议
需求:用户输入时实时显示搜索建议
1. 平台 SQL:
-- 用户搜索建议
-- 参数:keyword - 搜索关键词
SELECT
id,
name,
email,
avatar,
department
FROM users
WHERE status = 'active'
AND (
name LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%')
)
ORDER BY
CASE
WHEN name LIKE CONCAT(#{keyword}, '%') THEN 1 -- 名称前缀匹配优先
WHEN email LIKE CONCAT(#{keyword}, '%') THEN 2 -- 邮箱前缀匹配次之
ELSE 3 -- 其他模糊匹配
END,
name
LIMIT 10
2. 实现搜索组件:
import { debounce } from "lodash";
interface UserSuggestion {
id: number;
name: string;
email: string;
avatar: string;
department: string;
}
// 防抖搜索函数
const searchUsers = debounce(
async (keyword: string, callback: (users: UserSuggestion[]) => void) => {
if (keyword.length < 2) {
callback([]);
return;
}
try {
const result = await client.sql.execute<UserSuggestion>(
"12345-67890",
{ keyword }
);
if (result.execSuccess && result.execResult) {
callback(result.execResult);
} else {
callback([]);
}
} catch (error) {
console.error("搜索失败:", error);
callback([]);
}
},
300
); // 300ms 防抖
// React 组件示例
function UserSearchInput() {
const [suggestions, setSuggestions] = useState<UserSuggestion[]>([]);
const [loading, setLoading] = useState(false);
const handleSearch = (keyword: string) => {
setLoading(true);
searchUsers(keyword, (users) => {
setSuggestions(users);
setLoading(false);
});
};
return (
<div className="search-container">
<input
type="text"
placeholder="搜索用户..."
onChange={(e) => handleSearch(e.target.value)}
/>
{loading && <div>搜索中...</div>}
{suggestions.length > 0 && (
<ul className="suggestions">
{suggestions.map((user) => (
<li key={user.id}>
<img src={user.avatar} alt={user.name} />
<div>
<div className="name">{user.name}</div>
<div className="email">{user.email}</div>
<div className="department">{user.department}</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}
第六步:性能优化
6.1 使用缓存
class SQLCache {
private cache = new Map<string, { data: any; expiry: number }>();
private client: LovrabetClient;
constructor(client: LovrabetClient) {
this.client = client;
}
async execute<T>(
sqlCode: string,
params?: Record<string, any>,
cacheDuration: number = 5 * 60 * 1000 // 默认 5 分钟
) {
const cacheKey = `${sqlCode}:${JSON.stringify(params)}`;
const cached = this.cache.get(cacheKey);
// 缓存命中且未过期
if (cached && cached.expiry > Date.now()) {
console.log("从缓存读取数据");
return cached.data as T[];
}
// 执行查询
const result = await this.client.sql.execute<T>(sqlCode, params);
if (result.execSuccess && result.execResult) {
// 存入缓存
this.cache.set(cacheKey, {
data: result.execResult,
expiry: Date.now() + cacheDuration,
});
return result.execResult;
}
return null;
}
// 清除缓存
clear(sqlCode?: string) {
if (sqlCode) {
// 清除特定 SQL 的缓存
for (const key of this.cache.keys()) {
if (key.startsWith(sqlCode)) {
this.cache.delete(key);
}
}
} else {
// 清除所有缓存
this.cache.clear();
}
}
}
// 使用
const cache = new SQLCache(client);
// 第一次查询(从服务器)
const data1 = await cache.execute<UserStat>("12345-67890", {
status: "active",
});
// 第二次查询(从缓存)
const data2 = await cache.execute<UserStat>("12345-67890", {
status: "active",
});
// 清除缓存
cache.clear("12345-67890");
6.2 并发查询
// 多个独立查询可以并发执行
async function loadDashboardData() {
const [userStats, orderStats, revenueStats] = await Promise.all([
client.sql.execute<UserStat>("sql-code-1"),
client.sql.execute<OrderStat>("sql-code-2"),
client.sql.execute<RevenueStat>("sql-code-3"),
]);
// 处理各个结果
if (userStats.execSuccess && userStats.execResult) {
displayUserStats(userStats.execResult);
}
if (orderStats.execSuccess && orderStats.execResult) {
displayOrderStats(orderStats.execResult);
}
if (revenueStats.execSuccess && revenueStats.execResult) {
displayRevenueStats(revenueStats.execResult);
}
}
6.3 分页查询
interface PagedResult<T> {
data: T[];
total: number;
hasMore: boolean;
}
async function fetchPagedData<T>(
sqlCode: string,
page: number,
pageSize: number
): Promise<PagedResult<T>> {
const result = await client.sql.execute<T>(sqlCode, {
offset: (page - 1) * pageSize,
limit: pageSize,
});
if (!result.execSuccess || !result.execResult) {
return { data: [], total: 0, hasMore: false };
}
const data = result.execResult;
const hasMore = data.length === pageSize;
return {
data,
total: data.length,
hasMore,
};
}
// 使用
let currentPage = 1;
const pageSize = 20;
async function loadNextPage() {
const result = await fetchPagedData<UserData>(
"12345-67890",
currentPage,
pageSize
);
displayData(result.data);
if (result.hasMore) {
currentPage++;
console.log("还有更多数据");
} else {
console.log("已加载所有数据");
}
}
常见问题
Q1: SQL Code 在哪里查看?
答:在平台的 自定义 SQL 管理页面,每个 SQL 都有一个唯一的 Code,格式为 xxxxx-xxxxx。你可以:
- 在 SQL 列表中查看
- 点击 SQL 详情页面复制
- 在测试运行时查看
Q2: 如何调试 SQL 执行问题?
答:按以下步骤排查:
-
在平台上测试
- 先在平台的 SQL 管理页面点击"测试运行"
- 确认 SQL 能正常执行
-
启用 SDK 调试模式
const client = new LovrabetClient({
// ...其他配置
debug: true, // 启用调试日志
}); -
检查返回结果
const result = await client.sql.execute("12345-67890");
console.log("完整结果:", JSON.stringify(result, null, 2)); -
查看网络请求
- 打开浏览器 DevTools → Network 面板
- 查看请求和响应详情
Q3: execSuccess 为 false 是什么原因?
答:execSuccess: false 表示 SQL 在数据库执行失败,常见原因:
- SQL 语法错误:检查 SQL 语句是否正确
- 表或字段不存在:确认表名、字段名拼写正确
- 参数问题:检查参数名和类型是否匹配
- 数据库连接问题:联系管理员检查数据库状态
解决步骤:
- 在平台上重新测试 SQL
- 检查 SQL 语句和参数
- 查看平台的错误日志
- 联系技术支持
Q4: 如何优化慢查询?
答:优化建议:
-
添加索引
-- 为常用查询字段添加索引
CREATE INDEX idx_user_status ON users(status);
CREATE INDEX idx_order_date ON orders(order_date); -
限制返回数量
-- 添加 LIMIT 子句
SELECT * FROM large_table
WHERE status = 'active'
LIMIT 1000 -
避免 SELECT *
-- ❌ 不推荐
SELECT * FROM users
-- ✅ 推荐
SELECT id, name, email FROM users -
使用合适的 WHERE 条件
-- 添加时间范围限制
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) -
分页查询
LIMIT :limit OFFSET :offset
Q5: 是否支持写操作(INSERT/UPDATE/DELETE)?
答:目前 executeSql 主要用于查询(SELECT)。
- ✅ 支持:SELECT 查询
- ⚠️ 部分支持:某些平台配置允许 UPDATE/DELETE(需要管理员权限)
- ❌ 不建议:在前端直接执行写操作(安全风险)
推荐做法:
- 读操作:使用
executeSql - 写操作:使用 Dataset API(
create、update、delete)
最佳实践
✅ 推荐做法
-
始终检查
execSuccessif (result.execSuccess && result.execResult) {
// 使用数据
} -
使用参数化查询
// ✅ 安全
executeSql("sqlCode", { userId: "123" });
// ❌ 不安全(SQL 注入风险)
executeSql(`SELECT * FROM users WHERE id = ${userId}`); -
定义 TypeScript 类型
interface User {
id: number;
name: string;
}
executeSql<User>("sqlCode"); -
处理错误和空结果
try {
const result = await executeSql("sqlCode");
if (!result.execSuccess) {
// 处理失败
}
if (!result.execResult || result.execResult.length === 0) {
// 处理空结果
}
} catch (error) {
// 处理异常
} -
使用缓存优化性能
-
在平台上先测试 SQL
❌ 避免做法
-
不检查 execSuccess
// ❌ 危险
const data = await executeSql('sqlCode');
data.execResult.forEach(...) // 可能报错 -
硬编码敏感信息
// ❌ 不安全
const accessKey = "sk_123456789"; // 不要硬编码 -
在循环中执行查询
// ❌ 性能差
for (const id of userIds) {
await executeSql("sqlCode", { userId: id });
}
// ✅ 使用 IN 查询
await executeSql("sqlCode", { userIds: userIds.join(",") }); -
返回大量数据不分页
-- ❌ 可能导致超时
SELECT * FROM large_table
-- ✅ 使用分页
SELECT * FROM large_table LIMIT 100
下一步
恭喜你完成了自定义 SQL 的完整教程!
接下来你可以:
- 📚 查看 SQL API 完整文档
- 🔐 了解 认证方式
- 📖 学习 Dataset API
- 💡 查看 更多示例
- ❓ 查看 FAQ
最后更新:2025-11-12