iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Claude AI

用 AI Agent 重構一套無框架的 legacy PHP 系統系列 第 8

Day 08:SQL 一律經過 Repository——把裸寫 SQL 的 legacy 程式碼收斂

  • 分享至 

  • xImage
  •  

前言:AI 看得懂 SQL,為什麼還要多包一層?

「AI 又不是看不懂 SQL,裸寫 SQL 讓它直接改,不是比多包一層 Repository 介面更快嗎?」

這是我在推動這條規則時最常被問到的問題。乍聽有道理——AI 確實能讀懂一段 SELECT ... WHERE ...,也能照著語法改出一段新的。但「看得懂」跟「能安全改」是兩件完全不同的事。今天要講的,就是為什麼在這套系統裡,任何要碰資料庫的程式碼一律得經過 Repository,不能在呼叫端裸寫 SQL 字串——而且這條規則對 AI 重構的意義,比對人類工程師更關鍵。

今日目標

  • 理解「AI 看得懂 SQL」跟「AI 能安全改 SQL」之間的落差在哪裡
  • 具體感受裸寫 SQL 對 AI 重構造成的兩種窮舉困境
  • 認識 Repository 收斂了哪些機械式、容易出錯的工作
  • 建立「語言無關的設計原則」跟「這個專案的具體實現方式」的分層認知
  • 為明天的 Repository 重構實戰案例打底

裸寫 SQL 對 AI 重構特別危險的原因:兩個窮舉不完的問題

先講清楚今天的核心論點:AI 對著一段裸寫 SQL 判斷「這樣改應該安全」的時候,其實有兩件事它窮舉不完,卻常常表現得好像已經窮舉完了。

第一個窮舉不完的問題:這段 SQL 字串(或它的變體)在全站被複製貼上過幾次? Legacy 系統活得越久,同一段查詢邏輯被複製貼上、微調欄位、再複製貼上的機率就越高。AI 改掉眼前這一處,看起來測試綠燈、邏輯正確,但它沒有辦法保證這是唯一一處,也沒辦法保證其他複製出去的版本跟眼前這份完全一致——可能欄位順序不同、可能多了一個條件、可能悄悄修過一次沒人記得。裸寫 SQL 沒有任何機制強迫這些複製品保持同步,AI 只能看見它當下打開的這個檔案。

第二個窮舉不完的問題:這段 SQL 裡的欄位,到底對應哪一條資料庫連線? 在一個有多套子系統並存、資料庫連線分好幾條的架構裡(例如全站共用的一條連線、各租戶各自獨立的一條連線),裸寫 SQL 完全沒辦法從語法上看出「這個查詢應該打到哪一條連線」——這件事只存在於呼叫端當時怎麼拿到 PDO 物件的邏輯裡,往往深藏在好幾層函式呼叫之外。AI 如果只看著 SQL 字串本身判斷「這樣改沒問題」,其實完全沒有觸及這個真正危險的維度。

這正是 Day 01 那句話在資料庫層的具體樣貌:AI 對著一段裸寫 SQL 給出的「已確認安全」,其實只確認了語法正確,沒有確認這段查詢在全站的重複程度、也沒有確認它打的是哪一條連線。

Repository 收斂了什麼:把機械式工作變成安全介面

Repository 層的價值,不是「把 SQL 包起來讓程式碼比較好看」這種表面理由,而是把兩類高風險的機械式工作,收斂成一個 AI 只需要呼叫、不需要自己組字串的安全介面:

  • 組 WHERE 子句:條件陣列轉成安全的 WHERE 子句,這件事機械、重複、容易在手動組字串時漏處理某個邊界(例如某個條件值是 null 時該產生 IS NULL 還是被跳過)。
  • 處理 IN 陣列的具名 placeholder:手寫 SQL 處理陣列參數時,最容易漏掉的就是 placeholder 數量要跟陣列長度動態對齊——這種問題不會在開發時被發現,會在陣列長度剛好等於 1 或超過某個數量時才炸開。
  • 決定要打哪一條連線:Repository 依照自己所在的分類(例如「全站共用」還是「租戶專屬」)固定綁定對應的連線,呼叫端完全不用、也不能自己決定要用哪條連線——這個決策從「藏在呼叫邏輯裡、容易被複製貼上時弄錯」變成「寫程式碼的當下就被目錄結構逼著選對」。

用一組對照來看這個差異:

❌ 呼叫端裸寫 SQL:
$sql = "SELECT * FROM some_table WHERE status = " . $status
     . " AND site_id IN (" . implode(',', $siteIds) . ")";
$stmt = $pdo->query($sql);
→ 手動組字串、手動處理 IN 陣列、$pdo 從哪條連線來的完全看不出來,
  改動這段程式碼的人(不管是 AI 還是人類)只能靠讀懂整個呼叫鏈才知道風險在哪

✅ 經過 Repository:
$rows = $repository->findWhere([
    'status'  => $status,
    'site_id' => $siteIds,
]);
→ WHERE 子句、IN 陣列 placeholder 都由 Repository 內部安全組出;
  改動的人只需要知道「這支 Repository 對應哪條連線」這一件事,
  不需要重新驗證組 SQL 字串的邏輯有沒有漏洞

Repository 把「AI 需要自己判斷安全性」的機械式工作,收斂成「AI 只需要呼叫一個已經驗證過的介面」——這跟 Day 04 講的覆蓋率門檻是同一種思路:不是要求 AI 更謹慎,而是把它需要承擔的風險範圍縮小到一個可以被驗證的邊界。

原則語言無關,實現方式是這個專案的選擇

這裡要補上一層區分:「資料庫存取一律經過 Repository、不在呼叫端裸寫查詢」是一個語言無關的設計原則——Java 生態的 Repository Pattern、C# 的 Repository + Unit of Work、TypeScript 專案裡 ORM 的 Repository 概念,講的都是同一件事:把資料存取邏輯收斂到一層,呼叫端只依賴介面,不直接碰底層查詢語法。

今天範例裡看到的 findWhere() 這種條件陣列 DSL,只是這個 PHP 專案選擇的具體實現方式,換一個專案可能會用 Query Builder、可能會用 ORM 的 fluent interface,形式不同,但「不讓呼叫端自己組查詢字串」這個判斷邏輯是一致的。重點不是記住這支介面長什麼樣子,而是記住:只要你的重構對象有裸寫查詢字串的地方,就值得問一句「這段邏輯有沒有機會收斂成一個介面,讓改動者不用重新驗證組字串的安全性」

今日思考題

回想你手上的 legacy 系統:有沒有哪段裸寫 SQL(或裸寫查詢語法)你自己也說不清楚它在全站被複製貼上過幾次?如果讓 AI 去改其中一處,你有把握它會意識到還有其他份複製品存在嗎?

今日重點回顧

  • 裸寫 SQL 對 AI 重構危險,是因為 AI 沒辦法窮舉「這段邏輯在全站被複製過幾次」跟「這段查詢對應哪一條資料庫連線」
  • Repository 把組 WHERE 子句、處理 IN 陣列 placeholder、決定連線這幾件高風險機械工作收斂成安全介面
  • 核心思路跟覆蓋率門檻一致:不是要求 AI 更謹慎,而是縮小它需要承擔的風險範圍
  • 「一律經過 Repository」是語言無關的設計原則,具體 DSL 只是這個專案的實現選擇

明日預告

明天用一個真實案例把今天的原則落地:一個 Controller 直接查資料庫,重構成經過 Repository 的版本時,實際會踩到哪些意料之外的細節。


上一篇
Day 07:為什麼要在獨立 subagent/子流程裡跑驗證迴圈
系列文
用 AI Agent 重構一套無框架的 legacy PHP 系統8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言