在 Zend Framework 1.x 專案中,Zend_Db_Table_Abstract 是資料表模型的基類。本文示範如何在其內部(或關聯的 Mapper 類)使用 Zend_Db_Select 物件構建複雜查詢,特別是多表 JOIN 與 JOIN LEFT 操作,並輸出關聯資料陣列。
-- 充值記錄主表
CREATE TABLE `user_charges` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT(10) NOT NULL,
`charge_type` INT(3) NOT NULL,
`charge_subtype` INT(3) NOT NULL,
`charge_credits` INT(3) NOT NULL,
`buy_date` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
`valid_to` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
`next_charge_date` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
`payment` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 充值日誌詳情表
CREATE TABLE `charge_logs` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`charge_id` INT(11) NOT NULL,
`title` TEXT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 測試資料
INSERT INTO `user_charges` VALUES (1, 1, 1, 1, 100, '2011-09-09', '2011-09-16', '0000-00-00', 'paypal');
INSERT INTO `charge_logs` VALUES (1, 1, 'XXXXXXX');
class UserCharges extends Zend_Db_Table_Abstract
{
protected $_name = 'user_charges';
public function getChargesWithLogs($userId)
{
// 取得 Select 物件
$select = $this->getAdapter()->select();
// 建構查詢:FROM user_charges uc LEFT JOIN charge_logs cl
$select->from(array('uc' => 'user_charges'))
->joinLeft(
array('cl' => 'charge_logs'), // 表別名 => 表名
'uc.id = cl.charge_id', // JOIN 條件
array('title') // 只選取需要的欄位
)
->where('uc.user_id = ?', $userId); // 參數綁定,防 SQL 注入
// 執行並返回陣列
return $this->getAdapter()->fetchAll($select);
}
}
class UserChargesMapper
{
protected $_dbTable;
public function setDbTable($dbTable)
{
if (is_string($dbTable)) {
$dbTable = new $dbTable();
}
if (!$dbTable instanceof Zend_Db_Table_Abstract) {
throw new Exception('Invalid table data gateway provided');
}
$this->_dbTable = $dbTable;
return $this;
}
public function getDbTable()
{
if (null === $this->_dbTable) {
$this->setDbTable('UserCharges');
}
return $this->_dbTable;
}
public function fetchChargesWithLogs($userId)
{
$select = $this->getDbTable()->getAdapter()->select();
$select->from(array('uc' => 'user_charges'))
->joinLeft(array('cl' => 'charge_logs'), 'uc.id = cl.charge_id', array('title'))
->where('uc.user_id = ?', $userId);
return $this->getDbTable()->getAdapter()->fetchAll($select);
}
}
array(1) {
[0] => array(10) {
["id"] => string(1) "1"
["user_id"] => string(1) "1"
["charge_type"] => string(1) "1"
["charge_subtype"] => string(1) "1"
["charge_credits"] => string(3) "100"
["buy_date"] => string(19) "2011-09-09 00:00:00"
["valid_to"] => string(19) "2011-09-16 00:00:00"
["next_charge_date"] => string(19) "0000-00-00 00:00:00"
["payment"] => string(6) "paypal"
["title"] => string(7) "XXXXXXX"
}
}
| 方法 | SQL 對應 | 用途 |
|---|---|---|
join($name, $cond, $cols) | INNER JOIN | 只返回兩表都匹配的行 |
joinLeft($name, $cond, $cols) | LEFT JOIN | 返回左表所有行,右表無匹配則為 NULL |
joinRight($name, $cond, $cols) | RIGHT JOIN | 返回右表所有行,左表無匹配則為 NULL |
joinFull($name, $cond, $cols) | FULL JOIN | 返回兩表所有行(MySQL 不支援,會模擬) |
joinCross($name, $cond, $cols) | CROSS JOIN | 笛卡爾積 |
joinNatural($name, $cols) | NATURAL JOIN | 自動按同名欄位連接 |
$name:array('別名' => '表名') 或直接 '表名'(不建議,無別名易衝突)$cond:JOIN 條件字串,如 'uc.id = cl.charge_id',建議使用別名$cols:要選取的欄位陣列,如 array('title', 'created_at');* 表所有欄位;false/空陣列表不選取任何欄位$select->from(array('uc' => 'user_charges'), array('id', 'user_id', 'charge_type'))
->joinLeft(array('cl' => 'charge_logs'), 'uc.id = cl.charge_id', array('log_title' => 'title'));
-- 將 cl.title 別名為 log_title,避免與 uc.title 衝突(若存在)
$select->from(array('a' => 'table_a'))
->joinLeft(array('b' => 'table_b'), 'a.id = b.a_id', array('b_name'))
->joinLeft(array('c' => 'table_c'), 'b.id = c.b_id', array('c_name'))
->where('a.status = ?', 1);
$select->from(array('u' => 'users'), array(
'id',
'full_name' => new Zend_Db_Expr("CONCAT(first_name, ' ', last_name)")
));
| 錯誤 | 原因 | 修正 |
|---|---|---|
| 欄位名衝突導致資料覆蓋 | 兩表有同名欄位且未指定別名 | 明確指定 $cols 並使用別名 |
| JOIN 條件寫錯表別名 | 未使用 from() 定義的別名 | 統一使用陣列別名 array('uc' => 'table') |
| SQL 注入風險 | 直接拼接變數到條件字串 | 使用參數綁定 where('col = ?', $val) |
| 效能差 | 未加索引 / 選取過多欄位 / 大表無 LIMIT | 加索引、只選需要欄位、分頁 |
Zend_Db_Select 提供了流暢介面構建複雜 SQL,配合 Zend_Db_Table_Abstract 的 getAdapter() 可靈活實現各類 JOIN 查詢。關鍵點:使用陣列別名、明確指定選取欄位、善用參數綁定、理解各 JOIN 類型語義。Mapper 模式進一步分離關注點,讓模型專注業務邏輯,資料存取邏輯集中維護,是中大型 Zend 專案的推薦架構。