Share
Embed
履歴 on Rails: Bitemporal Data Modelで実現する履歴管理/history-on-rails-with-bitemporal-data-model
2025.09.27 Sat, Kaigi on Rails 2025@JP TOWER Hall & Conference
hypermkt
September 27, 2025
Transcript
53 slides
-
履歴 on Rails: Bitemporal Data Modelで実現する履歴管理 2025.09.27 Sat, Kaigi on
Rails 2025@JP TOWER Hall & Conference @hypermkt / ばーちー SmartHR プロダクトエンジニア 1 -
バージョン型 10 • 概要: バージョンごとに全レコードを保持する方式 • ✅ メリット: ◦ 最新も過去も同じテーブルで取得できる
• ⚠ デメリット : ◦ 常に最新バージョンを取得する条件が必要 ID 投稿ID バージョン タイトル 本文 1 1 1 新製品のお知らせ(初稿) 新製品をリリースします。 2 1 2 新製品のお知らせ(修正版) 新製品を◦月◦日に発売しま す。 -
有効期間型 ( 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 -
• 🕒 ある時点での状態を知りたい: ◦ 「あの時、この人はどこの部署にいた?」 • 📝 あとから分かった出来事も正しい日付で反映したい: ◦ 「1/10に引っ越したけど、住所変更を申請したのは1/20だった」
• 🔍 過去にどう変わってきたかを振り返りたい: ◦ 「いつ、どんな変更があったか」 「従業員を起点とした変化」をどう扱いたいか 14 -
履歴管理パターンの比較 課題 バージョン型 有効期間型 ある時点での状態を知りたい △ ◯ あとから分かった出来事も正しい日付で反映したい ✕ △
過去にどう変わってきたかを振り返りたい ◯ ◯ 15 -
履歴管理パターンの比較 課題 バージョン型 有効期間型 ほしいもの ある時点での状態を知りたい △ ◯ ◯ あとから分かった出来事も正しい日付で反映したい
✕ △ ◯ 過去にどう変わってきたかを振り返りたい ◯ ◯ ◯ 16 -
• 2つの時間軸 を持つ履歴管理モデル ◦ 有効期間 ▪ 現実世界でその事実が有効だった期間 ◦ システム期間 ▪
システム上でデータとして有効だった期間 Bitemporal Data Modelとは 19 -
Bitemporal Data Modelのテーブル例 ID 履歴ID 有効開始日 有効終了日 システム登録 開始日 システム登録
終了日 • 履歴ID: 同じ事実の履歴をまとめるためのID • 有効開始日 : その出来事が現実に始まった日 • 有効終了日 : その出来事が現実に終わった日 • システム登録開始日 : システムにこの情報が登録された日 • システム登録終了日 : システムにこの情報が無効となった日 20 -
4/1 山田太郎さん システム部に 入社 履歴ID 名前 部署 有効開始日 有効終了日 システム開始日
システム終了日 1 山田太郎 システム部 2025-04-01 9999-12-31 2025-04-01 9999-12-31 UPDATE 4/1 山田太郎 システム部 INSERT SELECT ※ 時刻は省略しています 22 -
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 -
今現在の情報を取得する 履歴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 -
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 -
✅ メリット • 正確な時点再現ができる • あとから履歴データの登録・変更ができる • 監査・調査に強い ⚠ デメリット
• SQLが複雑になる • 概念(2つの時間軸)が複雑で学習コストがある • 履歴が積み重なり、レコード数が増える メリット・デメリット 26 -
• 導入が簡単 • ActiveRecord互換 • 履歴を操作・アクセスする様々なメソッドが提供されている • 複雑なSQLを意識せずにBitemporal Data Modelを扱える
ActiveRecord::Bitemporalのメリット 31 -
従業員モデルを例にする Employee( id: integer, name: string, # 名前 department: string,
# 部署 bitemporal_id: integer, # 履歴ID valid_from: date, # 有効開始日 valid_to: date, # 有効終了日 transaction_from: datetime, # システム登録日時 transaction_to: datetime, # システム終了日時 ) 32 -
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 -
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 -
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' -
履歴を扱うための様々なメソッドを提供 • valid_at(datetime): ◦ 指定した時間で有効なレコードを検索 • ignore_valid_datetime: ◦ 有効時間の制約を無視してすべての履歴を検索 •
ignore_transaction_datetime: ◦ システム時間の制約を無視して論理削除されたレコードも含めて検索 • ignore_bitemporal_datetime: ◦ すべての時間制約を無視してすべてのレコードを検索 • など 36 -
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' -
課題1: 履歴に関するデータの調査が困難 • お客様から期待した挙動をしないというお問い合わせ • 原因: ◦ 不具合が原因で想定外の履歴データが作られていること • 難しいポイント
: ◦ 履歴の実データはとても複雑 ◦ 問題となっているデータの特定に時間を要する ◦ 調査に丸1日かかることもある... 40 -
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 -
課題2: 履歴に関わるシステムの複雑化 • 従業員情報の変更は日単位が基本 • しかし内部では「時分秒」まで保持していた ◦ 時分秒の違いによって、実装が複雑化 ◦ 利用者・開発者の双方にとって使いづらい
従業員がアルバイト (システム部 )→正社員(営業部)になった例 アルバイト 雇用形態 部署 システム部 営業部 2025/01/01 00:00:00 2025/04/01 10:11:23 2025/04/01 15:24:33 正社員 同日の別時刻の履 歴 45 -
有効期間の日付管理を 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 -
Bitemporal Data Modelを扱うときの気をつけること • ❗課題 ◦ データの肥大化 ◦ 実装が複雑化しやすい ◦
導入時に学習コストもかかる ◦ やめるのも大変そう...(社内事例無し) • 🛠 対策 ◦ 🧐本当に必要かどうか 、どこに適用するのか検討する 50