👨‍💻

イミュータブルデータモデルという設計手法について

2024/12/11に公開

こんにちは、ツクリンクでエンジニアをしているけいです。
モデルの設計方法について調べていた際に、イミュータブルデータモデルという手法に出会いました。この設計手法は、データの更新や削除を避けることで「履歴を保持」しながら「処理の集約化」を実現できるというものです。

さらに、データを「イベント系」と「リソース系」に分類して扱うことで、設計がより明確になる側面もあると感じました。そこで、イミュータブルデータモデルの基本的な考え方やメリットをご紹介します。

イミュータブルデータモデルの概要

イミュータブルデータモデルでは、データの変更を以下のように扱います。

  • 変更の履歴を記録する
    • 元のデータを直接更新せず、新しいイベントとして保存します。
  • 処理をイベントモデルに集約する
    • 状態変更に伴う処理(通知や外部連携など)を、イベントモデル内で一元管理します。
  • 履歴を失わない
    • 変更や削除が行われないため、過去の状態をいつでも追跡できます。

イベント系とリソース系の分類

イミュータブルデータモデルを採用する際、システム内のデータを「リソース系」と「イベント系」に分類することが重要です。このアプローチにより、データモデルの役割が明確化されます。

リソース系エンティティとは?

リソース系は、システムで管理される静的なオブジェクトやエンティティを表します。

例:

  • ユーザー(User
  • 商品(Product
  • プロジェクト(Project

イベント系エンティティとは?

イベント系は、システム内で発生する動的な出来事や状態の変化を記録するエンティティです。

主に履歴やアクションを表現します。

例:

  • 注文ステータスの変更(OrderStatusChange
  • プロジェクト担当者の変更(ProjectAssignmentChange
  • 商品価格の変更(ProductPriceChange

特徴:

  • 新しいレコードとして履歴が保存される(INSERT ONLY)。
  • 状態変化に伴う処理を集約できる。

例: 注文ステータスの変更

ECサイトで注文ステータスを管理する場合、以下のような状態遷移が発生します。

  • ステータスの流れ:
    • 「未処理」 → 「処理中」 → 「出荷済み」 → 「配送完了」

単純にUPDATEを使いステータスを変更するととに、通知や在庫更新の処理をサービスオブジェクトで実行することもできますが、この方法では以下の課題があります。

  • 履歴が失われる: 過去のステータスを追跡できない。
  • コードが分散する: 通知処理や在庫更新が複数のクラスに分散し、保守性が低下する。

イミュータブルデータモデルによる設計

イベントモデル:

OrderStatusChangeモデルを作成し、ステータス変更に伴う処理と履歴を記録します。

class OrderStatusChange < ApplicationRecord
  belongs_to :order
  validates :previous_status, :new_status, presence: true

  after_create :handle_status_change

  private

  def handle_status_change
    # 通知の送信
    NotificationService.notify_order_status_change(order, previous_status, new_status)

    # 在庫の調整
    InventoryService.adjust_stock(order) if new_status == "shipped"
  end
end

スキーマ例:

| id  | order_id | previous_status | new_status   | created_at          |
|-----|----------|-----------------|--------------|---------------------|
| 1   | 101      | pending         | processing   | 2024-12-01 10:00:00|
| 2   | 101      | processing      | shipped      | 2024-12-02 14:30:00|

ステータス変更の実装例:

OrderStatusChange.create!(
  order: order,
  previous_status: order.current_status,
  new_status: "shipped"
)

# orderに最新のステータスを持つのであれば更新する(orderにステータス自体を持たないという考え方もあり)
order.update!(current_status: "shipped")

この設計のメリット・デメリット

メリット

  • 履歴の保持
    • すべての状態遷移が記録され履歴を失うことがない。
  • 処理の集約化
    • ステータス変更に伴うすべての処理がOrderStatusChangeモデルに集約されコードが分散しづらくなる。
  • サービスオブジェクト不要
    • 変更に伴うロジックがモデル内で完結するため、複雑なサービスクラスを作成する必要がなくなる。
  • トレーサビリティの向上
    • いつ、どのように変更されたかが履歴から明確に追跡できる。

デメリット

  • データ量の増加
    • イベントごとにレコードが追加され、テーブルが膨らみやすい。
  • クエリの複雑化
    • 最新状態を取得するために、イベントテーブルを検索するロジックが必要。
  • モデルの増加
    • イベントごとにモデルを作成するため、設計が煩雑になる場合がある。
  • 学習コスト
    • この設計手法に不慣れなメンバーには理解に時間がかかる可能性がある。

最後に

イミュータブルデータモデルはすべてのシステムに適しているわけではないかもしれません。小規模なプロジェクトや複雑な履歴管理が不要な場合には、過剰設計となる可能性もあります。要件やスケールに応じて使い所を見極め、この設計手法を活用してみてください。

参考

Discussion