activerecord-bitemporal 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,724 @@
1
+ ActiveRecord::Bitemporal
2
+ ========================
3
+
4
+ [![License](https://img.shields.io/github/license/kufu/activerecord-bitemporal.svg?color=blue)](https://github.com/kufu/activerecord-bitemporal/blob/master/LICENSE)
5
+ [![gem-version](https://img.shields.io/gem/v/activerecord-bitemporal.svg)](https://rubygems.org/gems/activerecord-bitemporal)
6
+ [![gem-download](https://img.shields.io/gem/dt/activerecord-bitemporal.svg)](https://rubygems.org/gems/activerecord-bitemporal)
7
+ [![CircleCI](https://circleci.com/gh/kufu/activerecord-bitemporal.svg?style=svg)](https://circleci.com/gh/kufu/activerecord-bitemporal)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'activerecord-bitemporal'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install activerecord-bitemporal
24
+
25
+ ## 概要
26
+
27
+ activerecord-bitemporal は Rails の ActiveRecord で Bitemporal Data Model を扱うためのライブラリになります。
28
+ activerecord-bitemporal では、モデルを生成すると
29
+
30
+ ```ruby
31
+ employee = nil
32
+ # MEMO: データをわかりやすくする為に時間を固定
33
+ # 2019/1/10 にレコードを生成する
34
+ Timecop.freeze("2019/1/10") {
35
+ employee = Employee.create(emp_code: "001", name: "Jane")
36
+ }
37
+ ```
38
+
39
+ 以下のようなレコードが生成されます。
40
+
41
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
42
+ | --- | --- | --- | --- | --- | --- | --- | --- |
43
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 9999-12-31 |
44
+
45
+ そのモデルに対して更新を行うと
46
+
47
+ ```ruby
48
+ employee = nil
49
+ Timecop.freeze("2019/1/10") {
50
+ employee = Employee.create(emp_code: "001", name: "Jane")
51
+ }
52
+
53
+ Timecop.freeze("2019/1/15") {
54
+ # 更新する
55
+ employee.update(name: "Tom")
56
+ }
57
+ ```
58
+
59
+ 次のような履歴レコードが暗黙的に生成されます。
60
+
61
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
62
+ | --- | --- | --- | --- | --- | --- | --- | --- |
63
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-15 |
64
+ | 2 | 1 | 001 | Jane | 2019-01-10 | 2019-01-15 | 2019-01-15 | 9999-12-31 |
65
+ | 3 | 1 | 001 | Tom | 2019-01-15 | 9999-12-31 | 2019-01-15 | 9999-12-31 |
66
+
67
+ 更に更新すると
68
+
69
+ ```ruby
70
+ employee = nil
71
+ Timecop.freeze("2019/1/10") {
72
+ employee = Employee.create(emp_code: "001", name: "Jane")
73
+ }
74
+
75
+ Timecop.freeze("2019/1/15") {
76
+ employee.update(name: "Tom")
77
+ }
78
+
79
+ Timecop.freeze("2019/1/20") {
80
+ # 更に更新
81
+ employee.update(name: "Kevin")
82
+ }
83
+ ```
84
+
85
+ 更新する度にどんどん履歴レコードが増えていきます。
86
+
87
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
88
+ | --- | --- | --- | --- | --- | --- | --- | --- |
89
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-15 |
90
+ | 2 | 1 | 001 | Jane | 2019-01-10 | 2019-01-15 | 2019-01-15 | 9999-12-31 |
91
+ | 3 | 1 | 001 | Tom | 2019-01-15 | 9999-12-31 | 2019-01-15 | 2019-01-20 |
92
+ | 4 | 1 | 001 | Tom | 2019-01-15 | 2019-01-20 | 2019-01-20 | 9999-12-31 |
93
+ | 5 | 1 | 001 | Kevin | 2019-01-20 | 9999-12-31 | 2019-01-20 | 9999-12-31 |
94
+
95
+ また、レコードを読み込む場合は暗黙的に『一番最新のレコード』を参照します。
96
+
97
+ ```ruby
98
+ employee = nil
99
+ Timecop.freeze("2019/1/10") {
100
+ employee = Employee.create(emp_code: "001", name: "Jane")
101
+ }
102
+
103
+ Timecop.freeze("2019/1/15") {
104
+ employee.update(name: "Tom")
105
+ }
106
+
107
+ Timecop.freeze("2019/1/20") {
108
+ employee.update(name: "Kevin")
109
+ }
110
+
111
+ Timecop.freeze("2019/1/25") {
112
+ # 現時点で有効なレコードのみを参照する
113
+ pp Employee.count
114
+ # => 1
115
+
116
+ # name = "Tom" は過去の履歴レコードとして扱われるので参照されない
117
+ pp Employee.find_by(name: "Tom")
118
+ # => nil
119
+
120
+ # 最新のみ参照する
121
+ pp Employee.all
122
+ # [#<Employee:0x0000559b1b37eb08
123
+ # id: 1,
124
+ # bitemporal_id: 1,
125
+ # emp_code: "001",
126
+ # name: "Kevin",
127
+ # valid_from: 2019-01-20,
128
+ # valid_to: 9999-12-31,
129
+ # transaction_from: 2019-01-20,
130
+ # transaction_to: 9999-12-31>]
131
+ }
132
+ ```
133
+
134
+ 任意の時間の履歴レコードを参照したい場合は `find_at_time(datetime, id)` で時間指定して取得する事が出来ます。
135
+
136
+ ```ruby
137
+ employee = nil
138
+ Timecop.freeze("2019/1/10") {
139
+ employee = Employee.create(emp_code: "001", name: "Jane")
140
+ }
141
+
142
+ Timecop.freeze("2019/1/15") {
143
+ employee.update(name: "Tom")
144
+ }
145
+
146
+ Timecop.freeze("2019/1/20") {
147
+ employee.update(name: "Kevin")
148
+ }
149
+
150
+ # 2019/1/25 に固定
151
+ Timecop.freeze("2019/1/25") {
152
+ # 任意の時間の履歴レコードを取得する
153
+ pp Employee.find_at_time("2019/1/13", employee.id).name
154
+ # => "Jane"
155
+ pp Employee.find_at_time("2019/1/18", employee.id).name
156
+ # => "Tom"
157
+ pp Employee.find_at_time("2019/1/23", employee.id).name
158
+ # => "Kevin"
159
+ }
160
+ ```
161
+
162
+ このように activerecord-bitemporal は、
163
+
164
+ * 保存時に履歴レコードを自動生成
165
+ * `.find_at_time` 等で任意の時間のレコードを取得する
166
+
167
+ というような事を行うライブラリになります。
168
+
169
+
170
+ ## モデルを BiTemporal Data Model 化する
171
+
172
+ 任意のモデルを BiTemporal Data Model(以下、BTDM)として扱う場合は、以下のカラムを DB に追加する必要があります。
173
+
174
+ ```ruby
175
+ ActiveRecord::Schema.define(version: 1) do
176
+ create_table :employees, force: true do |t|
177
+ t.string :emp_code
178
+ t.string :name
179
+
180
+ # BTDM に必要なカラムを追加する
181
+ t.integer :bitemporal_id
182
+ t.datetime :valid_from
183
+ t.datetime :valid_to
184
+ t.datetime :transaction_from
185
+ t.datetime :transaction_to
186
+ end
187
+ end
188
+ ```
189
+
190
+ それぞれのカラムは以下のような意味を持ちます。
191
+
192
+ | カラム名 | 型 | 値 |
193
+ | --- | --- | --- |
194
+ | `bitemporal_id` | `id` と同じ型 | BTDM が共通で持つ `id` |
195
+ | `valid_from` | `datetime` | 有効時間の開始時刻 |
196
+ | `valid_to` | `datetime` | 有効時間の終了時刻 |
197
+ | `transaction_from` | `datetime` | システム時間の開始時刻 |
198
+ | `transaction_to` | `datetime` | システム時間の終了終了 |
199
+
200
+ また、モデルクラスでは `ActiveRecord::Bitemporal` を `include` をする必要があります。
201
+
202
+ ```ruby
203
+ class Employee < ActiveRecord::Base
204
+ include ActiveRecord::Bitemporal
205
+ end
206
+ ```
207
+
208
+ これで `Employee` モデルを BTDM として扱うことが出来ます。
209
+ このドキュメントではこのモデルをサンプルとしてコードを書いていきます。
210
+
211
+
212
+ ## モデルインスタンスに対する操作について
213
+
214
+ ここではモデルの生成・更新・削除といったインスタンスに対する操作に関して解説します。
215
+
216
+
217
+ ### 生成
218
+
219
+ 以下のように BTDM を生成した場合、
220
+
221
+ ```ruby
222
+ # MEMO: Timecop を使って擬似的に 2019/1/10 の日付でレコードを生成
223
+ # データをわかりやすくする為に使用しているだけで activerecord-bitemporal には Timecop は必要ありません
224
+ employee = nil
225
+ Timecop.freeze("2019/1/10") {
226
+ employee = Employee.create(emp_code: "001", name: "Jane")
227
+ }
228
+ ```
229
+
230
+ 以下のようなレコードが生成されます。
231
+
232
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
233
+ | --- | --- | --- | --- | --- | --- | --- | --- |
234
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 9999-12-31 |
235
+
236
+ この時に生成されるレコードのカラムには暗黙的に以下のような値が保存されます。
237
+
238
+ | カラム | 値 |
239
+ | --- | --- |
240
+ | `bitemporal_id` | 自身の `id` |
241
+ | `valid_from` | 生成した時間 |
242
+ | `valid_to` | 擬似的な `INFINITY` 時間 |
243
+
244
+ これは『`valid_from` から `valid_to` までの期間で有効なデータ』という意味になります。
245
+ また、 `valid_from` や `valid_to` を指定すれば『任意の時間』の履歴データも生成も出来ます。
246
+
247
+ ```ruby
248
+ Timecop.freeze("2019/1/10") {
249
+ # 現時点よりも前からのデータを生成する
250
+ Employee.create(emp_code: "001", name: "Jane", valid_from: "2019/1/1")
251
+ }
252
+ ```
253
+
254
+
255
+ ### 更新
256
+
257
+ `#update` 等でモデルを更新すると『更新時間』を基準とした履歴レコードが暗黙的に生成されます。
258
+
259
+ ```ruby
260
+ employee = nil
261
+ Timecop.freeze("2019/1/10") {
262
+ employee = Employee.create(emp_code: "001", name: "Jane")
263
+ }
264
+
265
+ Timecop.freeze("2019/1/20") {
266
+ # モデルを更新すると履歴レコードが生成される
267
+ employee.update(name: "Tom")
268
+ # これは #save でも同様に行われる
269
+ # employee.name = "Tom"
270
+ # employee.save
271
+ }
272
+ ```
273
+
274
+ 上記の操作を行うと以下のようなレコードが生成されます。
275
+
276
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
277
+ | --- | --- | --- | --- | --- | --- | --- | --- |
278
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-20 |
279
+ | 2 | 1 | 001 | Jane | 2019-01-10 | 2019-01-20 | 2019-01-20 | 9999-12-31 |
280
+ | 3 | 1 | 001 | Tom | 2019-01-20 | 9999-12-31 | 2019-01-20 | 9999-12-31 |
281
+
282
+ 更新時には以下のような処理を行っており、結果的に新しいレコードが2つ生成されることになります。
283
+ また、この時に生成されるレコードは共通の `bitemporal_id` を保持します。
284
+
285
+ 1. 更新対象のレコード(`id = 1`)のシステム時間の終了時刻を更新する
286
+ 2. 更新を行った時間までのレコード(`id = 2`)を新しく生成する
287
+ 3. 更新を行った時間からのレコード(`id = 3`)を新しく生成する
288
+
289
+ activerecord-bitemporal ではレコードの内容を変更する際にレコードを直接変更するのではなくて『既存のレコードはシステム時間では参照しないような時刻』にして『変更後のレコードを新しく生成』していきます。
290
+ ただし、`#update_columns` で更新を行うと強制的にレコードが上書きされるので注意してください。
291
+
292
+ ```ruby
293
+ employee = nil
294
+ Timecop.freeze("2019/1/10") {
295
+ employee = Employee.create(emp_code: "001", name: "Jane")
296
+ }
297
+
298
+ Timecop.freeze("2019/1/20") {
299
+ # #update_columns で更新するとレコードが直接変更される
300
+ employee.update_columns(name: "Tom")
301
+ }
302
+ ```
303
+
304
+ 上記の場合は以下のようなレコードになります。
305
+ `id = 1` のレコードが直接変更されるので注意してください。
306
+
307
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
308
+ | --- | --- | --- | --- | --- | --- | --- | --- |
309
+ | 1 | 1 | 001 | Tom | 2019-01-10 | 9999-12-31 | 2019-01-10 | 9999-12-31 |
310
+
311
+ 履歴を生成せずに上書きして更新したいのであれば activerecord-bitemporal 側で用意している `#force_update` を利用する事が出来ます。
312
+
313
+ ```ruby
314
+ employee = nil
315
+ Timecop.freeze("2019/1/10") {
316
+ employee = Employee.create(emp_code: "001", name: "Jane")
317
+ }
318
+
319
+ Timecop.freeze("2019/1/20") {
320
+ # #force_update のでは自身を受け取る
321
+ # このブロック内であれば履歴を生成せずにレコードの変更が行われる
322
+ employee.force_update { |employee|
323
+ employee.update(name: "Tom")
324
+ }
325
+ }
326
+ ```
327
+
328
+ 上記の場合は以下のレコードが生成されます。
329
+
330
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
331
+ | --- | --- | --- | --- | --- | --- | --- | --- |
332
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-20 |
333
+ | 2 | 1 | 001 | Tom | 2019-01-10 | 9999-12-31 | 2019-01-20 | 9999-12-31 |
334
+
335
+ この場合は `id = 1` はシステムの終了時刻が更新され、新しい `id = 2` のレコードが生成されます。
336
+
337
+
338
+ ### 更新時間を指定して更新
339
+
340
+ TODO:
341
+
342
+
343
+ ### 削除
344
+
345
+ 更新と同様にレコードのシステム時間の終了時刻を更新しつつ、新しいレコードが生成されます。
346
+
347
+ ```ruby
348
+ employee = nil
349
+ Timecop.freeze("2019/1/10") {
350
+ employee = Employee.create(emp_code: "001", name: "Jane")
351
+ }
352
+
353
+ Timecop.freeze("2019/1/20") {
354
+ employee.update(name: "Tom")
355
+ }
356
+
357
+ Timecop.freeze("2019/1/30") {
358
+ # 削除を行うとその時間までの履歴が生成される
359
+ employee.destroy
360
+ }
361
+ ```
362
+
363
+ 上記の場合では以下のようなレコードが生成されます。
364
+
365
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
366
+ | --- | --- | --- | --- | --- | --- | --- | --- |
367
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-20 |
368
+ | 2 | 1 | 001 | Jane | 2019-01-10 | 2019-01-20 | 2019-01-20 | 9999-12-31 |
369
+ | 3 | 1 | 001 | Tom | 2019-01-20 | 9999-12-31 | 2019-01-20 | 2019-01-30 |
370
+ | 4 | 1 | 001 | Tom | 2019-01-20 | 2019-01-30 | 2019-01-30 | 9999-12-31 |
371
+
372
+ 削除も更新と同様に
373
+
374
+ 1. 削除対象のレコード(`id = 3`)のシステム時間の終了時刻を更新する
375
+ 2. 削除を行った時間までの履歴レコード(`id = 4`)を新しく生成する
376
+
377
+ という風に『システム時間の終了時刻を更新してから新しいレコードを生成する』という処理を行っています。
378
+
379
+
380
+ ### ユニーク制約
381
+
382
+ BTDM では『履歴の時間が被っている場合』にユニーク制約のバリデーションを行います。
383
+
384
+ ```ruby
385
+ Employee.create!(name: "Jane", valid_from: "2019/1/1", valid_to: "2019/1/10")
386
+
387
+ # OK : 同じ時間帯で被っていない
388
+ Employee.create!(name: "Jane", valid_from: "2019/2/1", valid_to: "2019/2/10")
389
+
390
+ # NG : 同じ時間帯で被っている
391
+ Employee.create!(name: "Jane", valid_from: "2019/2/5", valid_to: "2019/2/15")
392
+
393
+ # OK : valid_from と valid_to は同じでも問題ない
394
+ Employee.create!(name: "Jane", valid_from: "2019/2/10", valid_to: "2019/2/20")
395
+ ```
396
+
397
+ また、 BTDM の `bitemporal_id` もユニーク制約となっているので注意してください。
398
+
399
+
400
+ ## 検索について
401
+
402
+ BTDM のレコードの検索について解説します。
403
+
404
+
405
+ ### 検索時にデフォルトで追加されるクエリ
406
+
407
+ BTDM では DB からレコードを参照する場合、暗黙的に
408
+
409
+ * 現在の時間を指定する時間指定クエリ
410
+ * 論理削除を除くクエリ
411
+
412
+ が追加された状態で SQL 文が構築されます。
413
+
414
+ ```ruby
415
+ Timecop.freeze("2019/1/20") {
416
+ # 現在の時間の履歴を返すために暗黙的に時間指定や論理削除されたレコードが除かれる
417
+ puts Employee.all.to_sql
418
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-01-20 00:00:00' AND "employees"."valid_to" > '2019-01-20 00:00:00' AND "employees"."transaction_to" = '9999-12-31 00:00:00'
419
+ }
420
+ ```
421
+
422
+ これにより DB 上に複数の履歴レコードや論理削除されているレコードがあっても『現時点で有効な』レコードが参照されます。
423
+
424
+ ```ruby
425
+ employee = nil
426
+ Timecop.freeze("2019/1/10") {
427
+ employee = Employee.create(name: "Jane")
428
+ }
429
+
430
+ Timecop.freeze("2019/1/15") {
431
+ employee.update(name: "Tom")
432
+ }
433
+
434
+ Timecop.freeze("2019/1/20") {
435
+ # DB 上では履歴レコードや論理削除済みレコードなどが複数存在するが、暗黙的にクエリが追加されているので
436
+ # 通常の ActiveRecord のモデルを操作した時と同じレコードを返す
437
+ pp Employee.count
438
+ # => 1
439
+
440
+ pp Employee.first
441
+ # => #<Employee:0x000055efd894e9e0
442
+ # id: 1,
443
+ # bitemporal_id: 1,
444
+ # emp_code: nil,
445
+ # name: "Tom",
446
+ # valid_from: 2019-01-15,
447
+ # valid_to: 9999-12-31,
448
+ # transaction_from: 2019-01-15,
449
+ # transaction_to: 9999-12-31>
450
+
451
+ # 更新前の名前で検索しても引っかからない
452
+ pp Employee.where(name: "Jane").first
453
+ # => nil
454
+
455
+ # なぜなら暗黙的に時間指定のクエリが追加されている為
456
+ puts Employee.where(name: "Jane").to_sql
457
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-01-20 00:00:00' AND "employees"."valid_to" > '2019-01-20 00:00:00' AND "employees"."transaction_to" = '9999-12-31 00:00:00' AND "employees"."name" = 'Jane'
458
+ }
459
+ ```
460
+
461
+ このように『現在の時間で有効なレコード』のみが検索の対象となります。
462
+ また、これは `default_scope` ではなくて BTDM が独自にハックして暗黙的に追加する仕組みを実装しているので `.unscoped` で取り除く事は出来ないので注意してください。
463
+
464
+ ```ruby
465
+ # default_scope であれば unscoped で無効化することが出来るが、BTDM のデフォルトクエリはそのまま
466
+ puts Employee.unscoped { Employee.all.to_sql }
467
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-10-25 07:56:06.731259' AND "employees"."valid_to" > '2019-10-25 07:56:06.731259' AND "employees"."transaction_to" = '9999-12-31 00:00:00'
468
+ ```
469
+
470
+
471
+ ### 検索時にデフォルトクエリを取り除く
472
+
473
+ 検索時にデフォルトクエリを取り除きたい場合、以下のスコープを使用します。
474
+
475
+ | スコープ | 動作 |
476
+ | --- | --- |
477
+ | `.ignore_valid_datetime` | 時間指定を無視する |
478
+ | `.within_deleted` | 論理削除されているレコードを含める |
479
+ | `.without_deleted` | 論理削除されているレコードを含めない |
480
+
481
+ ```ruby
482
+ Timecop.freeze("2019/1/20") {
483
+ # 時間指定をしているクエリを取り除く
484
+ puts Employee.ignore_valid_datetime.to_sql
485
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."transaction_to" = '9999-12-31 00:00:00'
486
+
487
+ # 論理削除しているレコードも含める
488
+ puts Employee.within_deleted.to_sql
489
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-01-20 00:00:00' AND "employees"."valid_to" > '2019-01-20 00:00:00'
490
+
491
+ # 全てのレコードを対象とする
492
+ puts Employee.ignore_valid_datetime.within_deleted.to_sql
493
+ # => SELECT "employees".* FROM "employees"
494
+ }
495
+ ```
496
+
497
+ 『任意のレコードの履歴一覧を取得する』ようなことを行う場合は `ignore_valid_datetime` を使用して全レコードを参照するようにします。
498
+
499
+ ```ruby
500
+ employee = nil
501
+ Timecop.freeze("2019/1/10") {
502
+ employee = Employee.create(name: "Jane")
503
+ }
504
+
505
+ Timecop.freeze("2019/1/15") {
506
+ employee.update(name: "Tom")
507
+ }
508
+
509
+ Timecop.freeze("2019/1/20") {
510
+ employee.update(name: "Kevin")
511
+
512
+ # NOTE: bitemporal_id を参照することで同一の履歴を取得する事が出来る
513
+ pp Employee.ignore_valid_datetime.where(bitemporal_id: employee.bitemporal_id).map(&:name)
514
+ # => ["Jane", "Tom", "Kevin"]
515
+ }
516
+ ```
517
+
518
+ ### 時間を指定して検索する
519
+
520
+ 任意の時間を指定して検索を行いたい場合、`.valid_at(datetime)` を利用する事が出来ます。
521
+
522
+ ```ruby
523
+ employee1 = nil
524
+ employee2 = nil
525
+ Timecop.freeze("2019/1/10") {
526
+ employee1 = Employee.create(emp_code: "001", name: "Jane")
527
+ }
528
+
529
+ Timecop.freeze("2019/1/15") {
530
+ employee1.update(name: "Tom")
531
+ employee2 = Employee.create(emp_code: "002", name: "Homu")
532
+ }
533
+
534
+ Timecop.freeze("2019/1/20") {
535
+ # valid_at で任意の時間を参照して検索する事が出来る
536
+ puts Employee.valid_at("2019/1/10").to_sql
537
+ # => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-01-10 00:00:00' AND "employees"."valid_to" > '2019-01-10 00:00:00' AND "employees"."transaction_to" = '9999-12-31 00:00:00'
538
+
539
+ pp Employee.valid_at("2019/1/10").map(&:name)
540
+ # => ["Jane"]
541
+ pp Employee.valid_at("2019/1/17").map(&:name)
542
+ # => ["Tom", "Homu"]
543
+
544
+ # そのまま続けてリレーション出来る
545
+ pp Employee.valid_at("2019/1/17").where(name: "Tom").first
546
+ # => #<Employee:0x000055678afd1d20
547
+ # id: 1,
548
+ # bitemporal_id: 1,
549
+ # emp_code: "001",
550
+ # name: "Tom",
551
+ # valid_from: 2019-01-15,
552
+ # valid_to: 9999-12-31,
553
+ # transaction_from: 2019-01-15,
554
+ # transaction_to: 9999-12-31>
555
+ }
556
+ ```
557
+
558
+ また、特定の `id` で検索するのであれば `.find_at_time(datetime, id)` も利用できます。
559
+
560
+ ```ruby
561
+ employee1 = nil
562
+ employee2 = nil
563
+ Timecop.freeze("2019/1/10") {
564
+ employee1 = Employee.create(emp_code: "001", name: "Jane")
565
+ }
566
+
567
+ Timecop.freeze("2019/1/15") {
568
+ employee1.update(name: "Tom")
569
+ employee2 = Employee.create(emp_code: "002", name: "Homu")
570
+ }
571
+
572
+ Timecop.freeze("2019/1/20") {
573
+ # 任意の時間の id のレコードを返す
574
+ pp Employee.find_at_time("2019/1/12", employee1.id)
575
+ # => #<Employee:0x000055b776d7ff18
576
+ # id: 1,
577
+ # bitemporal_id: 1,
578
+ # emp_code: "001",
579
+ # name: "Jane",
580
+ # valid_from: 2019-01-10,
581
+ # valid_to: 2019-01-15,
582
+ # transaction_from: 2019-01-15,
583
+ # transaction_to: 9999-12-31>
584
+
585
+ # 見つからなければ nil を返す
586
+ pp Employee.find_at_time("2019/1/12", employee2.id)
587
+ # => nil
588
+
589
+ # find_at_time の場合は例外を返す
590
+ pp Employee.find_at_time!("2019/1/12", employee2.id)
591
+ # => raise ActiveRecord::RecordNotFound (ActiveRecord::RecordNotFound)
592
+ }
593
+ ```
594
+
595
+
596
+ ## `id` と `bitemporal_id` について
597
+
598
+ BTDM のインスタンスの `id` は特殊で『レコードの `id`』ではなくて『`bitemporal_id` の値』が割り当てられています。
599
+
600
+ ```ruby
601
+ employee = nil
602
+ Timecop.freeze("2019/1/10") {
603
+ employee = Employee.create(emp_code: "001", name: "Jane")
604
+ }
605
+
606
+ Timecop.freeze("2019/1/15") {
607
+ employee.update(name: "Tom")
608
+ }
609
+
610
+ Timecop.freeze("2019/1/20") {
611
+ employee.update(name: "Kevin")
612
+
613
+ # 現在のレコードの id は 1 を返す
614
+ pp Employee.first.id
615
+ # => 1
616
+
617
+ # 別の履歴レコードを参照しても id は同じ
618
+ pp Employee.find_at_time("2019/1/12", employee.id).id
619
+ # => 1
620
+ }
621
+ ```
622
+
623
+ インスタンスの `id` はレコードの読み込み時に自動的に設定されています。
624
+ これは `Employee.find(employee.id)` で検索を行う際に `id` の値が `レコードの id` ではなくて `bitemporal_id` のほうが実装上都合がいい、という由来になっています。
625
+ この影響により `Employee.pluck(:id)` や `Employee.map(&:id)`、 `Employee.ids` が返す結果が微妙に異なるので注意してください。
626
+
627
+ ```ruby
628
+ employee = nil
629
+ Timecop.freeze("2019/1/10") {
630
+ employee = Employee.create(emp_code: "001", name: "Jane")
631
+ }
632
+
633
+ Timecop.freeze("2019/1/15") {
634
+ employee.update(name: "Tom")
635
+ }
636
+
637
+ Timecop.freeze("2019/1/20") {
638
+ employee.update(name: "Kevin")
639
+
640
+ # DB の生 id が返ってくる
641
+ pp Employee.ignore_valid_datetime.pluck(:id)
642
+
643
+ # bitemporal_id が返ってくる
644
+ pp Employee.ignore_valid_datetime.map(&:id)
645
+
646
+ # bitemporal_id が返ってくる
647
+ pp Employee.ignore_valid_datetime.ids
648
+ }
649
+ ```
650
+
651
+ レコードの内容
652
+
653
+ | id | bitemporal_id | emp_code | name | valid_from | valid_to | transaction_from | transaction_to |
654
+ | --- | --- | --- | --- | --- | --- | --- | --- |
655
+ | 1 | 1 | 001 | Jane | 2019-01-10 | 9999-12-31 | 2019-01-10 | 2019-01-15 |
656
+ | 2 | 1 | 001 | Jane | 2019-01-10 | 2019-01-15 | 2019-01-15 | 9999-12-31 |
657
+ | 3 | 1 | 001 | Tom | 2019-01-15 | 9999-12-31 | 2019-01-15 | 2019-01-20 |
658
+ | 4 | 1 | 001 | Tom | 2019-01-15 | 2019-01-20 | 2019-01-20 | 9999-12-31 |
659
+ | 5 | 1 | 001 | Kevin | 2019-01-20 | 9999-12-31 | 2019-01-20 | 9999-12-31 |
660
+
661
+ また、元々の DB の `id` は `#swapped_id` で参照する事が出来ます。
662
+
663
+ ```ruby
664
+ employee = nil
665
+ Timecop.freeze("2019/1/10") {
666
+ employee = Employee.create(emp_code: "001", name: "Jane")
667
+ }
668
+
669
+ Timecop.freeze("2019/1/15") {
670
+ employee.update(name: "Tom")
671
+ }
672
+
673
+ Timecop.freeze("2019/1/20") {
674
+ employee.update(name: "Kevin")
675
+
676
+ pp Employee.first.swapped_id
677
+ # => 5
678
+ pp Employee.find_at_time("2019/1/12", employee.id).swapped_id
679
+ # => 2
680
+ }
681
+ ```
682
+
683
+ まとめると BTDM のインスタンスは以下のような値を保持しています。
684
+
685
+ * `id` : `bitemporal_id` が暗黙的に設定される
686
+ * `bitemporal_id` : BTDM 共通の `id`
687
+ * `swapped_id` : DB の生 `id`
688
+
689
+
690
+ ### `id` 検索の注意点
691
+
692
+ BTDM では `find_by(id: xxx)` や `where(id: xxx)` を行う場合 `id` ではなくて `bitemporal_id` を参照する必要があります。
693
+
694
+ ```ruby
695
+ # NG : BTDM の場合は id 検索出来ない
696
+ Employee.find_by(id: employee.id)
697
+
698
+ # OK : bitemporal_id で検索を行う
699
+ # MEMO: id = bitemporal_id なの
700
+ # find_by(bitemporal_id: employee.id)
701
+ # でも動作するが employee.bitemporal_id と書いたほうが意図が伝わりやすい
702
+ Employee.find_by(bitemporal_id: employee.bitemporal_id)
703
+
704
+ # NG : BTDM の場合は id 検索出来ない
705
+ Employee.where(id: employee.id)
706
+
707
+ # OK : bitemporal_id で検索を行う
708
+ Employee.where(bitemporal_id: employee.bitemporal_id)
709
+ ```
710
+
711
+
712
+ ## Development
713
+
714
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
715
+
716
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
717
+
718
+ ## Contributing
719
+
720
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kufu/activerecord-bitemporal.
721
+
722
+ ## Copyright
723
+
724
+ See ./LICENSE