Upgrade to Pro — share decks privately, control downloads, hide ads and more …

履歴 on Rails: Bitemporal Data Modelで実現する履歴管理/hi...

Avatar for hypermkt hypermkt
September 27, 2025

履歴 on Rails: Bitemporal Data Modelで実現する履歴管理/history-on-rails-with-bitemporal-data-model

2025.09.27 Sat, Kaigi on Rails 2025@JP TOWER Hall & Conference

Avatar for hypermkt

hypermkt

September 27, 2025
Tweet

More Decks by hypermkt

Other Decks in Technology

Transcript

  1. 履歴 on Rails: Bitemporal Data Modelで実現する履歴管理 2025.09.27 Sat, Kaigi on

    Rails 2025@JP TOWER Hall & Conference @hypermkt / ばーちー SmartHR プロダクトエンジニア 1
  2. バージョン型 10 • 概要: バージョンごとに全レコードを保持する方式 • ✅ メリット: ◦ 最新も過去も同じテーブルで取得できる

    • ⚠ デメリット : ◦ 常に最新バージョンを取得する条件が必要 ID 投稿ID バージョン タイトル 本文 1 1 1 新製品のお知らせ(初稿) 新製品をリリースします。 2 1 2 新製品のお知らせ(修正版) 新製品を◦月◦日に発売しま す。
  3. 有効期間型 ( Uni-Temporal ) 11 • 概要:各レコードに「いつの情報か」を示す期間を持たせて管理する方式 • ✅ メリット:

    ◦ ある日付時点での状態を再現しやすい • ⚠ デメリット : ◦ 「いつ登録されたか」が記録されないため、あとから登録した履歴かどうかがわ からない ID employee_id department valid_from valid_to 1 1 総務部 2022-04-01 2023-03-31 2 1 システム部 2023-04-01 9999-12-31
  4. • 🕒 ある時点での状態を知りたい: ◦ 「あの時、この人はどこの部署にいた?」 • 📝 あとから分かった出来事も正しい日付で反映したい: ◦ 「1/10に引っ越したけど、住所変更を申請したのは1/20だった」

    • 🔍 過去にどう変わってきたかを振り返りたい: ◦ 「いつ、どんな変更があったか」 「従業員を起点とした変化」をどう扱いたいか 14
  5. Bitemporal Data Modelのテーブル例 ID 履歴ID 有効開始日 有効終了日 システム登録 開始日 システム登録

    終了日 • 履歴ID: 同じ事実の履歴をまとめるためのID • 有効開始日 : その出来事が現実に始まった日 • 有効終了日 : その出来事が現実に終わった日 • システム登録開始日 : システムにこの情報が登録された日 • システム登録終了日 : システムにこの情報が無効となった日 20
  6. 4/1 山田太郎さん システム部に 入社 履歴ID 名前 部署 有効開始日 有効終了日 システム開始日

    システム終了日 1 山田太郎 システム部 2025-04-01 9999-12-31 2025-04-01 9999-12-31 UPDATE 4/1 山田太郎 システム部 INSERT SELECT ※ 時刻は省略しています 22
  7. 6/1 営業部に異動 履歴ID 名前 部署 有効開始日 有効終了日 システム開始日 システム終了日 1

    山田太郎 システム部 2025-04-01 9999-12-31 2025-04-01 2025-06-01 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 9999-12-31 1 山田太郎 営業部 2025-06-01 9999-12-31 2025-06-01 9999-12-31 4/1 山田太郎 システム部 6/1 山田太郎 営業部 UPDATE INSERT SELECT 23
  8. 今現在の情報を取得する 履歴ID 名前 部署 有効開始日 有効終了日 システム開始日 システム終了日 1 山田太郎

    システム部 2025-04-01 9999-12-31 2025-04-01 2025-06-01 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 9999-12-31 1 山田太郎 営業部 2025-06-01 9999-12-31 2025-06-01 9999-12-31 4/1 山田太郎 システム部 6/1 山田太郎 営業部 UPDATE INSERT SELECT 24
  9. SELECT * FROM employees WHERE "有効開始日" <= '2025-09-27' AND "有効終了日"

    > '2025-09-27' AND "システム開始日" <= '2025-09-27' AND "システム終了日" > '2025-09-27' 今現在の情報を SQLで取得する UPDATE INSERT SELECT 履歴ID 名前 部署 有効開始日 有効終了日 システム開始日 システム終了日 1 山田太郎 システム部 2025-04-01 9999-12-31 2025-04-01 2025-06-01 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 9999-12-31 1 山田太郎 営業部 2025-06-01 9999-12-31 2025-05-01 9999-12-31 25
  10. ✅ メリット • 正確な時点再現ができる • あとから履歴データの登録・変更ができる • 監査・調査に強い ⚠ デメリット

    • SQLが複雑になる • 概念(2つの時間軸)が複雑で学習コストがある • 履歴が積み重なり、レコード数が増える メリット・デメリット 26
  11. 従業員モデルを例にする Employee( id: integer, name: string, # 名前 department: string,

    # 部署 bitemporal_id: integer, # 履歴ID valid_from: date, # 有効開始日 valid_to: date, # 有効終了日 transaction_from: datetime, # システム登録日時 transaction_to: datetime, # システム終了日時 ) 32
  12. 4/1 山田太郎さん 入社 id bitemporal_id name department valid_from valid_to transaction_from

    transaction_to 1 1 山田太郎 システム部 2025-04-01 9999-12:31 2025-04-01 10:00:00 9999-12:31 09:00:00 INSERT UPDATE SELECT employee = Employee.create( name: "山田太郎", department: "システム部" ) INSERT INTO "employees" ("name", "department", "bitemporal_id", "valid_from", "valid_to"..... UPDATE "employees" SET "bitemporal_id" = 1 WHERE "employees"."id" = 1; 33
  13. 6/1 営業部に異動 id bitemporal_i d name department valid_from valid_to transaction_from

    transaction_to 1 1 山田太郎 システム部 2025-04-01 9999-12:31 2025-04-01 10:00:00 2025-06-01 10:00:00 2 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 10:00:00 9999-12:31 09:00:00 3 1 山田太郎 営業部 2025-06-01 9999-12:31 2025-06-01 10:00:00 9999-12:31 09:00:00 INSERT UPDATE SELECT employee.update(department: "営業部") UPDATE "employees" SET "transaction_to" = $1 WHERE "employees"."id" = $2 INSERT INTO "employees" ("name", "department", "bitemporal_id", "valid_from", "valid_to", …. INSERT INTO "employees" ("name", "department", "bitemporal_id", "valid_from", "valid_to", …. 34
  14. id bitemporal_i d name department valid_from valid_to transaction_from transaction_to 1

    1 山田太郎 システム部 2025-04-01 9999-12:31 2025-04-01 10:00:00 2025-06-01 10:00:00 2 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 10:00:00 9999-12:31 09:00:00 3 1 山田太郎 営業部 2025-06-01 9999-12:31 2025-06-01 10:00:00 9999-12:31 09:00:00 現時点で従業員情報を取得する INSERT UPDATE SELECT Employee.all 35 SELECT "employees".* FROM "employees" WHERE "employees"."transaction_from" <= '2025-09-27 10:00:00' AND "employees"."transaction_to" > '2025-09-27 10:00:00' AND "employees"."valid_from" <= '2025-09-27' AND "employees"."valid_to" > '2025-09-27'
  15. 履歴を扱うための様々なメソッドを提供 • valid_at(datetime): ◦ 指定した時間で有効なレコードを検索 • ignore_valid_datetime: ◦ 有効時間の制約を無視してすべての履歴を検索 •

    ignore_transaction_datetime: ◦ システム時間の制約を無視して論理削除されたレコードも含めて検索 • ignore_bitemporal_datetime: ◦ すべての時間制約を無視してすべてのレコードを検索 • など 36
  16. id bitemporal_i d name department valid_from valid_to transaction_from transaction_to 1

    1 山田太郎 システム部 2025-04-01 9999-12:31 2025-04-01 10:00:00 2025-06-01 10:00:00 2 1 山田太郎 システム部 2025-04-01 2025-06-01 2025-06-01 10:00:00 9999-12:31 09:00:00 3 1 山田太郎 営業部 2025-06-01 9999-12:31 2025-06-01 10:00:00 9999-12:31 09:00:00 時間を指定して検索をする INSERT UPDATE SELECT Employee.valid_at("2025-04-10").all 37 SELECT "employees".* FROM "employees" WHERE "employees"."transaction_from" <= '2025-09-27 10:00:00' AND "employees"."transaction_to" > '2025-09-27 10:00:00' AND "employees"."valid_from" <= '2025-04-10' AND "employees"."valid_to" > '2025-04-10'
  17. 課題1: 履歴に関するデータの調査が困難 • お客様から期待した挙動をしないというお問い合わせ • 原因: ◦ 不具合が原因で想定外の履歴データが作られていること • 難しいポイント

    : ◦ 履歴の実データはとても複雑 ◦ 問題となっているデータの特定に時間を要する ◦ 調査に丸1日かかることもある... 40
  18. Employee.ignore_bitemporal_datetime.bitemporal_for(Employee.first).pluck(:name, :department, :valid_from, :valid_to, :transaction_from, :transaction_to) => [["山田ハナコ", "システム部", Tue,

    01 Apr 2025 10:00:00.000000000 JST +09:00, Fri, 31 Dec 9999 09:00:00.000000000 JST +09:00, Tue, 01 Apr 2025 10:00:00.000000000 JST +09:00, Thu, 01 May 2025 10:00:00.000000000 JST +09:00], ["佐藤ハナコ", "システム部", Tue, 01 Apr 2025 10:00:00.000000000 JST +09:00, Thu, 01 May 2025 10:00:00.000000000 JST +09:00, Thu, 01 May 2025 10:00:00.000000000 JST +09:00, Fri, 31 Dec 9999 09:00:00.000000000 JST +09:00], ["佐藤ハナコ", "営業部", Thu, 01 May 2025 10:00:00.000000000 JST +09:00, Fri, 31 Dec 9999 09:00:00.000000000 JST +09:00, Thu, 01 May 2025 10:00:00.000000000 JST +09:00, Fri, 31 Dec 9999 09:00:00.000000000 JST +09:00]] 41
  19. 課題2: 履歴に関わるシステムの複雑化 • 従業員情報の変更は日単位が基本 • しかし内部では「時分秒」まで保持していた ◦ 時分秒の違いによって、実装が複雑化 ◦ 利用者・開発者の双方にとって使いづらい

    従業員がアルバイト (システム部 )→正社員(営業部)になった例 アルバイト 雇用形態 部署 システム部 営業部 2025/01/01 00:00:00 2025/04/01 10:11:23 2025/04/01 15:24:33 正社員 同日の別時刻の履 歴 45
  20. 有効期間の日付管理を timestamp型からdate型に変更 アルバイト 雇用形態 部署 システム部 営業部 2025/01/01 00:00:00 2025/04/01

    10:11:23 2025/04/01 15:24:33 正社員 Before After アルバイト 雇用形態 部署 システム部 営業部 2025/01/01 2025/04/01 正社員 46
  21. Bitemporal Data Modelを扱うときの気をつけること • ❗課題 ◦ データの肥大化 ◦ 実装が複雑化しやすい ◦

    導入時に学習コストもかかる ◦ やめるのも大変そう...(社内事例無し) • 🛠 対策 ◦ 🧐本当に必要かどうか 、どこに適用するのか検討する 50