Yii2 ActiveRecord 透過 hasOne()、hasMany() 定義關聯,支援一對一、一對多、多對多 (中間表)、動態條件查詢。本文系統整理關聯宣告、存取方式、SQL 執行機制、中間表 viaTable()/via()、條件參數化、快取與常見效能陷阱。
namespace common\models;
use yii\db\ActiveRecord;
class Customer extends ActiveRecord {
// 一對多:客戶擁有多筆訂單
public function getOrders() {
return $this->hasMany(Order::className(), ['customer_id' => 'id']);
}
// 一對多 + 條件:大額訂單
public function getBigOrders($threshold = 100) {
return $this->hasMany(Order::className(), ['customer_id' => 'id'])
->where(['>', 'subtotal', $threshold])
->orderBy('id');
}
}
class Order extends ActiveRecord {
// 一對一:訂單屬於一位客戶
public function getCustomer() {
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
}
}
hasMany($class, $link):$link = ['關聯表欄位' => '當前表欄位']hasOne($class, $link):同上,但 LIMIT 1yii\db\ActiveQuery 物件,可鏈式呼叫 where(), orderBy(), with() 等。$customer = Customer::findOne(1); // 屬性語法:觸發關聯查詢,結果快取於物件 $orders = $customer->orders; // 執行 SQL,回傳 Order[] 陣列 $orders2 = $customer->orders; // 直接回傳快取,不再查詢 // 方法語法:取得 ActiveQuery 物件,可進一步修飾 $query = $customer->getOrders(); // 無 SQL,回傳 ActiveQuery $bigOrders = $customer->getBigOrders(200)->all(); // 重新查詢,帶參數 // 強制重新載入 unset($customer->orders); $orders = $customer->orders; // 再次查詢
-- $customer = Customer::findOne(1); SELECT * FROM customer WHERE id=1; -- $orders = $customer->orders; SELECT * FROM order WHERE customer_id=1;
class Order extends ActiveRecord {
// 方式一:viaTable() 直接指定中間表
public function getItems() {
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->viaTable('order_item', ['order_id' => 'id']);
}
// 方式二:via() 透過已定義關聯
public function getOrderItems() {
return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
}
public function getItemsVia() {
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems'); // 關聯方法名
}
}
SELECT * FROM item WHERE id IN (SELECT item_id FROM order_item WHERE order_id=1);
class Customer extends ActiveRecord {
// 方法參數傳遞條件
public function getOrdersByStatus($status) {
return $this->hasMany(Order::className(), ['customer_id' => 'id'])
->andWhere(['status' => $status]);
}
// 關聯範圍
public function getRecentOrders($days = 30) {
return $this->hasMany(Order::className(), ['customer_id' => 'id'])
->where(['>=', 'created_at', date('Y-m-d', strtotime("-$days days"))]);
}
}
// 使用
$orders = $customer->getOrdersByStatus('paid')->all();
$recent = $customer->getRecentOrders(7)->all();
// ❌ N+1 查詢:1 筆客戶 + N 筆訂單 = N+1 次 SQL
$customers = Customer::find()->all();
foreach ($customers as $c) { echo count($c->orders); }
// ✅ 急切載入:2 次 SQL (IN 子查詢或 JOIN)
$customers = Customer::find()->with('orders')->all();
foreach ($customers as $c) { echo count($c->orders); }
// 多層急切載入
$orders = Order::find()->with(['customer', 'items.category'])->all();
// 急切載入帶條件
$customers = Customer::find()->with([
'orders' => function($q) { $q->where(['status' => 'paid'])->orderBy('id DESC'); }
])->all();
$order = new Order();
$order->customer_id = $customer->id;
$order->save();
// 或使用 link() 自動處理外鍵
$order = new Order(['amount' => 100]);
$customer->link('orders', $order); // 自動填 customer_id 並儲存
// 解除關聯 (不刪除 Order,只清外鍵)
$customer->unlink('orders', $order, true); // true = 刪除關聯記錄 (中間表才用)
// 多對多 link
$order->link('items', $item); // 自動寫入 order_item
| 問題 | 症狀 | 解決方案 |
|---|---|---|
| N+1 查詢 | 迴圈中存取關聯屬性 | 使用 with() 急切載入 |
| 重複查詢 | 同一請求多次存取同關聯 | 屬性語法自動快取;跨請求用 Redis 快取結果 |
| 過度 JOIN | joinWith() 導致巨大結果集 | 改用 with() 分開查詢,或只 SELECT 必要欄位 |
| 缺少索引 | 關聯欄位 (FK) 無索引 | 遷移檔加 $this->createIndex('idx_customer_id', 'order', 'customer_id') |
| 大量資料載入 | all() 記憶體爆炸 | 用 each() 批次處理、cursor() 游標、分頁 |
Yii2 ActiveRecord 關聯查詢核心在於:正確宣告 hasOne/hasMany、善用 with() 急切載入、理解屬性語法快取機制、中間表用 viaTable/via、動態條件用方法語法。配合索引優化、必要時降級用 DAO/SQL,即能兼顧開發效率與執行效能。
相關專題:《Yii 框架入門及常用技巧總結》、《PHP 優秀開發框架總結》。