使用 Predicate 建構訂單查詢條件
本文檔展示如何在訂單管理中使用 Predicate 模組建構 RSQL/FIQL 查詢條件。
基本用法
1. 簡單查詢
import { Predicate } from '@appfuse/appfuse-web/utils/predicate';
import { orderService } from '@/services/sales/order-service';
// 查詢待確認的訂單
const result = await orderService.query({
predicate: Predicate.eq('status', 'pending_confirmation').toString(),
page: 0,
size: 20,
sort: 'createdAt,desc',
});
// 計算總頁數(前端衍生欄位)
const totalPages = result.size ? Math.ceil(result.totalElements / result.size) : 1;
console.log(`Total: ${result.totalElements}, Page: ${(result.page ?? 0) + 1}/${totalPages}`);
console.log('Orders:', result.content);
2. 組合查詢(AND)
// 查詢特定日期範圍內的待處理訂單
const predicate = Predicate.eq('status', 'pending_confirmation')
.and(Predicate.ge('deliveryDate', new Date('2025-11-01')))
.and(Predicate.le('deliveryDate', new Date('2025-11-30')))
.toString();
const result = await orderService.query({
predicate,
page: 0,
size: 20,
});
// 生成的 RSQL 查詢:
// (status=="pending_confirmation";deliveryDate=ge=2025-11-01T00:00:00.000Z;deliveryDate=le=2025-11-30T23:59:59.999Z)
3. 多狀態查詢(IN)
// 查詢多個狀態的訂單
const predicate = Predicate.in('status', [
'pending_confirmation',
'confirmed',
'in_production'
]).toString();
const result = await orderService.query({ filter });
// 生成的 RSQL 查詢:
// status=in=("pending_confirmation","confirmed","in_production")
4. 模糊查詢
// 根據訂單編號模糊查詢
const searchTerm = 'ORD-2025';
const predicate = Predicate.like('orderNumber', searchTerm).toString();
const result = await orderService.query({ filter });
// 生成的 RSQL 查詢:
// orderNumber=="*ORD-2025*"
5. 金額範圍查詢
// 查詢金額在 1000-5000 之間的訂單
const predicate = Predicate.ge('total', 1000)
.and(Predicate.le('total', 5000))
.toString();
const result = await orderService.query({ filter });
// 生成的 RSQL 查詢:
// (total=ge=1000;total=le=5000)
完整範例:OrderFinder 組件
import { useEffect, useState } from 'react';
import { Predicate } from '@appfuse/appfuse-web/utils/predicate';
import { orderService } from '@/services/sales/order-service';
import type { Order } from '@/mocks/types';
export function OrderFinder() {
const [orders, setOrders] = useState<Order[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
const fetchOrders = async () => {
// 動態建構查詢條件
let predicate = Predicate.empty();
// 搜尋條件
if (searchTerm) {
predicate = predicate.and(Predicate.like('orderNumber', searchTerm));
}
// 狀態過濾
if (statusFilter.length > 0) {
predicate = predicate.and(Predicate.in('status', statusFilter));
}
// 日期範圍
if (dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(dateFrom)));
}
if (dateTo) {
predicate = predicate.and(Predicate.le('deliveryDate', new Date(dateTo)));
}
// 調用 API
const result = await orderService.query({
predicate: predicate.toString(),
page: currentPage - 1, // Spring 從 0 開始
size: 20,
sort: 'createdAt,desc',
});
setOrders(result.content);
// 計算總頁數(UI 元件自己計算)
const actualSize = result.size ?? 20;
const calculatedTotalPages = Math.ceil(result.totalElements / actualSize);
setTotalPages(calculatedTotalPages);
};
void fetchOrders();
}, [searchTerm, statusFilter, dateFrom, dateTo, currentPage]);
return (
<div>
{/* UI components */}
</div>
);
}
後端對應格式(Spring)
前端生成的 RSQL 查詢會傳遞給後端的 Spring Controller:
@GetMapping
public ResponseEntity<List<PropertyMap>> findAll(
@RequestParam(required = false) SearchPredicate predicate,
Pageable pageable,
Authentication authentication) {
Page<PropertyMap> data = orderService.queryAll(predicate, pageable);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("X-Total-Count", String.valueOf(data.getTotalElements()));
return ResponseEntity.ok().headers(httpHeaders).body(data.getContent());
}
查詢參數格式
| 參數 | 說明 | 範例 |
|---|---|---|
predicate | RSQL 查詢字串 | status=="pending" |
page | 頁碼(從 0 開始) | 0, 1, 2 |
size | 每頁筆數 | 20, 50, 100 |
sort | 排序欄位和方向 | createdAt,desc |
回應格式
Headers:
X-Total-Count: 總筆數
Body:
[
{
"id": "order-1",
"orderNumber": "ORD-2025-001",
"status": "pending_confirmation",
"total": 2500,
...
}
]
實際 HTTP 請求範例
請求
GET /api/v1/orders?predicate=(status%3D%3D%22pending_confirmation%22%3BdeliveryDate%3Dge%3D2025-11-01)&page=0&size=20&sort=createdAt,desc
Authorization: Bearer <access_token>
回應
HTTP/1.1 200 OK
X-Total-Count: 45
Content-Type: application/json
[
{ "id": "order-1", ... },
{ "id": "order-2", ... },
...
]
支援的操作符
| 操作符 | RSQL 格式 | Predicate 方法 | 說明 |
|---|---|---|---|
| 等於 | == | eq(field, value) | 精確匹配 |
| 不等於 | != | notEq(field, value) | 不相等 |
| 大於 | =gt= | gt(field, value) | 大於 |
| 大於等於 | =ge= | ge(field, value) | 大於等於 |
| 小於 | =lt= | lt(field, value) | 小於 |
| 小於等於 | =le= | le(field, value) | 小於等於 |
| 在集合中 | =in= | in(field, array) | 值在陣列中 |
| 不在集合中 | =out= | notIn(field, array) | 值不在陣列中 |
| 模糊查詢 | == | like(field, value) | 包含子字串(自動添加 *) |
最佳實踐
1. 空條件處理(核心優勢)
Predicate 會自動忽略空值,無需手動檢查條件是否存在!
// ❌ 傳統做法:需要大量 if 檢查
let predicate = Predicate.empty();
if (searchTerm) {
predicate = predicate.and(Predicate.like('orderNumber', searchTerm));
}
if (filters.status.length > 0) {
predicate = predicate.and(Predicate.in('status', filters.status));
}
if (filters.dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(filters.dateFrom)));
}
// ✅ 優雅做法:直接串接,空值自動被忽略
const predicate = Predicate.empty()
.and(Predicate.like('orderNumber', searchTerm)) // 空字串 → 忽略
.and(Predicate.in('status', filters.status)) // [] → 忽略
.and(Predicate.ge('deliveryDate',
filters.dateFrom ? new Date(filters.dateFrom) : undefined // undefined → 忽略
));
// 結果:只保留有效條件
自動忽略的值:
null和undefined- 空字串
''(對於like()) - 空陣列
[](對於in()和notIn())
// 這些條件會被自動忽略
Predicate.eq('field', null) // → EmptyNode(忽略)
Predicate.eq('field', undefined) // → EmptyNode(忽略)
Predicate.in('field', []) // → EmptyNode(忽略)
Predicate.like('field', '') // → EmptyNode(忽略)
// 組合後
Predicate.empty()
.and(Predicate.eq('a', null))
.and(Predicate.in('b', []))
.toString() // → null(完全沒有條件)
唯一例外:is() 和 isNot() 不會忽略 null(用於明確檢查 null 或 boolean)
Predicate.is('parent', null) // → "parent=is=null" ✅(不忽略)
Predicate.isNot('deleted', true) // → "deleted=isnt=true" ✅(不忽略)
2. 動態條件建構
function buildOrderQuery(filters: {
search?: string;
status?: string[];
dateFrom?: string;
dateTo?: string;
minAmount?: number;
maxAmount?: number;
}) {
let predicate = Predicate.empty();
if (filters.search) {
predicate = predicate.and(Predicate.like('orderNumber', filters.search));
}
if (filters.status && filters.status.length > 0) {
predicate = predicate.and(Predicate.in('status', filters.status));
}
if (filters.dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(filters.dateFrom)));
}
if (filters.dateTo) {
predicate = predicate.and(Predicate.le('deliveryDate', new Date(filters.dateTo)));
}
if (filters.minAmount !== undefined) {
predicate = predicate.and(Predicate.ge('total', filters.minAmount));
}
if (filters.maxAmount !== undefined) {
predicate = predicate.and(Predicate.le('total', filters.maxAmount));
}
return predicate.toString();
}
// 使用
const filter = buildOrderQuery({
status: ['pending_confirmation', 'confirmed'],
dateFrom: '2025-11-01',
minAmount: 1000,
});
await orderService.query({ filter, page: 0, size: 20 });
3. 分頁處理
// 前端頁碼從 1 開始,Spring 從 0 開始
const uiPage = 1; // 使用者看到的頁碼
const springPage = uiPage - 1; // 傳給後端的頁碼
const result = await orderService.query({
filter: predicate.toString(),
page: springPage,
size: 20,
});
// 顯示分頁資訊
const totalPages = result.size ? Math.ceil(result.totalElements / result.size) : 1;
console.log(`Page ${(result.page ?? 0) + 1} of ${totalPages}`);
console.log(`Total: ${result.totalElements} items`);
相關資源
- Predicate API 文檔(參閱 appfuse-web/lib/utils/predicate/)
- RSQL 標準
- Service Layer 架構(參閱 appfuse-web/src/services/)