昨天看到一個只有十幾行、把檔案路徑換成人類看得懂的結構的類別。但這裡有一個沒有講完的問題:如果系統已經上線一段時間,累積了大量用舊路徑規則儲存的檔案,換了新規則之後,這些舊檔案要怎麼辦?
新的程式碼會用新規則去讀取路徑,但檔案實際上還躺在舊路徑底下——不做點什麼,這些歷史檔案就會變成「資料庫裡有紀錄、但實際路徑對不上」的孤兒資料。今天要看的是一支專門處理這個問題的 Artisan command,它的設計細節,是任何要寫資料遷移工具的人都該參考的範本。
寫一支資料遷移工具,最危險的地方在於它通常是「一次性」執行、「大規模」影響資料、而且很多操作是不可逆的——尤其是牽涉到搬移或刪除實體檔案的情境,一旦搬錯,找不到原始檔案就真的找不回來了。
正因為風險這麼高,一支寫得好的遷移 command,不會是「一行迴圈把所有資料跑過一遍」這麼簡單,而是要在設計上主動降低犯錯的代價。
這個系列的素材專案裡,有一支負責把媒體檔案從舊路徑結構搬到新路徑結構的 command,具備三個值得逐一拆解的設計。
第一,--dry-run:先看計畫,不動手。 這支 command 支援一個選項,開啟後只會印出「哪個檔案會被搬到哪裡」的完整清單,不會真的移動任何檔案。這讓操作者可以在正式執行前,先確認遷移計畫符合預期——如果 dry-run 的輸出裡出現不合理的路徑,代表遷移邏輯本身可能有問題,這時候還沒真的動到任何檔案,回頭修邏輯的代價是零。
第二,chunk() 分批處理。 command 沒有一次把所有媒體紀錄全部載入記憶體處理,而是用 Laravel 的 chunk() 方法,一次只處理一小批(一百筆),處理完這批才去讀下一批。這個設計在資料量大的時候特別重要:一次載入全部資料容易撐爆記憶體,而分批處理也讓整個遷移過程可以在中途觀察進度、甚至在必要時安全中斷。
第三,逐筆比對、統計三種結果,而不是只回報成功/失敗。 這是最容易被忽略、但其實最重要的設計。這支 command 對每一筆媒體紀錄,會先比對新舊路徑——如果新舊路徑其實一樣(代表這筆資料原本就已經符合新規則,不需要搬),計入「跳過」;如果來源檔案在磁碟上找不到(代表資料庫紀錄跟實際檔案系統已經不一致,這是遷移過程中才會揭露的既有問題),計入「找不到」,並且用警告訊息點名是哪一筆,而不是讓整個遷移中斷;只有真的需要搬、也真的搬成功的,才計入「已搬移」。
用一組對照來看第三點的價值:
❌ 只回報「處理了幾筆」「失敗了幾筆」
→ 「找不到來源檔案」跟「這筆資料本來就不用搬」
被混在一起計算,操作者沒辦法從最終報告判斷
「找不到」的那幾筆是不是資料本身已經有問題,
只能自己再翻一次 log 才知道
✅ 拆成「已搬移」「跳過」「找不到」三種統計
→ 執行完直接看報告就知道:有多少筆是正常遷移完成、
有多少筆本來就不需要動、有多少筆代表資料庫跟
實際檔案系統已經不一致——這第三類數字,
往往是遷移過程額外揭露出來的既有問題,
值得獨立追蹤,不該被混進「失敗」裡草草帶過
「找不到來源檔案」不必然是這次遷移造成的問題,很可能是更早之前就存在的資料完整性缺口,只是沒有人去檢查過。用一支遷移工具順便把這類既有問題攤開來看,是這個設計額外帶來的價值。
另一個值得注意的細節:遇到找不到來源檔案這種情況,command 選擇用 $this->warn() 印出警告訊息、記一筆統計,然後繼續處理下一筆,而不是直接拋出例外中斷整個遷移。
這個選擇背後的判斷是:資料遷移工具處理的往往是幾百、幾千筆資料,如果因為其中一筆有問題就讓整個流程中斷,操作者要嘛得手動排除掉那筆問題資料才能重跑,要嘛每次中斷都要從頭排查——把單筆的問題留下記錄、讓其他健康的資料正常完成遷移,事後再統一處理那份「找不到」的清單,是效率跟穩健性都更好的做法。
如果你要寫一支資料遷移工具,除了「dry-run」跟「分批處理」,你會怎麼設計統計報告的分類?回想你維護過的系統,有沒有類似「資料庫紀錄跟實際狀態不一致」的既有問題,是可以透過一次遷移工具順便揭露出來的?
--dry-run 先看計畫不動手、chunk() 分批處理避免撐爆記憶體、統計拆成三種結果而不是只有成功/失敗明天要看一個套件能力邊界的案例:一個原生只處理 Model CRUD 異動的稽核套件,怎麼被延伸去涵蓋「登入」這種不屬於 Model CRUD 的事件。