Commit 4e570389 by Seefs Committed by GitHub

fix: use GORM v2 row locking for subscription resets (#6057)

* fix: use GORM v2 row locking for subscription resets

* docs: codify GORM row locking rules
parent 246d62aa
...@@ -80,6 +80,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag ...@@ -80,6 +80,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
- Prefer GORM methods (`Create`, `Find`, `Where`, `Updates`, etc.) over raw SQL. - Prefer GORM methods (`Create`, `Find`, `Where`, `Updates`, etc.) over raw SQL.
- Let GORM handle primary key generation; do not use `AUTO_INCREMENT` or `SERIAL` directly. - Let GORM handle primary key generation; do not use `AUTO_INCREMENT` or `SERIAL` directly.
- Standard `SELECT ... FOR UPDATE` row locks built with GORM query methods in `model/` MUST use `lockForUpdate(tx)`. Do not use the legacy GORM v1 pattern `tx.Set("gorm:query_option", "FOR UPDATE")`, because GORM v2 silently ignores it and no lock is acquired. Do not duplicate `clause.Locking{Strength: "UPDATE"}` at call sites; the shared helper emits `FOR UPDATE` for MySQL/PostgreSQL and skips it for SQLite, where the syntax is unsupported. Dialect-specific locking with different semantics (for example, a MySQL next-key/gap lock) may use raw SQL only behind explicit database-type branches with valid fallbacks for every supported database.
- When raw SQL is unavoidable, account for dialect differences: - When raw SQL is unavoidable, account for dialect differences:
- PostgreSQL uses `"column"` quoting, while MySQL/SQLite use `` `column` ``. - PostgreSQL uses `"column"` quoting, while MySQL/SQLite use `` `column` ``.
- Use `commonGroupCol`, `commonKeyCol` from `model/main.go` for reserved-word columns like `group` and `key`. - Use `commonGroupCol`, `commonKeyCol` from `model/main.go` for reserved-word columns like `group` and `key`.
......
...@@ -1027,7 +1027,7 @@ func adminResetUserSubscriptionsByPlanTx(tx *gorm.DB, userId int, plan *Subscrip ...@@ -1027,7 +1027,7 @@ func adminResetUserSubscriptionsByPlanTx(tx *gorm.DB, userId int, plan *Subscrip
return nil, errors.New("invalid reset args") return nil, errors.New("invalid reset args")
} }
var subs []UserSubscription var subs []UserSubscription
if err := tx.Set("gorm:query_option", "FOR UPDATE"). if err := lockForUpdate(tx).
Where("user_id = ? AND plan_id = ? AND status = ? AND end_time > ?", userId, plan.Id, "active", now). Where("user_id = ? AND plan_id = ? AND status = ? AND end_time > ?", userId, plan.Id, "active", now).
Order("end_time asc, id asc"). Order("end_time asc, id asc").
Find(&subs).Error; err != nil { Find(&subs).Error; err != nil {
...@@ -1049,7 +1049,7 @@ func adminResetPlanSubscriptionsTx(tx *gorm.DB, plan *SubscriptionPlan, now int6 ...@@ -1049,7 +1049,7 @@ func adminResetPlanSubscriptionsTx(tx *gorm.DB, plan *SubscriptionPlan, now int6
return nil, errors.New("invalid reset args") return nil, errors.New("invalid reset args")
} }
var subs []UserSubscription var subs []UserSubscription
if err := tx.Set("gorm:query_option", "FOR UPDATE"). if err := lockForUpdate(tx).
Where("plan_id = ? AND status = ? AND end_time > ?", plan.Id, "active", now). Where("plan_id = ? AND status = ? AND end_time > ?", plan.Id, "active", now).
Order("user_id asc, end_time asc, id asc"). Order("user_id asc, end_time asc, id asc").
Find(&subs).Error; err != nil { Find(&subs).Error; err != nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment