9.

LaravelのモデルのUpdateで現在値に追加(加算)する方法

編集
この記事の要点
  • SQL で現在値に加算するには UPDATE 〜 SET 列 = 列 + 値
  • 例: UPDATE products SET stock = stock + 10 WHERE id = 1;
  • 競合状態に注意: 同時更新でデータ不整合の可能性 → トランザクション + ロック推奨
  • 高頻度カウンタは UPDATE 〜 + 1 より楽観ロック (@Version) や Redis INCR が向く
  • 減算は逆符号、複数列の同時加算も可

 

基本構文

-- 在庫を 10 個加算
UPDATE products
SET stock = stock + 10
WHERE id = 1;

-- ポイントを 100 ポイント加算
UPDATE users
SET points = points + 100
WHERE id = 123;

-- 売上を 1000 円加算
UPDATE sales
SET amount = amount + 1000
WHERE date = CURRENT_DATE;

各 DB での書き方

DB構文
MySQLUPDATE t SET col = col + N WHERE ...
PostgreSQL同上
Oracle同上
SQL Server同上
SQLite同上

応用例

① 複数列を同時加算

UPDATE users
SET
    points = points + 100,
    total_purchases = total_purchases + 1,
    last_purchased_at = CURRENT_TIMESTAMP
WHERE id = 1;

② 別テーブルの値を加算

-- MySQL / PostgreSQL
UPDATE users u
SET points = u.points + (SELECT bonus FROM events WHERE id = 5)
WHERE u.id = 1;

-- JOIN を使う書き方 (MySQL)
UPDATE users u
INNER JOIN event_bonuses eb ON u.id = eb.user_id
SET u.points = u.points + eb.bonus_amount
WHERE eb.event_id = 5;

③ 条件式で動的に加算量を変える

-- ランクで加算量を変える
UPDATE users
SET points = points + CASE
    WHEN rank = 'GOLD' THEN 500
    WHEN rank = 'SILVER' THEN 200
    ELSE 100
END
WHERE active = 1;

④ 上限を超えないようにする

-- 在庫上限 100 を超えないように
UPDATE products
SET stock = LEAST(stock + 10, 100)
WHERE id = 1;

-- 0 以下にならないように(減算時)
UPDATE products
SET stock = GREATEST(stock - 10, 0)
WHERE id = 1;

⑤ NULL を考慮(COALESCE)

-- NULL の場合は 0 として加算
UPDATE users
SET points = COALESCE(points, 0) + 100
WHERE id = 1;

-- 初期値もセット
UPDATE users
SET points = COALESCE(points, 0) + 100,
    points_updated_at = CURRENT_TIMESTAMP
WHERE id = 1;

競合状態(Race Condition)と対策

複数トランザクションが同時に同じ行を加算しようとすると更新ロストが起きる可能性:

-- トランザクション A: stock = 100 → 100 + 10 = 110
-- トランザクション B: stock = 100 → 100 + 5 = 105 (A の更新を見ない)
-- 最終的に 105 になり、A の +10 が失われる可能性

-- 対策 1: 行レベルロック (SELECT FOR UPDATE)
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;  -- ロック取得
-- ここで取得した値を元に計算
UPDATE products SET stock = stock + 10 WHERE id = 1;
COMMIT;

-- 対策 2: 単純な UPDATE はアトミック(推奨)
UPDATE products SET stock = stock + 10 WHERE id = 1;
-- 行レベルロックが自動的に取得される
-- 同時 UPDATE は順次実行される
-- ただし結果が想定通りかは別問題(後述の楽観ロック)

楽観ロック(@Version)

「更新前と更新後で別のトランザクションが書き換えていたら失敗扱いにする」方式:

-- バージョン番号を併用
UPDATE products
SET stock = stock + 10,
    version = version + 1
WHERE id = 1 AND version = 5;  -- 期待バージョン

-- 影響行数が 0 なら他のトランザクションが先に更新 → リトライ
-- JPA + @Version で自動化される

大量カウンタは別の仕組みを検討

「PV カウント」「いいね数」のような高頻度更新は SQL の UPDATE では性能不足:

① Redis INCR

# Redis (キー1つあたり 数万 ops/s)
INCR page_view:article:123
INCRBY user_points:1 100
DECR stock:product:1

# Redis → 定期的に DB へ flush

② 別カウンタテーブル(バッチ集計)

-- イベント追記 (高速、競合なし)
INSERT INTO point_events (user_id, points, created_at)
VALUES (1, 100, NOW());

-- 集計はバッチで
UPDATE users u
SET points = u.points + (
    SELECT SUM(points) FROM point_events
    WHERE user_id = u.id AND processed = 0
);
UPDATE point_events SET processed = 1 WHERE processed = 0;

ORM (Java JPA) で加算

// 通常のエンティティ更新
@Transactional
public void addPoints(Long userId, int amount) {
    User user = userRepository.findById(userId).orElseThrow();
    user.setPoints(user.getPoints() + amount);
    // Dirty Checking で UPDATE が自動発行
}

// バルク UPDATE (高速)
@Modifying
@Query("UPDATE User u SET u.points = u.points + :amount WHERE u.id = :id")
int addPointsBulk(@Param("id") Long id, @Param("amount") int amount);

// 楽観ロック
@Entity
public class User {
    @Id private Long id;
    private int points;

    @Version
    private Long version;  // ← 自動でインクリメント、競合検出
}

注意点

  • 初期値 NULL: NULL + 数値 = NULL なので COALESCE が必要
  • オーバーフロー: INT 範囲(21億)を超える可能性は BIGINT に変更
  • マイナス値: 負の数で加算 = 減算、上限 / 下限を CHECK 制約で守る
  • トリガとの干渉: AFTER UPDATE トリガで集計値が二重カウントされないか確認
  • Read After Write 整合性: 加算直後の SELECT で最新値を得るには同トランザクション内で実行

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. モデルの作成
  2. $fillable $guarded $hiddenの説明
  3. テーブルの紐づけ
  4. 主キーの指定とインクリメント
  5. タイムスタンプ
  6. モデルでselect
  7. モデルでinsert
  8. モデルでupdate
  9. 現在値に加算する方法
  10. created_at/updated_atの別名指定