Skip to content

自定义 SQL 使用教程

本教程将手把手教你如何在 Lovrabet 平台上配置和使用自定义 SQL 查询,从零开始完成一个完整的数据查询功能。

💡 学习目标通过本教程,你将学会:

📋 前提条件

在开始之前,请确保:

  • ✅ 已注册 Lovrabet 平台账号
  • ✅ 已创建应用并获取 AppCode
  • ✅ 已安装 @lovrabet/sdk (>= 1.1.19)
  • ✅ 已获取 AccessKey(服务端)或已登录平台(浏览器)

如果还没有完成上述准备,请先查看 快速开始指南。


第一步:在平台上创建自定义 SQL

1.1 进入 SQL 管理页面

  1. 登录 Lovrabet 平台
  2. 选择你的应用
  3. 点击进入 SQL 管理页面: 【应用配置】 → 【应用资产】 → 【自定义 SQL 管理】

1.2 创建新的 SQL 查询

点击右上角的 "新建 SQL" 按钮,填写以下信息:

基础信息

字段说明示例
SQL 名称查询的名称(必填)查询活跃用户统计
SQL 描述查询的用途说明(可选)统计最近7天活跃用户的登录次数和操作次数
SQL 分组便于管理(可选)用户统计

SQL 语句

SQL 编辑器 中输入你的 SQL 语句。

入门示例

SQL
-- 简单查询:查询所有活跃用户
SELECT
  id,
  name,
  email,
  created_at
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 100

进阶示例

SQL
-- 复杂统计:查询最近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

💡 SQL 编写要点

基础规范

性能考虑

安全建议

常见 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 语句

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用户状态

⚠️ 参数语法规范

参数类型支持

类型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
-- ❌ 不安全:容易受到 SQL 注入攻击
SELECT * FROM users WHERE id = 123

-- ✅ 安全:使用参数化查询
SELECT * FROM users WHERE id = #{userId}

💡 SQL 编写最佳实践

性能优化

可读性

示例


1.4 测试 SQL

在保存之前,务必测试 SQL 是否能正常执行:

  1. 点击 "测试运行" 按钮
  2. 如果有参数,填写测试参数值
  3. 查看执行结果和耗时
  4. 确认返回的数据格式正确

测试结果示例

JSON
{
  "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"
}

💡 测试要点


1.5 保存并获取 SQL Code

  1. 点击 "保存" 按钮
  2. 保存成功后,系统会生成一个唯一的 SQL Code
  3. 复制并保存这个 SQL Code,后续调用时需要用到

SQL Code 格式xxxxx-xxxxx(例如:12345-67890


第二步:在代码中调用 SQL

2.1 安装和配置 SDK

安装 SDK

Bash
npm install @lovrabet/sdk

配置客户端

TypeScript
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 执行基础查询

最简单的调用

TypeScript
// 执行 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 执行失败");
}

输出示例

Plain
查询成功,返回 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 带参数的查询

传递参数

TypeScript
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);
}

动态参数示例

TypeScript
// 从用户输入获取参数
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)

定义结果类型

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 基础数据处理

遍历结果

TypeScript
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 数据聚合统计

TypeScript
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 数据转换和格式化

TypeScript
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);
}

输出示例

Plain
┌─────────┬────────────┬────────────┬────────┬─────────┬──────────┐
│ (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

TypeScript
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},<equation>{row.order_date},</equation>{row.product_name},<equation>{row.quantity},</equation>{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 完整的错误处理

TypeScript
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 用户友好的错误提示

TypeScript
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

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. 实现前端代码

TypeScript
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

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. 实现导出功能

TypeScript
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, `销售报表_<equation>{startDate}_</equation>{endDate}.xlsx`);

  return summary;
}

// 使用
const summary = await generateSalesReport(
  "2025-01-01",
  "2025-01-31",
  "completed"
);
console.log("报表生成成功:", summary);

案例 3:实时搜索建议

需求:用户输入时实时显示搜索建议

1. 平台 SQL

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. 实现搜索组件

TypeScript
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 使用缓存

TypeScript
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 并发查询

TypeScript
// 多个独立查询可以并发执行
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 分页查询

TypeScript
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 执行问题?

:按以下步骤排查:

  1. 在平台上测试

    • 先在平台的 SQL 管理页面点击"测试运行"
    • 确认 SQL 能正常执行
  2. 启用 SDK 调试模式

  3. 检查返回结果

  4. 查看网络请求

    • 打开浏览器 DevTools → Network 面板
    • 查看请求和响应详情

Q3: execSuccess 为 false 是什么原因?

execSuccess: false 表示 SQL 在数据库执行失败,常见原因:

  • SQL 语法错误:检查 SQL 语句是否正确
  • 表或字段不存在:确认表名、字段名拼写正确
  • 参数问题:检查参数名和类型是否匹配
  • 数据库连接问题:联系管理员检查数据库状态

解决步骤

  1. 在平台上重新测试 SQL
  2. 检查 SQL 语句和参数
  3. 查看平台的错误日志
  4. 联系技术支持

Q4: 如何优化慢查询?

:优化建议:

  1. 添加索引
  2. 限制返回数量
  3. 避免 SELECT \*
  4. 使用合适的 WHERE 条件
  5. 分页查询

Q5: 是否支持写操作(INSERT/UPDATE/DELETE)?

:目前 executeSql 主要用于查询(SELECT)

  • 支持:SELECT 查询
  • ⚠️ 部分支持:某些平台配置允许 UPDATE/DELETE(需要管理员权限)
  • 不建议:在前端直接执行写操作(安全风险)

推荐做法

  • 读操作:使用 executeSql
  • 写操作:使用 Dataset API(createupdatedelete

最佳实践

✅ 推荐做法

  1. 始终检查 execSuccess
  2. 使用参数化查询
  3. 定义 TypeScript 类型
  4. 处理错误和空结果
  5. 使用缓存优化性能
  6. 在平台上先测试 SQL

❌ 避免做法

  1. 不检查 execSuccess
  2. 硬编码敏感信息
  3. 在循环中执行查询
  4. 返回大量数据不分页

下一步

恭喜你完成了自定义 SQL 的完整教程!

接下来你可以:

  • 📚 查看 SQL API 完整文档
  • 🔐 了解 认证方式
  • 📖 学习 Dataset API
  • 💡 查看 更多示例
  • ❓ 查看 FAQ

最后更新:2025-11-12

基于飞书知识库同步生成,内容以飞书源文档为准