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

SmartHRにおけるBiTemporal Data Modelの実践のその後 / After...

Kazuma Watanabe
September 15, 2021

SmartHRにおけるBiTemporal Data Modelの実践のその後 / After the practice of BiTemporal Data Model in SmartHR

Kazuma Watanabe

September 15, 2021
Tweet

More Decks by Kazuma Watanabe

Other Decks in Programming

Transcript

  1. CONFIDENTIAL (Bi)Temporal Data Model 各レコードが有効な時間の開始時点と終了時点を持つ ID 名前 役職 開始 終了

    追加 削除 1 田中 - 9/1 ∞ 9/1 9/15 1 田中 - 9/1 9/15 9/15 ∞ 1 田中 部長 9/15 ∞ 9/15 ∞
  2. CONFIDENTIAL (Bi)Temporal Data Model 各レコードが有効な時間の開始時点と終了時点を持つ ID 名前 役職 開始 終了

    追加 削除 1 田中 - 9/1 ∞ 9/1 9/15 1 田中 - 9/1 9/15 9/15 ∞ 1 田中 部長 9/15 ∞ 9/15 ∞ 9/10に役職なしだったという記録
  3. CONFIDENTIAL (Bi)Temporal Data Model 各レコードが有効な時間の開始時点と終了時点を持つ ID 名前 役職 開始 終了

    追加 削除 1 田中 - 9/1 ∞ 9/1 9/15 1 田中 - 9/1 9/15 9/15 ∞ 1 田中 部長 9/15 ∞ 9/15 ∞ 9/10に役職なしだったという記録 9/20に部長だったという記録
  4. CONFIDENTIAL ActiveRecord::Bitemporal Active Record の _create_record や _update_row を上書きして、直接更新する代わりに 履歴レコードを積み上げる拡張Gem

    通常のクエリを書き換えて、現在時点(または指定された時点)で有効なレコードのみを返 すことができる(WHERE句による絞り込みを暗に付与する) https://github.com/kufu/activerecord-bitemporal
  5. CONFIDENTIAL ActiveRecord::Bitemporal Timecop.freeze("2021/09/15") { employee.update!(job_title: "部長") } ID 名前 役職

    開始 終了 追加 削除 1 田中 - 9/1 ∞ 9/1 ∞ → 9/15 1 田中 - 9/1 9/15 9/15 ∞ 1 田中 部長 9/15 ∞ 9/15 ∞
  6. CONFIDENTIAL ActiveRecord::Bitemporal Timecop.freeze("2021/08/30") { employee.reload # => RecordNotFound } Timecop.freeze("2021/09/10")

    { employee.reload.job_title # => nil } Timecop.freeze("2021/09/20") { employee.reload.job_title # => 部長 } ID 名前 役職 開始 終了 追加 削除 1 田中 - 9/1 ∞ 9/1 9/15 1 田中 - 9/1 9/15 9/15 ∞ 1 田中 部長 9/15 ∞ 9/15 ∞
  7. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end
  8. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end 9/1の情報を追加したい
  9. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end 9/1の情報を追加したい 最も近い未来のレコードを探す
  10. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end 9/1の情報を追加したい 起点となる日付に固定してイ ンスタンスを取得 最も近い未来のレコードを探す
  11. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end 9/1の情報を追加したい 起点となる日付に固定してイ ンスタンスを取得 9/1に戻って更新 最も近い未来のレコードを探す
  12. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end
  13. CONFIDENTIAL 最も近い未来のレコードを起点にする Timecop.freeze("2021/09/01") do employee = Employee.find_by(bitemporal_id: id) unless employee

    earliest_employee = Employee.ignore_valid_datetime.where(bitemporal_id: id).order(:valid_from).first Timecop.freeze(earliest_employee.valid_from) do employee = Employee.find(id: id) end end employee.update!(name: "Tom") end 9/1として処理される 9/15として処理される 9/1として処理されるが、インスタンスは 9/15の時点のデータを保持する
  14. CONFIDENTIAL 一意制約つらい問題 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10に社員番号3の レコードは入れられるか?
  15. CONFIDENTIAL 一意制約つらい問題 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10に社員番号3の レコードは入れられるか?
  16. CONFIDENTIAL 一意制約つらい問題 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10 8/10時点を見れば、社員番号 3 は確かに使われていない
  17. CONFIDENTIAL 実際に追加される履歴 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10 社員番号: 3
  18. CONFIDENTIAL 実際に追加される履歴 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10 社員番号: 3
  19. CONFIDENTIAL 実際に追加される履歴 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10 社員番号: 3 一意制約に違反する期間!!
  20. CONFIDENTIAL 一意制約に違反する期間は空白の履歴を許容する 8/15 9/1 社員番号: 1 社員番号: 2 社員番号: 3

    社員番号: 4 8/1 松田さん 山本さん 8/10 社員番号: 3 8/15から9/1まで所属してい ないことになってしまう
  21. CONFIDENTIAL それぞれが異なる有効期間を持つ 9/1 Employee Job title: 部長 Job title: VP

    8/1 Employeeテーブルだけでは履歴を表現できない!