iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 10

Day 10:一支 Command,修一次資料遷移後留下的欄位缺失

  • 分享至 

  • xImage
  •  

前言:不是所有 Command 都要設計成可以重複執行

「Artisan Command 不是應該寫成通用、可重複執行的工具嗎?」

大部分教學會教你把 Command 設計成通用工具——吃參數、可以重複跑、要考慮各種邊界情況。但真實系統裡,也有另一種完全合理的 Command 用途:一次性地修正一批資料,跑完這一次,這支 Command 的任務就結束了。今天要看的就是這種案例。

今日目標

  • 認識 Laravel Artisan Command 的基本結構
  • 看一個真實案例:一支只為了修正一次資料缺失而寫的 Command
  • 理解「一次性 Command」跟「通用工具型 Command」在設計上的差異
  • 知道從舊系統匯入資料時,欄位缺失是很常見的真實情境

本文主體

一支只跑一次的 Command 長什麼樣

這個系統從舊系統遷移資料的過程裡,發現一批新聞資料缺少了某個欄位的值——這個欄位在遷移當下沒有被正確帶過來。修法是寫一支專門處理這個缺失的 Command:

class FixNewsGraph extends Command
{
    protected $signature = 'fix:news-graph';

    protected $description = 'Command description';

    public function handle(): int
    {
        Utils::csvReader(resource_path('seeders/graph.csv'))->each(function (array $row) {
            if (empty($row['apply_no'])) {
                return;
            }

            when(
                News::query()->where('foreign_id', '=', $row['apply_no'])->first(),
                static fn (News $news) => $news->fill(['graph' => $row['graph']])->save()
            );
        });

        News::query()
            ->where('content', 'LIKE', '%class="news_image"%')
            ->each(fn (News $news) => $news->touch());

        return self::SUCCESS;
    }
}

邏輯很直接:讀一份 CSV,逐行用 foreign_id(對應舊系統的識別碼)去查對應的新聞資料,找到就補上缺失的欄位值,找不到就直接跳過(when() 是一個輔助函式,第一個參數是 null 或 falsy 時,第二個 callback 就不會被執行)。

為什麼不用「防呆」寫法

如果照著通用工具的標準來看,這支 Command 有很多「不夠嚴謹」的地方——沒有 --dry-run 選項先看看會影響哪些資料、找不到對應資料時只是安靜跳過,沒有留下任何記錄、$description 甚至還留著預設值沒改。但這些「不足」其實是刻意的:這支 Command 的任務範圍非常明確,就是「把這批已知的資料缺失修正一次」,跑完之後不會再被呼叫第二次。 幫一支只會執行一次的工具做完整的錯誤處理、記錄、互動確認,投入的心力跟它實際的使用壽命不成比例。

順手處理的一個副作用

注意最後一段:

News::query()
    ->where('content', 'LIKE', '%class="news_image"%')
    ->each(fn (News $news) => $news->touch());

這段在做的事,是把「內容裡含有特定 CSS class 的圖片」的新聞資料全部 touch() 一次——touch() 會更新這筆資料的 updated_at 時間戳,但不改變任何實際內容。這通常是為了觸發某個依賴「資料被更新」這件事的機制(例如快取失效、或某個監聽 updated 事件的 Observer 要重新處理內容)。這種「順手在同一支修復 Command 裡處理相關的副作用」,是真實維運工作常見的模式——不是每個問題都值得開一支獨立的 Command,把相關聯的修正放在同一次執行裡,反而更貼近實際發生問題的脈絡。

從舊系統遷移資料,欄位缺失是常態

這支 Command 之所以存在,背後的脈絡是這個系統曾經從一套舊系統把歷史資料整批遷移過來。從舊系統匯入資料時,欄位對不齊、某些衍生欄位在舊系統裡沒有直接對應、遷移腳本漏算了某個欄位——這些狀況幾乎是資料遷移的常態,不是例外。 與其在遷移當下想辦法把每一種邊界情況都處理到完美,更務實的做法往往是:遷移完成後,用類似這支 Command 的方式,針對事後發現的具體缺失,各自寫一支小工具補救。

❌ 為了一次性任務投入通用工具等級的設計

class FixNewsGraph extends Command
{
    protected $signature = 'fix:news-graph
                            {--dry-run : 只顯示會影響的資料,不實際執行}
                            {--chunk=100 : 每批處理筆數}';
    // 加上完整的進度條、記錄、錯誤處理...
}

如果這支 Command 真的只會執行一次,投入這些設計換來的維護價值有限。

✅ 針對明確的一次性任務,寫最直接的修正邏輯

public function handle(): int
{
    // 讀取資料、逐筆修正,找不到就跳過
    return self::SUCCESS;
}

(Day 28 會看到另一支資料遷移 Command,設計成完全相反的風格——因為那支要處理的是全站規模的路徑搬遷,風險等級不一樣,值得投入 --dry-run/分批處理/完整統計,兩者的差異正好可以對照著看。)

今日思考題

你的專案裡有沒有為了處理一次性的資料問題,寫過一支「夠用就好」的 Command?回頭看的話,那個「夠用就好」的判斷,事後證明是對的嗎?

今日重點回顧

  • 不是所有 Command 都要設計成通用、可重複執行的工具,一次性的資料修正任務有自己合理的簡化空間
  • 真實案例:FixNewsGraph 讀一份 CSV,逐筆比對舊系統識別碼、補上缺失欄位,找不到就跳過
  • 同一支 Command 裡順手處理了相關的副作用(touch() 觸發依賴更新時間的機制),貼近實際問題發生的脈絡
  • 從舊系統遷移資料時,欄位缺失是常態,用小型一次性 Command 補救,往往比追求遷移當下的完美更務實

明日預告

明天要看排程機制——這個系統怎麼設計 sitemap 自動產生,而且刻意把「產生」跟「回應請求」拆成兩件事。


上一篇
Day 09:角色可見性控制——為什麼最高權限角色不能靠一般權限判斷保護
下一篇
Day 11:排程 sitemap 自動產生——為什麼「產生」跟「回應請求」要刻意拆開
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言