Laravel送出同一表單兩次:讓資料庫唯一條件擋重複

訪客連按兩次送出,資料表多了兩筆相同詢問。把按鈕停用能減少誤按,但請求也可能被重試,或從另一個視窗送來。若產品要求同一次提交只保存一筆,資料庫需要有能識別這次提交的唯一條件。

Laravel 的 unique 驗證規則可以在保存前提示已有資料;資料庫的 unique constraint 則在真正寫入時阻止違反條件的紀錄。兩者處理的時點不同,不能用一次驗證通過,就保證稍後寫入時仍然沒有重複。

先定義什麼叫同一次提交

先選業務上穩定的識別方式。這篇以同一使用者、同一 submission_key 為例,要求這個組合只能存在一次。兩個不同使用者可以有相同的示例字串,但同一使用者重送同一個 key,不能新增第二筆。

實際網站應由伺服器產生或依既有協定驗證提交識別碼,讓同一次請求的重試沿用相同值。不要每次送出時都重新產生 key,否則兩次請求會被視為不同提交。也不要把「訊息文字完全一樣」直接當作重複,訪客可能隔天真的要再詢問同一個問題。

識別碼的範圍、有效期間和重試規則都要由產品決定。付款、訂單或外部 API 的重試還涉及其他副作用;這篇只處理資料表的一筆保存,不把唯一索引稱為完整冪等機制。

在資料表建立真正的唯一條件

Laravel migration 可替兩個欄位建立複合唯一索引:

$table->unsignedBigInteger('user_id');
$table->string('submission_key');
$table->string('message');
$table->unique(
    ['user_id', 'submission_key'],
    'submissions_user_key_unique'
);

這裡兩個識別欄位都不是 nullable。若允許 NULL,唯一限制對 NULL 的行為需看使用的資料庫,不能假定空值也會形成同一個提交。欄位型別、長度與比較方式,也應符合實際 key 的格式。

替已有資料表增加限制前,先盤點目前是否已存在相同組合。不能直接上線 migration,再碰到重複資料才臨時刪除。由維護者確認該保留哪筆、其他資料有沒有引用,以及備份和回復方法,再安排正式變更。

驗證規則仍然保留,條件要與資料庫一致

前置驗證可以讓訪客較早收到清楚提示。複合唯一條件的驗證也要包含使用者範圍,而不是把所有人的 key 當成全站唯一:

'submission_key' => [
    'required',
    Rule::unique('submissions')
        ->where(fn ($query) =>
            $query->where('user_id', $request->user()->id)
        ),
],

這是已驗證登入身分的應用程式片段,Rule 需正確匯入。使用者 ID 應取自伺服器確認的身分,不是由表單任意指定。提交 key 本身也需依產品協定核對,不能只要有字串就認定它代表合法的提交。

驗證與資料庫的條件要一致。驗證查全站唯一,資料庫卻只限制同一使用者,會錯擋合法資料;反過來則可能讓驗證通過,寫入卻被另一個更嚴格的條件擋下。先把實際索引欄位與規則並排核對。

兩次驗證都通過,第二次仍被擋下

在隔離的 Laravel 13 與合成 SQLite 案例中,我們先建立空資料表,令請求 A、B 帶同一組 user_id=1 和 submission_key=demo-request-A。在任何寫入之前,先後執行兩次 Validator,兩次都通過。

接著保存 A,得到一筆資料。保存 B 時,SQLite 回報複合唯一條件衝突,Laravel 拋出 UniqueConstraintViolationException;最後重新計數,資料表只有一筆。再驗證同一組識別值,這時前置驗證也會失敗。

A 驗證:通過
B 驗證:通過
A 寫入:新增第 1 筆
B 寫入:唯一條件衝突
資料表:仍為 1 筆

這個順序刻意重現「檢查時還沒有,寫入時已經有」的間隔,沒有同時啟動兩條執行緒。它能說明為何前置檢查不能代替寫入限制;自己的網站若有併發要求,仍應在實際資料庫與請求流程另做對應測試。

只處理預期的重複,不把所有錯誤吞掉

保存時可以針對預期的唯一衝突安排回應。但一張資料表可能有多個唯一索引,不能看到這類例外就全部當成「已經送過」。核對衝突是否來自這次提交的識別條件,再依產品規則找回既有結果,或提示使用者不要再次建立。

如果相同 key 帶來不同訊息,應如何處理也要先定義。回傳已有紀錄、不允許改內容,或另外提供編輯流程,各自有不同用途。不要悄悄用第二次內容覆蓋第一筆,卻仍把功能描述成「避免重複」。

其他資料庫錯誤,例如連線失敗、欄位長度或另一個約束衝突,應保留原有錯誤處理。不要捕捉所有例外後一律回成功,否則資料根本沒保存,訪客卻以為網站已收到。

外部動作仍要安排在自己的流程裡

若在保存之前已寄信、扣款或呼叫外部服務,第二筆資料被擋下,也不會自動撤銷那些動作。先畫出目前順序:驗證、授權、保存、交易完成及外部通知,再決定哪些工作需要以既有紀錄或可靠的任務流程執行。

資料庫交易能處理適用範圍內的資料變更,不能保證外部系統跟著回復。這篇的案例沒有寄信或付款;涉及這些動作時,應另依服務商的重試與冪等協定處理,不把一個本地唯一索引套成所有問題的答案。

最後核對合法的新提交仍然可用

至少測三種資料:同一使用者同一 key、同一使用者新 key,以及另一使用者的合規提交。第一種按規則擋重複,其他種類是否可以保存則依你定義的範圍判斷。也確認拒絕後沒有多一筆、沒有改掉第一筆內容。

前端停用按鈕仍可保留,幫助訪客理解送出中的狀態;完成或失敗後要有清楚回饋,讓他知道能否重試。前端減少誤按、驗證提早提示、資料庫守住唯一條件,三者各自在需要的位置工作。

參考資料

原文鏈接:https://wntheme.com/laravel-unique-form-submission/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?