使用 CDbCriteria::join 進行關聯刪除時,Yii 1.x 核心類 CDbCommandBuilder::createDeleteCommand() 生成的 SQL 缺少目標表別名,導致 MySQL 語法錯誤。本文深度分析錯誤原因,提供核心類修復補丁與不修改框架代碼的替代方案。
$criteria = new CDbCriteria();
$criteria->join = 'LEFT JOIN {{positions}} p ON p.zpo_id = t.zpo_id';
$criteria->addCondition("p.zpo_type = 1");
$criteria->addCondition("t.zpl_content_id IN ($ids)");
PosLog::model()->deleteAll($criteria);
DELETE FROM `zd_pos_log` LEFT JOIN zd_positions p ON p.zpo_id = t.zpo_id WHERE (p.zpo_type = 1) AND (t.zpl_content_id IN (76))
錯誤:You have an error in your SQL syntax near 'LEFT JOIN...' — MySQL 多表 DELETE 必須指定刪除目標表別名:DELETE t FROM ...
DELETE t FROM `zd_pos_log` t LEFT JOIN zd_positions p ON p.zpo_id = t.zpo_id WHERE (p.zpo_type = 1) AND (t.zpl_content_id IN (76))
// framework/db/schema/CDbCommandBuilder.php
public function createDeleteCommand($table, $criteria) {
$this->ensureTable($table);
$sql = "DELETE FROM {$table->rawName}"; // 缺少別名
$sql = $this->applyJoin($sql, $criteria->join);
$sql = $this->applyCondition($sql, $criteria->condition);
// ... 其他子句
return $command;
}
當 $criteria->join 非空時,生成的 SQL 缺少 DELETE 別名 FROM 表 別名 結構。
// 修改 framework/db/schema/CDbCommandBuilder.php
public function createDeleteCommand($table, $criteria, $alias = 't') {
$this->ensureTable($table);
$alias = $this->_schema->quoteTableName($alias);
if (empty($criteria->join)) {
$sql = "DELETE FROM {$table->rawName}";
} else {
// 關鍵修復:添加目標表別名
$sql = "DELETE {$alias} FROM {$table->rawName} {$alias}";
}
$sql = $this->applyJoin($sql, $criteria->join);
$sql = $this->applyCondition($sql, $criteria->condition);
$sql = $this->applyGroup($sql, $criteria->group);
$sql = $this->applyHaving($sql, $criteria->having);
$sql = $this->applyOrder($sql, $criteria->order);
$sql = $this->applyLimit($sql, $criteria->limit, $criteria->offset);
$command = $this->_connection->createCommand($sql);
$this->bindValues($command, $criteria->params);
return $command;
}
$sql = "DELETE t FROM {{pos_log}} t
LEFT JOIN {{positions}} p ON p.zpo_id = t.zpo_id
WHERE p.zpo_type = 1 AND t.zpl_content_id IN ($ids)";
Yii::app()->db->createCommand($sql)->execute();
$ids = Yii::app()->db->createCommand()
->select('t.zpl_content_id')
->from('{{pos_log}} t')
->join('{{positions}} p', 'p.zpo_id = t.zpo_id')
->where('p.zpo_type = 1 AND t.zpl_content_id IN (' . $ids . ')')
->queryColumn();
if ($ids) {
PosLog::model()->deleteAllByPk($ids); // 或 deleteAll(['IN', 'zpl_content_id', $ids])
}
class PosLog extends CActiveRecord {
public function deleteAll($criteria = null) {
if ($criteria && $criteria->join) {
// 關聯刪除走 DAO
$builder = Yii::app()->db->getSchema()->getCommandBuilder();
// 手動構建正確 SQL...
return $this->getCommandBuilder()->createDeleteCommand($this->getTableSchema(), $criteria)->execute();
}
return parent::deleteAll($criteria);
}
}
| 方案 | 優點 | 缺點 | 推薦 |
|---|---|---|---|
| 修改核心類 | 徹底修復、所有模型受益 | 升級覆蓋、違反框架封裝、團隊協作風險 | ⭐ |
| DAO 手寫 SQL | 完全可控、性能最好 | 失去 AR 便利、SQL 分散 | ⭐⭐⭐⭐ |
| 子查詢 EXIST | 純 AR 寫法、無需 JOIN | MySQL 優化器可能不走索引、慢查詢風險 | ⭐⭐⭐ |
| 兩步法 (查ID→刪除) | 邏輯清晰、易調試、支持大批量分批 | 兩次查詢、並發窗口理論存在 (極低概率) | ⭐⭐⭐⭐⭐ |
| 覆寫模型方法 | 封裝在模型層、調用處無感 | 需每個關聯刪除模型都覆寫 | ⭐⭐⭐ |
// Yii 2.0 QueryBuilder::delete()
public function delete($table, $condition, $params = []) {
$sql = "DELETE FROM {$table}";
if (is_array($condition)) {
// 正確處理 JOIN 情況
$sql = "DELETE t FROM {$table} t ...";
}
// ...
}
Yii 1.x CDbCommandBuilder::createDeleteCommand() 在處理 CDbCriteria::join 時存在缺陷,生成無效 SQL。生產環境強烈建議使用「兩步法:先查主鍵 ID,再批量刪除」,邏輯清晰、性能可控、無框架耦合風險。若項目大量關聯刪除,可封裝基類 BaseActiveRecord::deleteAllWithJoin() 統一處理。升級至 Yii 2.0 可從根源解決此問題。