0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

残量・状態はカラムに持たず計算で出す(小規模なら正規化を崩さない、Laravel)

0
Posted at

保守サポート時間の前払いパック(10h/20h/30hを購入し、使った分を消化していく仕組み)を実装したときの設計メモです。「残り時間」をどこに持たせるかで、後々の運用のしやすさが変わります。

選択肢: カラムに持つか、都度計算するか

方式 やること 弱点
remaining_hoursカラムを持つ 消化記録を追加・削除するたびに更新する 更新を1箇所でも忘れるとズレる
都度計算する 消化記録の合計から都度引き算する 毎回集計クエリが走る

小規模(顧客数十件、パックも同程度)なシステムでは、後者を選びました。

実装: 3つのメソッドで完結させる

// app/Models/SupportHourPackage.php
public function usages(): HasMany
{
    return $this->hasMany(SupportHourUsage::class);
}

public function usedHours(): float
{
    return (float) $this->usages()->sum('used_hours');
}

public function remainingHours(): float
{
    return max(0, (float) $this->hours_purchased - $this->usedHours());
}

public function isExpired(): bool
{
    return $this->expires_at->isPast();
}

/**
 * active: 期限内かつ残時間あり / expired: 期限切れ / exhausted: 残時間なし
 */
public function status(): string
{
    if ($this->isExpired()) {
        return 'expired';
    }

    if ($this->remainingHours() <= 0) {
        return 'exhausted';
    }

    return 'active';
}

usedHours()は消化記録テーブル(support_hour_usages)からSUMを取るだけ、remainingHours()は購入時間からそれを引くだけです。status()はこの2つを組み合わせて、期限切れ・使い切り・利用中の3状態を判定します。

remainingHours()でmax(0, ...)にしているのは、万が一消化記録の合計が購入時間を超えてしまっても(運用ミスなどで)、残時間がマイナス表示にならないようにするためです。実際のデータ不整合そのものは直す必要がありますが、画面表示だけは壊れないようにしています。

この方式が壊れない理由: 「足すだけ」の記録テーブル

この設計が安全に機能するのは、消化記録(support_hour_usages)が追加専用のテーブルになっているためです。消化した時間を後から編集・削除する運用がなく、常に「新しい消化記録を1件足す」操作だけで運用が回ります。追加専用なら、SUMを都度取っても値が揺れる心配がありません。

もし「消化記録を後から修正する」運用が必要になったら、この前提が崩れるので、キャッシュカラムを持つ設計に切り替える判断が必要になります。今の規模と運用ではそこまでの複雑さは不要、というのがこのときの判断でした。

いつカラムに切り替えるべきか

計算方式が辛くなるのは、主に次の場面です。

  • 一覧画面で大量のパックを同時に表示し、それぞれの残時間を出す必要がある(N+1的な集計クエリが並ぶ)
  • 集計対象の行数が数万件規模になり、都度のSUMが重くなる

このプロジェクトの規模(顧客数十件)では、どちらにも該当しませんでした。**「今困っていないなら、まだ正規化を崩さない」**という判断です。将来この規模を超えたら、remaining_hoursカラムを追加し、消化記録の作成・削除時にオブザーバーで更新する形に切り替える設計変更をすればよいので、今から複雑にしておく理由がありません。

まとめ

  • 「残量」や「状態」は、都度計算で出せるうちは正規化を崩さない
  • 計算方式が安全に機能するのは、元になる記録が追加専用(追記のみ)である場合
  • 規模が大きくなって計算コストが問題になったら、そのときにキャッシュカラムへ切り替える。先回りして複雑にしない

この記事はZennにも投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?