关联查询:订单显示客户信息
前面几篇都是单表操作。但实际业务中,数据往往是相互关联的——订单要显示客户信息、文章要显示作者信息、员工要显示部门名称。
这篇介绍两种实现多表关联查询的方式:Filter API 的 JOIN 能力和自定义 SQL,帮你根据场景做出选择。
- Filter API 的多表关联语法
- 自定义 SQL 实现复杂关联查询
- 如何选择合适的查询方案
- 关联关系的配置方法
需求
- 订单列表需要显示客户姓名、电话
- 文章列表需要显示作者姓名、头像
- 员工列表需要显示部门名称
- 支持按关联表字段过滤和排序
最终效果:

两种方案对比
| 特性 | filter 多表查询 | 自定义 SQL |
|---|---|---|
| 复杂度 | 低(使用 JS 对象语法) | 中(需要写 SQL) |
| 类型安全 | 完整 TypeScript 支持 | 需手动定义类型 |
| 维护性 | 高(代码可读性强) | 中(SQL 分散) |
| 灵活性 | 中(支持标准关联) | 高(任意复杂查询) |
| 适用场景 | 标准关联查询(1:1、N:1) | 复杂统计、多表聚合 |
| 返回格式 | 结构化对象(嵌套) | 扁平化结果 |
一般来说,优先用 filter 多表查询(简单、类型安全),搞不定时再用自定义 SQL。
重要前提:关联关系配置
filter 多表查询 基于 Lovrabet 逆向推理引擎分析出的表关联关系运行。只有被引擎识别为有关联关系的表才能进行 JOIN 查询。
关联关系来源:
- 自动识别:系统通过逆向分析数据库结构,自动识别表之间的关联关系(如外键、命名约定等)
- 手动配置:如果两表确实存在关联但未被识别,可在 ER 图配置界面手动添加
ER 图配置地址:https://app.lovrabet.com/app/{appCode}/data/er
如果 filter 多表查询 查询没有返回关联表数据,请检查:
- 两表是否已在 ER 图中配置关联关系
- 关联字段是否正确配置
- 使用的是数据库表名,而非数据集编码
方案一: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 图配置界面
- 查看系统自动识别的表关联关系(以连线显示)
- 如果两表存在关联但未显示连线,可手动添加:
- 点击"添加关联"按钮
- 选择主表和从表
- 配置关联字段(如
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 多表查询 限制
- 关联关系前提:两表必须在 ER 图中已配置关联关系(系统自动识别或手动添加),否则查询不会生效
- 关联类型限制:目前支持 1:1 和 N:1 关联,暂不支持反向的 1:N 关联
- 嵌套层级:最多支持 5 层嵌套
- 表名引用:必须使用数据库表名,而不是数据集编码
如果 filter 多表查询 查询没有返回预期的关联数据:
- 检查 ER 图配置:
https://app.lovrabet.com/app/{appCode}/data/er - 确认两表之间有关联连线
- 确认使用的是数据库表名(如
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 <= #{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:请按以下步骤排查:
-
检查 ER 图配置:确认两表之间已建立关联关系
- 访问
https://app.lovrabet.com/app/{appCode}/data/er - 查看两表之间是否有连线
- 如果没有,手动添加关联
- 访问
-
检查表名引用:确认使用的是数据库表名
// ✅ 正确:使用数据库表名
"customer.name";
// ❌ 错误:使用数据集编码
"dataset_xxx.name"; -
检查关联方向:确认关联类型是支持的 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 图中已配置
下一步
- 销售数据报表 — 学习自定义 SQL 做复杂统计
相关阅读
核心文档
- Filter API 完整指南 — filter 多表查询完整说明
- SQL API 使用指南 — 自定义 SQL 查询完整说明
- 多表关联查询 — Filter 和 SQL 完整对比
进阶主题
- 销售报表:自定义 SQL — 复杂统计使用 SQL 查询
- Backend Function 调用 SQL — BF 调用自定义 SQL
- 数据校验:先验函数 — 后端数据校验和权限控制
难度等级:L2 | 预计耗时:50 分钟