跳到主要内容

关联查询:订单显示客户信息

前面几篇都是单表操作。但实际业务中,数据往往是相互关联的——订单要显示客户信息、文章要显示作者信息、员工要显示部门名称。

这篇介绍两种实现多表关联查询的方式:Filter API 的 JOIN 能力和自定义 SQL,帮你根据场景做出选择。

本节你将学到
  • Filter API 的多表关联语法
  • 自定义 SQL 实现复杂关联查询
  • 如何选择合适的查询方案
  • 关联关系的配置方法

需求

  • 订单列表需要显示客户姓名、电话
  • 文章列表需要显示作者姓名、头像
  • 员工列表需要显示部门名称
  • 支持按关联表字段过滤和排序

最终效果:

订单列表页面(多表关联)


两种方案对比

特性filter 多表查询自定义 SQL
复杂度低(使用 JS 对象语法)中(需要写 SQL)
类型安全完整 TypeScript 支持需手动定义类型
维护性高(代码可读性强)中(SQL 分散)
灵活性中(支持标准关联)高(任意复杂查询)
适用场景标准关联查询(1:1、N:1)复杂统计、多表聚合
返回格式结构化对象(嵌套)扁平化结果

一般来说,优先用 filter 多表查询(简单、类型安全),搞不定时再用自定义 SQL。

重要前提:关联关系配置

Lovrabet 逆向推理引擎

filter 多表查询 基于 Lovrabet 逆向推理引擎分析出的表关联关系运行。只有被引擎识别为有关联关系的表才能进行 JOIN 查询。

关联关系来源

  1. 自动识别:系统通过逆向分析数据库结构,自动识别表之间的关联关系(如外键、命名约定等)
  2. 手动配置:如果两表确实存在关联但未被识别,可在 ER 图配置界面手动添加

ER 图配置地址https://app.lovrabet.com/app/{appCode}/data/er

查询不生效?

如果 filter 多表查询 查询没有返回关联表数据,请检查:

  1. 两表是否已在 ER 图中配置关联关系
  2. 关联字段是否正确配置
  3. 使用的是数据库表名,而非数据集编码

方案一:filter 多表查询(推荐)

filter 多表查询 是 SDK 内置的多表关联功能,使用简单、类型安全。系统自动检测表之间的关联关系,统一使用 LEFT JOIN,支持最多 5 层嵌套。

语法格式

使用 tableName.fieldName 格式引用关联表字段:

await client.models.orders.filter({
select: [
"id", // 主表字段
"order_no",
"customer.name", // 关联表字段:表名.字段名
"customer.phone",
"customer.level",
],
where: {
status: { $eq: "pending" },
"customer.level": { $eq: "VIP" }, // 按关联表字段过滤
},
orderBy: [
{ create_time: "desc" },
{ "customer.name": "asc" }, // 按关联表字段排序
],
});

实现步骤

步骤 1:查看和配置表关联关系

filter 多表查询 基于 Lovrabet 逆向推理引擎分析的表关联关系。在使用前,需要确认表之间的关联关系已正确配置。

ER 图配置地址https://app.lovrabet.com/app/{appCode}/data/er

ER 图配置界面

操作步骤

  1. 进入 ER 图配置界面
  2. 查看系统自动识别的表关联关系(以连线显示)
  3. 如果两表存在关联但未显示连线,可手动添加:
    • 点击"添加关联"按钮
    • 选择主表和从表
    • 配置关联字段(如 orders.customer_id → customers.id
    • 选择关联类型(1:1 或 N:1)

常见关联类型

关联类型说明示例
1:1一对一用户 ← 用户资料
N:1多对一订单 → 客户
1:N一对多客户 ← 订单(反向查询暂不支持)

步骤 2:使用 filter 多表查询 查询

场景 1:订单关联客户信息

import { lovrabetClient } from "./api/client";

/**
* 查询订单列表,同时获取客户信息
*/
async function getOrderListWithCustomer(page = 1) {
const result = await lovrabetClient.models.dataset_orders.filter({
select: [
// 主表字段
"id",
"order_no",
"total_amount",
"status",
"create_time",
// 关联表字段(customer 是关联表的表名)
"customer.name",
"customer.phone",
"customer.level",
"customer.address",
],
where: {
// 按关联表字段过滤
"customer.level": { $in: ["VIP", "SVIP"] },
status: { $ne: "cancelled" },
},
orderBy: [
{ create_time: "desc" },
{ "customer.level": "desc" }, // VIP 排前面
],
currentPage: page,
pageSize: 20,
});

return result;
}

// 使用示例
const result = await getOrderListWithCustomer(1);

console.log(result.tableData[0]);
// 输出:
// {
// id: 1,
// order_no: "ORD20240115001",
// total_amount: 1000,
// status: "completed",
// create_time: "2024-01-15T10:30:00Z",
// customer: { // 关联表数据自动嵌套
// name: "张三",
// phone: "138****8000",
// level: "VIP",
// address: "北京市朝阳区"
// }
// }

场景 2:文章关联作者信息

/**
* 查询已发布文章,同时获取作者信息
*/
async function getArticleListWithAuthor(page = 1) {
const result = await lovrabetClient.models.dataset_articles.filter({
select: [
"id",
"title",
"summary",
"publish_time",
"author.username", // 作者用户名
"author.avatar_url", // 作者头像
"author.bio", // 作者简介
],
where: {
status: { $eq: "published" },
"author.is_signed": { $eq: true }, // 只显示签约作者
},
orderBy: [{ publish_time: "desc" }, { "author.username": "asc" }],
currentPage: page,
pageSize: 10,
});

return result;
}

场景 3:多层关联查询

/**
* 查询订单及其客户、客户的销售代表
* 支持最多 5 层嵌套:orders → customers → sales_users
*/
async function getOrderListWithSales(page = 1) {
const result = await lovrabetClient.models.dataset_orders.filter({
select: [
"id",
"order_no",
"total_amount",
"customer.name", // 客户名称(第1层)
"customer.level", // 客户等级
"customer.sales.name", // 销售代表姓名(第2层)
"customer.sales.phone", // 销售代表电话
"customer.sales.dept.name", // 销代表所属部门(第3层)
],
where: {
status: { $eq: "pending" },
"customer.sales.dept.region": { $eq: "华北区" },
},
orderBy: [{ create_time: "desc" }],
currentPage: page,
pageSize: 20,
});

return result;
}

步骤 3:在 React 组件中使用

// src/pages/order-list.tsx

import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";

interface OrderWithCustomer {
id: number;
order_no: string;
total_amount: number;
status: string;
create_time: string;
customer: {
name: string;
phone: string;
level: string;
};
}

export default function OrderList() {
const [orders, setOrders] = useState<OrderWithCustomer[]>([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);

useEffect(() => {
loadOrders();
}, [currentPage]);

const loadOrders = async () => {
setLoading(true);
try {
const result = await lovrabetClient.models.dataset_orders.filter({
select: [
"id",
"order_no",
"total_amount",
"status",
"customer.name",
"customer.phone",
"customer.level",
],
where: {
status: { $ne: "deleted" },
},
orderBy: [{ create_time: "desc" }],
currentPage,
pageSize: 20,
});

setOrders(result.tableData);
setTotal(result.total);
} catch (error) {
console.error("加载订单失败:", error);
} finally {
setLoading(false);
}
};

const getStatusName = (status: string) => {
const names: Record<string, string> = {
pending: "待付款",
paid: "已付款",
shipped: "已发货",
completed: "已完成",
cancelled: "已取消",
};
return names[status] || status;
};

const getLevelColor = (level: string) => {
const colors: Record<string, string> = {
SVIP: "#ff6b6b",
VIP: "#ffd93d",
普通: "#a8dadc",
};
return colors[level] || "#ccc";
};

return (
<div className="order-list">
<h1>订单列表 (共 {total} 条)</h1>

{loading ? (
<div>加载中...</div>
) : (
<table>
<thead>
<tr>
<th>订单号</th>
<th>金额</th>
<th>状态</th>
<th>客户名称</th>
<th>客户电话</th>
<th>客户等级</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td>{order.order_no}</td>
<td>¥{order.total_amount.toFixed(2)}</td>
<td>{getStatusName(order.status)}</td>
<td>{order.customer.name}</td>
<td>{order.customer.phone}</td>
<td>
<span
style={{
padding: "2px 8px",
borderRadius: "4px",
backgroundColor: getLevelColor(order.customer.level),
}}
>
{order.customer.level}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}

{/* 分页 */}
<div className="pagination">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
上一页
</button>
<span>{currentPage}</span>
<button
disabled={currentPage * 20 >= total}
onClick={() => setCurrentPage(currentPage + 1)}
>
下一页
</button>
</div>
</div>
);
}

filter 多表查询 限制

  1. 关联关系前提:两表必须在 ER 图中已配置关联关系(系统自动识别或手动添加),否则查询不会生效
  2. 关联类型限制:目前支持 1:1 和 N:1 关联,暂不支持反向的 1:N 关联
  3. 嵌套层级:最多支持 5 层嵌套
  4. 表名引用:必须使用数据库表名,而不是数据集编码
查询不生效?

如果 filter 多表查询 查询没有返回预期的关联数据:

  1. 检查 ER 图配置:https://app.lovrabet.com/app/{appCode}/data/er
  2. 确认两表之间有关联连线
  3. 确认使用的是数据库表名(如 customer)而非数据集编码(如 dataset_xxx

方案二:自定义 SQL

当 filter 多表查询 无法满足需求时(如复杂统计、多表聚合),可以使用自定义 SQL。

适用场景

  • 复杂的数据统计和聚合
  • 需要使用数据库特定函数
  • 多表 GROUP BY、HAVING 查询
  • 性能优化(减少查询次数)

实现步骤

步骤 1:在平台上创建自定义 SQL

SQL 名称getOrderWithCustomer

平台配置地址https://app.lovrabet.com/app/{appCode}/data/custom-sql

SELECT
o.id,
o.order_no,
o.total_amount,
o.status,
o.create_time,
c.name AS customer_name,
c.phone AS customer_phone,
c.level AS customer_level,
c.address AS customer_address,
u.name AS sales_name,
d.name AS sales_dept
FROM dataset_orders o
LEFT JOIN dataset_customers c ON o.customer_id = c.id
LEFT JOIN dataset_users u ON c.sales_id = u.id
LEFT JOIN dataset_departments d ON u.dept_id = d.id
WHERE o.status != 'deleted'
<if test="minAmount">
AND o.total_amount >= #{minAmount}
</if>
<if test="customerLevel">
AND c.level = #{customerLevel}
</if>
<if test="salesDept">
AND d.name = #{salesDept}
</if>
ORDER BY o.create_time DESC
LIMIT #{pageSize} OFFSET #{offset}

步骤 2:前端调用自定义 SQL

import { lovrabetClient } from "./api/client";

interface OrderWithCustomerFlat {
id: number;
order_no: string;
total_amount: number;
status: string;
create_time: string;
customer_name: string;
customer_phone: string;
customer_level: string;
customer_address: string;
sales_name: string;
sales_dept: string;
}

/**
* 使用自定义 SQL 查询订单及关联信息
*/
async function getOrderListBySQL(params: {
page?: number;
pageSize?: number;
minAmount?: number;
customerLevel?: string;
salesDept?: string;
}) {
const {
page = 1,
pageSize = 20,
minAmount,
customerLevel,
salesDept,
} = params;

const data = await lovrabetClient.sql.execute({
sqlCode: "getOrderWithCustomer",
params: {
minAmount,
customerLevel,
salesDept,
pageSize,
offset: (page - 1) * pageSize,
},
});

if (!data.execSuccess) {
throw new Error("查询失败");
}

return {
tableData: data.execResult || [],
total: data.execResult?.length || 0, // 简化处理,实际应返回总数
currentPage: page,
pageSize,
};
}

// 使用示例
const result = await getOrderListBySQL({
page: 1,
pageSize: 20,
minAmount: 1000,
customerLevel: "VIP",
});

console.log(result.tableData[0]);
// 输出(扁平化结构):
// {
// id: 1,
// order_no: "ORD20240115001",
// total_amount: 1000,
// status: "completed",
// create_time: "2024-01-15T10:30:00Z",
// customer_name: "张三",
// customer_phone: "138****8000",
// customer_level: "VIP",
// customer_address: "北京市朝阳区",
// sales_name: "李四",
// sales_dept: "华北区"
// }

步骤 3:复杂统计查询

-- SQL 名称:getCustomerOrderStats
-- 查询客户的订单统计信息

SELECT
c.id,
c.name,
c.phone,
c.level,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_amount,
COALESCE(SUM(CASE WHEN o.status = 'completed' THEN o.total_amount ELSE 0 END), 0) AS completed_amount,
MAX(o.create_time) AS last_order_time
FROM dataset_customers c
LEFT JOIN dataset_orders o ON c.id = o.customer_id
<if test="startDate">
AND o.create_time >= #{startDate}
</if>
<if test="endDate">
AND o.create_time &lt;= #{endDate}
</if>
WHERE c.status = 'active'
<if test="minLevel">
AND c.level = #{minLevel}
</if>
GROUP BY c.id, c.name, c.phone, c.level
HAVING COUNT(o.id) >= #{minOrderCount}
ORDER BY total_amount DESC
LIMIT #{pageSize} OFFSET #{offset}
/**
* 查询客户订单统计
*/
async function getCustomerStats(params: {
page?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
minLevel?: string;
minOrderCount?: number;
}) {
const { page = 1, pageSize = 20, ...rest } = params;

const data = await lovrabetClient.sql.execute({
sqlCode: "getCustomerOrderStats",
params: {
...rest,
minOrderCount: rest.minOrderCount ?? 1,
pageSize,
offset: (page - 1) * pageSize,
},
});

if (!data.execSuccess) {
throw new Error("查询失败");
}

return data.execResult || [];
}

自定义 SQL 返回数据结构

特性filter 多表查询自定义 SQL
数据结构嵌套对象 { customer: { name: "..." } }扁平化 { customer_name: "..." }
字段命名保持原字段名需要 AS 别名
类型推断自动推断类型需手动定义类型

方案对比与选择

功能对比

场景filter 多表查询自定义 SQL
基础关联查询✅ 推荐✅ 可用
多层关联✅ 支持(5 层)✅ 无限制
复杂条件过滤✅ 支持✅ 更灵活
统计聚合❌ 不支持✅ 推荐
GROUP BY❌ 不支持✅ 支持
窗口函数❌ 不支持✅ 支持
数据库特定函数❌ 不支持✅ 支持

性能对比

场景filter 多表查询自定义 SQL
简单关联相当相当
复杂关联可能有多次查询一次查询完成
大数据量受分页限制可优化 LIMIT
统计查询不适用更高效

选择建议

┌─────────────────────────────────────────────────────────────────┐
│ 多表关联查询方案选择 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 是否需要复杂统计、GROUP BY、窗口函数? │
│ │ │
│ ├── 是 → 使用【自定义 SQL】 │
│ │ - 订单统计、客户分析等 │
│ │ - 销售报表、数据看板 │
│ │ - 需要数据库特定函数的场景 │
│ │ │
│ └── 否 → 是否超过 5 层嵌套? │
│ │ │
│ ├── 是 → 使用【自定义 SQL】 │
│ │ │
│ └── 否 → 使用【filter 多表查询】(推荐) │
│ - 订单列表 + 客户信息 │
│ - 文章列表 + 作者信息 │
│ - 员工列表 + 部门信息 │
│ │
└─────────────────────────────────────────────────────────────────┘

实战案例

案例:销售订单看板

结合 filter 多表查询 和自定义 SQL,实现一个完整的销售订单看板。

前端组件

// src/pages/sales-dashboard.tsx

import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";

export default function SalesDashboard() {
const [summary, setSummary] = useState({
totalOrders: 0,
totalAmount: 0,
vipCustomerCount: 0,
});
const [orders, setOrders] = useState([]);
const [topCustomers, setTopCustomers] = useState([]);

useEffect(() => {
loadDashboard();
}, []);

const loadDashboard = async () => {
// 1. 使用 filter 多表查询 获取订单列表(带客户信息)
const orderResult = await lovrabetClient.models.dataset_orders.filter({
select: [
"id",
"order_no",
"total_amount",
"status",
"create_time",
"customer.name",
"customer.level",
],
where: {
create_time: {
$gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
},
},
orderBy: [{ create_time: "desc" }],
currentPage: 1,
pageSize: 10,
});

setOrders(orderResult.tableData);

// 2. 使用自定义 SQL 获取统计数据
const statsData = await lovrabetClient.sql.execute({
sqlCode: "getSalesSummary",
params: {
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0],
},
});

if (statsData.execSuccess && statsData.execResult) {
setSummary(statsData.execResult[0] || {});
}

// 3. 使用自定义 SQL 获取 Top 客户
const topData = await lovrabetClient.sql.execute({
sqlCode: "getTopCustomers",
params: {
limit: 10,
},
});

if (topData.execSuccess && topData.execResult) {
setTopCustomers(topData.execResult);
}
};

return (
<div className="sales-dashboard">
<h1>销售订单看板</h1>

{/* 统计卡片 */}
<div className="summary-cards">
<div className="card">
<h3>总订单数</h3>
<p className="value">{summary.totalOrders}</p>
</div>
<div className="card">
<h3>总金额</h3>
<p className="value">¥{summary.totalAmount?.toFixed(2) || 0}</p>
</div>
<div className="card">
<h3>VIP 客户数</h3>
<p className="value">{summary.vipCustomerCount}</p>
</div>
</div>

{/* 最新订单 */}
<div className="section">
<h2>最新订单</h2>
<table>
<thead>
<tr>
<th>订单号</th>
<th>金额</th>
<th>状态</th>
<th>客户名称</th>
<th>客户等级</th>
</tr>
</thead>
<tbody>
{orders.map((order: any) => (
<tr key={order.id}>
<td>{order.order_no}</td>
<td>¥{order.total_amount.toFixed(2)}</td>
<td>{order.status}</td>
<td>{order.customer.name}</td>
<td>{order.customer.level}</td>
</tr>
))}
</tbody>
</table>
</div>

{/* Top 客户 */}
<div className="section">
<h2>Top 客户</h2>
<ul>
{topCustomers.map((customer: any, index) => (
<li key={index}>
{index + 1}. {customer.name} - ¥{customer.total_amount.toFixed(2)}
</li>
))}
</ul>
</div>
</div>
);
}

知识点整理

filter 多表查询 语法

// select: 使用 tableName.fieldName
select: [
"id", // 主表字段
"customer.name", // 关联表字段
"customer.level"
]

// where: 使用 tableName.fieldName
where: {
"customer.level": { "$eq": "VIP" } // 关联表字段过滤
}

// orderBy: 使用 tableName.fieldName
orderBy: [
{ "customer.name": "asc" } // 关联表字段排序
]

自定义 SQL 参数

// SQL 中使用 #{paramName}
const data = await lovrabetClient.sql.execute({
sqlCode: "getOrderWithCustomer",
params: {
minAmount: 1000,
customerLevel: "VIP",
pageSize: 20,
offset: 0,
},
});

常见问题

Q: filter 多表查询 和自定义 SQL 可以混用吗?

A:可以。在同一个应用中,可以根据场景选择不同的方案:

  • 列表查询使用 filter 多表查询(开发效率高)
  • 统计报表使用自定义 SQL(性能更好)

Q: 如何获取表之间的关联关系?

A:filter 多表查询 基于 Lovrabet 逆向推理引擎分析的表关联关系。

ER 图配置地址https://app.lovrabet.com/app/{appCode}/data/er

系统会通过逆向分析自动识别表之间的关联关系(如外键、命名约定等)。如果两表确实存在关联但未被识别,可以在 ER 图界面手动添加关联。

Q: filter 多表查询 查询没有返回关联数据怎么办?

A:请按以下步骤排查:

  1. 检查 ER 图配置:确认两表之间已建立关联关系

    • 访问 https://app.lovrabet.com/app/{appCode}/data/er
    • 查看两表之间是否有连线
    • 如果没有,手动添加关联
  2. 检查表名引用:确认使用的是数据库表名

    // ✅ 正确:使用数据库表名
    "customer.name";

    // ❌ 错误:使用数据集编码
    "dataset_xxx.name";
  3. 检查关联方向:确认关联类型是支持的 1:1 或 N:1

Q: filter 多表查询 性能如何?

A:filter 多表查询 最终会转换为 SQL 的 LEFT JOIN,性能与手写 SQL 相当。对于复杂查询,自定义 SQL 可能更灵活,可以进一步优化。

Q: 关联表数据如何在前端使用?

A:filter 多表查询 返回嵌套结构,需要通过对象访问:

// filter 多表查询
order.customer.name;

// 自定义 SQL(扁平化)
order.customer_name;

本节小结

恭喜你掌握了多表关联查询!两种方案对比:

方案适用场景特点
Filter 多表查询标准关联查询简单、类型安全、嵌套结构
自定义 SQL复杂统计聚合灵活、高性能、扁平结果
选择建议
  • 优先使用 Filter 多表查询(简单易用)
  • 搞不定时再用自定义 SQL(复杂统计)
  • 确保表关联关系在 ER 图中已配置

下一步

相关阅读

核心文档

进阶主题


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