Slide 1

Slide 1 text

履歴 on Rails: Bitemporal Data Modelで実現する履歴管理 2025.09.27 Sat, Kaigi on Rails 2025@JP TOWER Hall & Conference @hypermkt / ばーちー SmartHR プロダクトエンジニア 1

Slide 2

Slide 2 text

ばーちー 自己紹介 株式会社SmartHR バックエンドエンジニア @hypermkt 2

Slide 3

Slide 3 text

ばーちー 自己紹介 株式会社SmartHR バックエンドエンジニア @hypermkt 3

Slide 4

Slide 4 text

なぜ私は今日発表するのか ● SmartHRで履歴に関わる機能の開発・運用を担当 ● 日々の現場で「履歴」の難しさ・面白さ・課題に直面 ● その経験から得られた知見をRailsエンジニアの 皆さんに共有したい 4

Slide 5

Slide 5 text

1. 履歴管理の基本 2. Bitemporal Data Modelとは 3. ActiveRecord::Bitemporal 4. 履歴運用の課題と向き合い方 アジェンダ 5

Slide 6

Slide 6 text

履歴管理の基本 6

Slide 7

Slide 7 text

履歴管理とは ● 「いつ・何を・どのように」 変えたかを 後から追える仕組み が履歴管理 7

Slide 8

Slide 8 text

システムでどうやって履歴を管 理するのか 🤔 8

Slide 9

Slide 9 text

履歴管理の代表的な手法 9 ● バージョン型 ● 有効期間型

Slide 10

Slide 10 text

バージョン型 10 ● 概要: バージョンごとに全レコードを保持する方式 ● ✅ メリット: ○ 最新も過去も同じテーブルで取得できる ● ⚠ デメリット : ○ 常に最新バージョンを取得する条件が必要 ID 投稿ID バージョン タイトル 本文 1 1 1 新製品のお知らせ(初稿) 新製品をリリースします。 2 1 2 新製品のお知らせ(修正版) 新製品を○月○日に発売しま す。

Slide 11

Slide 11 text

有効期間型 ( 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

Slide 12

Slide 12 text

人事労務ソフトとして 欲しいものはなんだろうか 🤔 12

Slide 13

Slide 13 text

● 「従業員」 を起点に情報が変わった推移が見たい ○ 情報が変化するきっかけ ■ 異動による部署や上長の変更 ■ 引っ越しによる住所変更 人事労務ソフトとして欲しいもの 13

Slide 14

Slide 14 text

● 🕒 ある時点での状態を知りたい: ○ 「あの時、この人はどこの部署にいた?」 ● 📝 あとから分かった出来事も正しい日付で反映したい: ○ 「1/10に引っ越したけど、住所変更を申請したのは1/20だった」 ● 🔍 過去にどう変わってきたかを振り返りたい: ○ 「いつ、どんな変更があったか」 「従業員を起点とした変化」をどう扱いたいか 14

Slide 15

Slide 15 text

履歴管理パターンの比較 課題 バージョン型 有効期間型 ある時点での状態を知りたい △ ◯ あとから分かった出来事も正しい日付で反映したい ✕ △ 過去にどう変わってきたかを振り返りたい ◯ ◯ 15

Slide 16

Slide 16 text

履歴管理パターンの比較 課題 バージョン型 有効期間型 ほしいもの ある時点での状態を知りたい △ ◯ ◯ あとから分かった出来事も正しい日付で反映したい ✕ △ ◯ 過去にどう変わってきたかを振り返りたい ◯ ◯ ◯ 16

Slide 17

Slide 17 text

そこで Bitemporal Data Model!! 17

Slide 18

Slide 18 text

Bitemporal Data Modelとは 18

Slide 19

Slide 19 text

● 2つの時間軸 を持つ履歴管理モデル ○ 有効期間 ■ 現実世界でその事実が有効だった期間 ○ システム期間 ■ システム上でデータとして有効だった期間 Bitemporal Data Modelとは 19

Slide 20

Slide 20 text

Bitemporal Data Modelのテーブル例 ID 履歴ID 有効開始日 有効終了日 システム登録 開始日 システム登録 終了日 ● 履歴ID: 同じ事実の履歴をまとめるためのID ● 有効開始日 : その出来事が現実に始まった日 ● 有効終了日 : その出来事が現実に終わった日 ● システム登録開始日 : システムにこの情報が登録された日 ● システム登録終了日 : システムにこの情報が無効となった日 20

Slide 21

Slide 21 text

Bitemporal Data Modelでの レコードの挙動 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

今現在の情報を取得する 履歴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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

✅ メリット ● 正確な時点再現ができる ● あとから履歴データの登録・変更ができる ● 監査・調査に強い ⚠ デメリット ● SQLが複雑になる ● 概念(2つの時間軸)が複雑で学習コストがある ● 履歴が積み重なり、レコード数が増える メリット・デメリット 26

Slide 27

Slide 27 text

Railsでどうやって Bitemporal Data Modelを 扱うのか...🤔 27

Slide 28

Slide 28 text

そこで ActiveRecord::Bitemporal! 28

Slide 29

Slide 29 text

ActiveRecord::Bitemporal 29

Slide 30

Slide 30 text

● ActiveRecordでBitemporal Data Modelを自然に 扱えるようにするためのGem ● https://github.com/kufu/activerecord-bitemporal ActiveRecord::Bitemporalとは 30

Slide 31

Slide 31 text

● 導入が簡単 ● ActiveRecord互換 ● 履歴を操作・アクセスする様々なメソッドが提供されている ● 複雑なSQLを意識せずにBitemporal Data Modelを扱える ActiveRecord::Bitemporalのメリット 31

Slide 32

Slide 32 text

従業員モデルを例にする Employee( id: integer, name: string, # 名前 department: string, # 部署 bitemporal_id: integer, # 履歴ID valid_from: date, # 有効開始日 valid_to: date, # 有効終了日 transaction_from: datetime, # システム登録日時 transaction_to: datetime, # システム終了日時 ) 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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'

Slide 36

Slide 36 text

履歴を扱うための様々なメソッドを提供 ● valid_at(datetime): ○ 指定した時間で有効なレコードを検索 ● ignore_valid_datetime: ○ 有効時間の制約を無視してすべての履歴を検索 ● ignore_transaction_datetime: ○ システム時間の制約を無視して論理削除されたレコードも含めて検索 ● ignore_bitemporal_datetime: ○ すべての時間制約を無視してすべてのレコードを検索 ● など 36

Slide 37

Slide 37 text

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'

Slide 38

Slide 38 text

履歴運用の課題と向き合い方 38

Slide 39

Slide 39 text

履歴運用の課題 ● 履歴に関するデータの調査が困難 ● 履歴に関わるシステムの複雑化 ● 履歴が不要なモデルもBitemporal Data Model化 39

Slide 40

Slide 40 text

課題1: 履歴に関するデータの調査が困難 ● お客様から期待した挙動をしないというお問い合わせ ● 原因: ○ 不具合が原因で想定外の履歴データが作られていること ● 難しいポイント : ○ 履歴の実データはとても複雑 ○ 問題となっているデータの特定に時間を要する ○ 調査に丸1日かかることもある... 40

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

履歴を可視化して理解する 42

Slide 43

Slide 43 text

ActiveRecord::Bitemporal::Visualizer.visualize(record, height:, width:, highlight: true) ● レコードの履歴を視覚化するためのメソッド 履歴を可視化して理解する : その1 Sample 43

Slide 44

Slide 44 text

Googleスプレッドシートで履歴状態を描く 履歴を可視化して理解する : その2 Sample 44

Slide 45

Slide 45 text

課題2: 履歴に関わるシステムの複雑化 ● 従業員情報の変更は日単位が基本 ● しかし内部では「時分秒」まで保持していた ○ 時分秒の違いによって、実装が複雑化 ○ 利用者・開発者の双方にとって使いづらい 従業員がアルバイト (システム部 )→正社員(営業部)になった例 アルバイト 雇用形態 部署 システム部 営業部 2025/01/01 00:00:00 2025/04/01 10:11:23 2025/04/01 15:24:33 正社員 同日の別時刻の履 歴 45

Slide 46

Slide 46 text

有効期間の日付管理を 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

Slide 47

Slide 47 text

詳しくはSmartHR Tech Blogをご参考ください !! https://tech.smarthr.jp/entry/2025/09/12/115617 47

Slide 48

Slide 48 text

課題3: 履歴が不要なモデルも Bitemporal Data Model化 ● いくつかのモデルではBitemporal Data Model化が不要 なものがある ● 経緯までは不明 48

Slide 49

Slide 49 text

非Bitemporal Data Model化をしたいが .... ● データ移行が大変そう... ● SmartHRでは実例なし 49

Slide 50

Slide 50 text

Bitemporal Data Modelを扱うときの気をつけること ● ❗課題 ○ データの肥大化 ○ 実装が複雑化しやすい ○ 導入時に学習コストもかかる ○ やめるのも大変そう...(社内事例無し) ● 🛠 対策 ○ 🧐本当に必要かどうか 、どこに適用するのか検討する 50

Slide 51

Slide 51 text

まとめ 51

Slide 52

Slide 52 text

● Bitemporal Data Modelを導入することで、時点の再現や履歴管 理が実現できる ● 一方で運用上の様々な課題があるため、導入前に「本当にその 履歴が必要か」「どこまで適用するか」は検討が必要 まとめ 52

Slide 53

Slide 53 text

We Are Hiring! 53