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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +123 -0
- data/.github/auto_assign.yml +27 -0
- data/.gitignore +2 -8
- data/Appraisals +21 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +62 -2
- data/LICENSE +202 -0
- data/README.md +724 -0
- data/Rakefile +6 -0
- data/activerecord-bitemporal.gemspec +20 -18
- data/bin/console +1 -0
- data/docker-compose.yml +11 -0
- data/gemfiles/rails_5.2.gemfile +8 -0
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/gemfiles/rails_6.1.gemfile +8 -0
- data/gemfiles/rails_7.0.gemfile +8 -0
- data/gemfiles/rails_main.gemfile +8 -0
- data/lib/activerecord-bitemporal/bitemporal.rb +588 -0
- data/lib/activerecord-bitemporal/patches.rb +130 -0
- data/lib/activerecord-bitemporal/scope.rb +501 -0
- data/lib/activerecord-bitemporal/version.rb +4 -2
- data/lib/activerecord-bitemporal.rb +177 -4
- metadata +156 -15
@@ -0,0 +1,588 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Bitemporal
|
5
|
+
module BitemporalChecker
|
6
|
+
refine ::Class do
|
7
|
+
def bi_temporal_model?
|
8
|
+
include?(ActiveRecord::Bitemporal)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
refine ::ActiveRecord::Relation do
|
13
|
+
def bi_temporal_model?
|
14
|
+
klass.include?(ActiveRecord::Bitemporal)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
using BitemporalChecker
|
19
|
+
|
20
|
+
module Optionable
|
21
|
+
def bitemporal_option
|
22
|
+
::ActiveRecord::Bitemporal.merge_by(bitemporal_option_storage)
|
23
|
+
end
|
24
|
+
|
25
|
+
def bitemporal_option_merge!(other)
|
26
|
+
self.bitemporal_option_storage = bitemporal_option.merge other
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_bitemporal_option(**opt)
|
30
|
+
tmp_opt = bitemporal_option_storage
|
31
|
+
self.bitemporal_option_storage = tmp_opt.merge(opt)
|
32
|
+
yield self
|
33
|
+
ensure
|
34
|
+
self.bitemporal_option_storage = tmp_opt
|
35
|
+
end
|
36
|
+
private
|
37
|
+
def bitemporal_option_storage
|
38
|
+
@bitemporal_option_storage ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def bitemporal_option_storage=(value)
|
42
|
+
@bitemporal_option_storage = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add Optionable to Bitemporal
|
47
|
+
# Example:
|
48
|
+
# ActiveRecord::Bitemporal.valid_at("2018/4/1") {
|
49
|
+
# # in valid_datetime is "2018/4/1".
|
50
|
+
# }
|
51
|
+
module ::ActiveRecord::Bitemporal
|
52
|
+
class Current < ActiveSupport::CurrentAttributes
|
53
|
+
attribute :option
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
include Optionable
|
58
|
+
|
59
|
+
def valid_at(datetime, &block)
|
60
|
+
with_bitemporal_option(ignore_valid_datetime: false, valid_datetime: datetime, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_at!(datetime, &block)
|
64
|
+
with_bitemporal_option(ignore_valid_datetime: false, valid_datetime: datetime, force_valid_datetime: true, &block)
|
65
|
+
end
|
66
|
+
|
67
|
+
def valid_datetime
|
68
|
+
bitemporal_option[:valid_datetime]&.in_time_zone
|
69
|
+
end
|
70
|
+
|
71
|
+
def ignore_valid_datetime(&block)
|
72
|
+
with_bitemporal_option(ignore_valid_datetime: true, valid_datetime: nil, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def transaction_at(datetime, &block)
|
76
|
+
with_bitemporal_option(ignore_transaction_datetime: false, transaction_datetime: datetime, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def transaction_at!(datetime, &block)
|
80
|
+
with_bitemporal_option(ignore_transaction_datetime: false, transaction_datetime: datetime, force_transaction_datetime: true, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
def transaction_datetime
|
84
|
+
bitemporal_option[:transaction_datetime]&.in_time_zone
|
85
|
+
end
|
86
|
+
|
87
|
+
def ignore_transaction_datetime(&block)
|
88
|
+
with_bitemporal_option(ignore_transaction_datetime: true, transaction_datetime: nil, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def merge_by(option)
|
92
|
+
option_ = option.dup
|
93
|
+
if bitemporal_option_storage[:force_valid_datetime]
|
94
|
+
option_.merge!(valid_datetime: bitemporal_option_storage[:valid_datetime])
|
95
|
+
end
|
96
|
+
|
97
|
+
if bitemporal_option_storage[:force_transaction_datetime]
|
98
|
+
option_.merge!(transaction_datetime: bitemporal_option_storage[:transaction_datetime])
|
99
|
+
end
|
100
|
+
|
101
|
+
bitemporal_option_storage.merge(option_)
|
102
|
+
end
|
103
|
+
private
|
104
|
+
def bitemporal_option_storage
|
105
|
+
Current.option ||= {}
|
106
|
+
end
|
107
|
+
|
108
|
+
def bitemporal_option_storage=(value)
|
109
|
+
Current.option = value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module Relation
|
115
|
+
module Finder
|
116
|
+
def find(*ids)
|
117
|
+
return super if block_given?
|
118
|
+
all.spawn.yield_self { |obj|
|
119
|
+
def obj.primary_key
|
120
|
+
"bitemporal_id"
|
121
|
+
end
|
122
|
+
obj.method(:find).super_method.call(*ids)
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_at_time!(datetime, *ids)
|
127
|
+
valid_at(datetime).find(*ids)
|
128
|
+
end
|
129
|
+
|
130
|
+
def find_at_time(datetime, *ids)
|
131
|
+
find_at_time!(datetime, *ids)
|
132
|
+
rescue ActiveRecord::RecordNotFound
|
133
|
+
expects_array = ids.first.kind_of?(Array) || ids.size > 1
|
134
|
+
expects_array ? [] : nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
include Finder
|
138
|
+
|
139
|
+
def build_arel(*)
|
140
|
+
ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
|
141
|
+
super
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def load
|
146
|
+
return super if loaded?
|
147
|
+
|
148
|
+
# このタイミングで先読みしているアソシエーションが読み込まれるので時間を固定
|
149
|
+
records = ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) { super }
|
150
|
+
|
151
|
+
return records if records.empty?
|
152
|
+
|
153
|
+
valid_datetime_ = valid_datetime
|
154
|
+
if ActiveRecord::Bitemporal.valid_datetime.nil? && (bitemporal_value[:with_valid_datetime].nil? || bitemporal_value[:with_valid_datetime] == :default_scope || valid_datetime_.nil?)
|
155
|
+
valid_datetime_ = nil
|
156
|
+
end
|
157
|
+
|
158
|
+
transaction_datetime_ = transaction_datetime
|
159
|
+
if ActiveRecord::Bitemporal.transaction_datetime.nil? && (bitemporal_value[:with_transaction_datetime].nil? || bitemporal_value[:with_transaction_datetime] == :default_scope || transaction_datetime_.nil?)
|
160
|
+
transaction_datetime_ = nil
|
161
|
+
end
|
162
|
+
|
163
|
+
return records if valid_datetime_.nil? && transaction_datetime_.nil?
|
164
|
+
|
165
|
+
records.each do |record|
|
166
|
+
record.send(:bitemporal_option_storage)[:valid_datetime] = valid_datetime_ if valid_datetime_
|
167
|
+
record.send(:bitemporal_option_storage)[:transaction_datetime] = transaction_datetime_ if transaction_datetime_
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def primary_key
|
172
|
+
bitemporal_id_key
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# create, update, destroy に処理をフックする
|
177
|
+
module Persistence
|
178
|
+
module EachAssociation
|
179
|
+
refine ActiveRecord::Persistence do
|
180
|
+
def each_association(
|
181
|
+
deep: false,
|
182
|
+
ignore_associations: [],
|
183
|
+
only_cached: false,
|
184
|
+
&block
|
185
|
+
)
|
186
|
+
klass = self.class
|
187
|
+
enum = Enumerator.new { |y|
|
188
|
+
reflections = klass.reflect_on_all_associations
|
189
|
+
reflections.each { |reflection|
|
190
|
+
next if only_cached && !association_cached?(reflection.name)
|
191
|
+
|
192
|
+
associations = reflection.collection? ? public_send(reflection.name) : [public_send(reflection.name)]
|
193
|
+
associations.compact.each { |asso|
|
194
|
+
next if ignore_associations.include? asso
|
195
|
+
ignore_associations << asso
|
196
|
+
y << asso
|
197
|
+
asso.each_association(deep: deep, ignore_associations: ignore_associations, only_cached: only_cached) { |it| y << it } if deep
|
198
|
+
}
|
199
|
+
}
|
200
|
+
self
|
201
|
+
}
|
202
|
+
enum.each(&block)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
using EachAssociation
|
207
|
+
|
208
|
+
module PersistenceOptionable
|
209
|
+
include Optionable
|
210
|
+
|
211
|
+
def force_update(&block)
|
212
|
+
with_bitemporal_option(force_update: true, &block)
|
213
|
+
end
|
214
|
+
|
215
|
+
def force_update?
|
216
|
+
bitemporal_option[:force_update].present?
|
217
|
+
end
|
218
|
+
|
219
|
+
def valid_at(datetime, &block)
|
220
|
+
with_bitemporal_option(valid_datetime: datetime, &block)
|
221
|
+
end
|
222
|
+
|
223
|
+
def transaction_at(datetime, &block)
|
224
|
+
with_bitemporal_option(transaction_datetime: datetime, &block)
|
225
|
+
end
|
226
|
+
|
227
|
+
def bitemporal_option_merge_with_association!(other)
|
228
|
+
bitemporal_option_merge!(other)
|
229
|
+
|
230
|
+
# Only cached associations will be walked for performance issues
|
231
|
+
each_association(deep: true, only_cached: true).each do |association|
|
232
|
+
next unless association.respond_to?(:bitemporal_option_merge!)
|
233
|
+
association.bitemporal_option_merge!(other)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def valid_datetime
|
238
|
+
bitemporal_option[:valid_datetime]&.in_time_zone
|
239
|
+
end
|
240
|
+
|
241
|
+
def transaction_datetime
|
242
|
+
bitemporal_option[:transaction_datetime]&.in_time_zone
|
243
|
+
end
|
244
|
+
end
|
245
|
+
include PersistenceOptionable
|
246
|
+
|
247
|
+
using Module.new {
|
248
|
+
refine Persistence do
|
249
|
+
def build_new_instance
|
250
|
+
self.class.new.tap { |it|
|
251
|
+
(self.class.column_names - %w(id type created_at updated_at) - bitemporal_ignore_update_columns.map(&:to_s)).each { |name|
|
252
|
+
# 生のattributesの値でなく、ラッパーメソッド等を考慮してpublic_send(name)する
|
253
|
+
it.public_send("#{name}=", public_send(name))
|
254
|
+
}
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
def has_column?(name)
|
259
|
+
self.class.column_names.include? name.to_s
|
260
|
+
end
|
261
|
+
|
262
|
+
def assign_transaction_to(value)
|
263
|
+
if has_column?(:deleted_at)
|
264
|
+
assign_attributes(transaction_to: value, deleted_at: value)
|
265
|
+
else
|
266
|
+
assign_attributes(transaction_to: value)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def update_transaction_to(value)
|
271
|
+
if has_column?(:deleted_at)
|
272
|
+
update_columns(transaction_to: value, deleted_at: value)
|
273
|
+
else
|
274
|
+
update_columns(transaction_to: value)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
refine ActiveRecord::Base do
|
280
|
+
# MEMO: Do not copy `swapped_id`
|
281
|
+
def dup(*)
|
282
|
+
super.tap { |itself|
|
283
|
+
itself.instance_exec { @_swapped_id = nil } unless itself.frozen?
|
284
|
+
}
|
285
|
+
end
|
286
|
+
end
|
287
|
+
}
|
288
|
+
|
289
|
+
def _create_record(attribute_names = self.attribute_names)
|
290
|
+
bitemporal_assign_initialize_value(valid_datetime: self.valid_datetime)
|
291
|
+
|
292
|
+
ActiveRecord::Bitemporal.valid_at!(self.valid_from) {
|
293
|
+
super()
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
def save(**)
|
298
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
299
|
+
self.class.where(bitemporal_id: self.id).lock!.pluck(:id) if self.id
|
300
|
+
super
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def save!(**)
|
305
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
306
|
+
self.class.where(bitemporal_id: self.id).lock!.pluck(:id) if self.id
|
307
|
+
super
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def _update_row(attribute_names, attempted_action = 'update')
|
312
|
+
current_valid_record, before_instance, after_instance = bitemporal_build_update_records(valid_datetime: self.valid_datetime, force_update: self.force_update?)
|
313
|
+
|
314
|
+
# MEMO: このメソッドに来るまでに validation が発動しているので、以後 validate は考慮しなくて大丈夫
|
315
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
316
|
+
current_valid_record&.update_transaction_to(current_valid_record.transaction_to)
|
317
|
+
before_instance&.save!(validate: false)
|
318
|
+
# NOTE: after_instance always exists
|
319
|
+
after_instance.save!(validate: false)
|
320
|
+
|
321
|
+
# update 後に新しく生成したインスタンスのデータを移行する
|
322
|
+
@_swapped_id = after_instance.swapped_id
|
323
|
+
self.valid_from = after_instance.valid_from
|
324
|
+
|
325
|
+
1
|
326
|
+
# MEMO: Must return false instead of nil, if `#_update_row` failure.
|
327
|
+
end || false
|
328
|
+
end
|
329
|
+
|
330
|
+
def destroy(force_delete: false)
|
331
|
+
return super() if force_delete
|
332
|
+
|
333
|
+
current_time = Time.current
|
334
|
+
target_datetime = valid_datetime || current_time
|
335
|
+
|
336
|
+
duplicated_instance = self.class.find_at_time(target_datetime, self.id).dup
|
337
|
+
|
338
|
+
ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
|
339
|
+
@destroyed = false
|
340
|
+
_run_destroy_callbacks {
|
341
|
+
@destroyed = update_transaction_to(current_time)
|
342
|
+
|
343
|
+
# 削除時の状態を履歴レコードとして保存する
|
344
|
+
duplicated_instance.valid_to = target_datetime
|
345
|
+
duplicated_instance.transaction_from = current_time
|
346
|
+
duplicated_instance.save!(validate: false)
|
347
|
+
}
|
348
|
+
raise ActiveRecord::RecordInvalid unless @destroyed
|
349
|
+
|
350
|
+
self
|
351
|
+
end
|
352
|
+
rescue => e
|
353
|
+
@destroyed = false
|
354
|
+
@_association_destroy_exception = ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record: class=#{e.class}, message=#{e.message}", self)
|
355
|
+
false
|
356
|
+
end
|
357
|
+
|
358
|
+
module ::ActiveRecord::Persistence
|
359
|
+
# MEMO: Must be override ActiveRecord::Persistence#reload
|
360
|
+
alias_method :active_record_bitemporal_original_reload, :reload unless method_defined? :active_record_bitemporal_original_reload
|
361
|
+
if Gem::Version.new("7.0.0.alpha") <= ActiveRecord.version
|
362
|
+
def reload(options = nil)
|
363
|
+
return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
|
364
|
+
|
365
|
+
self.class.connection.clear_query_cache
|
366
|
+
|
367
|
+
fresh_object =
|
368
|
+
ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
|
369
|
+
if apply_scoping?(options)
|
370
|
+
_find_record(options)
|
371
|
+
else
|
372
|
+
self.class.unscoped { self.class.bitemporal_default_scope.scoping { _find_record(options) } }
|
373
|
+
end
|
374
|
+
}
|
375
|
+
|
376
|
+
@association_cache = fresh_object.instance_variable_get(:@association_cache)
|
377
|
+
@attributes = fresh_object.instance_variable_get(:@attributes)
|
378
|
+
@new_record = false
|
379
|
+
@previously_new_record = false
|
380
|
+
# NOTE: Hook to copying swapped_id
|
381
|
+
@_swapped_id = fresh_object.swapped_id
|
382
|
+
self
|
383
|
+
end
|
384
|
+
elsif Gem::Version.new("6.1.0") <= ActiveRecord.version
|
385
|
+
def reload(options = nil)
|
386
|
+
return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
|
387
|
+
|
388
|
+
self.class.connection.clear_query_cache
|
389
|
+
|
390
|
+
fresh_object =
|
391
|
+
ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
|
392
|
+
if options && options[:lock]
|
393
|
+
self.class.unscoped { self.class.lock(options[:lock]).bitemporal_default_scope.find(id) }
|
394
|
+
else
|
395
|
+
self.class.unscoped { self.class.bitemporal_default_scope.find(id) }
|
396
|
+
end
|
397
|
+
}
|
398
|
+
|
399
|
+
@attributes = fresh_object.instance_variable_get(:@attributes)
|
400
|
+
@new_record = false
|
401
|
+
@previously_new_record = false
|
402
|
+
# NOTE: Hook to copying swapped_id
|
403
|
+
@_swapped_id = fresh_object.swapped_id
|
404
|
+
self
|
405
|
+
end
|
406
|
+
else
|
407
|
+
def reload(options = nil)
|
408
|
+
return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
|
409
|
+
|
410
|
+
self.class.connection.clear_query_cache
|
411
|
+
|
412
|
+
fresh_object =
|
413
|
+
ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
|
414
|
+
if options && options[:lock]
|
415
|
+
self.class.unscoped { self.class.lock(options[:lock]).bitemporal_default_scope.find(id) }
|
416
|
+
else
|
417
|
+
self.class.unscoped { self.class.bitemporal_default_scope.find(id) }
|
418
|
+
end
|
419
|
+
}
|
420
|
+
|
421
|
+
@attributes = fresh_object.instance_variable_get("@attributes")
|
422
|
+
@new_record = false
|
423
|
+
# NOTE: Hook to copying swapped_id
|
424
|
+
@_swapped_id = fresh_object.swapped_id
|
425
|
+
self
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
private
|
431
|
+
|
432
|
+
def bitemporal_assign_initialize_value(valid_datetime:, current_time: Time.current)
|
433
|
+
# 自身の `valid_from` を設定
|
434
|
+
self.valid_from = valid_datetime || current_time if self.valid_from == ActiveRecord::Bitemporal::DEFAULT_VALID_FROM
|
435
|
+
|
436
|
+
self.transaction_from = current_time if self.transaction_from == ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_FROM
|
437
|
+
|
438
|
+
# Assign only if defined created_at and deleted_at
|
439
|
+
if has_column?(:created_at)
|
440
|
+
self.transaction_from = self.created_at if changes.key?("created_at")
|
441
|
+
self.created_at = self.transaction_from
|
442
|
+
end
|
443
|
+
if has_column?(:deleted_at)
|
444
|
+
self.transaction_to = self.deleted_at if changes.key?("deleted_at")
|
445
|
+
self.deleted_at = self.transaction_to == ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO ? nil : self.transaction_to
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def bitemporal_build_update_records(valid_datetime:, current_time: Time.current, force_update: false)
|
450
|
+
target_datetime = valid_datetime || current_time
|
451
|
+
# NOTE: force_update の場合は自身のレコードを取得するような時間を指定しておく
|
452
|
+
target_datetime = valid_from_changed? ? valid_from_was : valid_from if force_update
|
453
|
+
|
454
|
+
# 対象基準日において有効なレコード
|
455
|
+
# NOTE: 論理削除対象
|
456
|
+
current_valid_record = self.class.find_at_time(target_datetime, self.id)&.tap { |record|
|
457
|
+
# 元々の id を詰めておく
|
458
|
+
record.id = record.swapped_id
|
459
|
+
record.clear_changes_information
|
460
|
+
}
|
461
|
+
|
462
|
+
# 履歴データとして保存する新しいインスタンス
|
463
|
+
# NOTE: 以前の履歴データ(現時点で有効なレコードを元にする)
|
464
|
+
before_instance = current_valid_record.dup
|
465
|
+
# NOTE: 以降の履歴データ(自身のインスタンスを元にする)
|
466
|
+
after_instance = build_new_instance
|
467
|
+
|
468
|
+
# force_update の場合は既存のレコードを論理削除した上で新しいレコードを生成する
|
469
|
+
if current_valid_record.present? && force_update
|
470
|
+
# 有効なレコードは論理削除する
|
471
|
+
current_valid_record.assign_transaction_to(current_time)
|
472
|
+
# 以前の履歴データは valid_from/to を更新しないため、破棄する
|
473
|
+
before_instance = nil
|
474
|
+
# 以降の履歴データはそのまま保存
|
475
|
+
after_instance.transaction_from = current_time
|
476
|
+
|
477
|
+
# 有効なレコードがある場合
|
478
|
+
elsif current_valid_record.present?
|
479
|
+
# 有効なレコードは論理削除する
|
480
|
+
current_valid_record.assign_transaction_to(current_time)
|
481
|
+
|
482
|
+
# 以前の履歴データは valid_to を詰めて保存
|
483
|
+
before_instance.valid_to = target_datetime
|
484
|
+
raise ActiveRecord::RecordInvalid.new(before_instance) if before_instance.valid_from_cannot_be_greater_equal_than_valid_to
|
485
|
+
before_instance.transaction_from = current_time
|
486
|
+
|
487
|
+
# 以降の履歴データは valid_from と valid_to を調整して保存する
|
488
|
+
after_instance.valid_from = target_datetime
|
489
|
+
after_instance.valid_to = current_valid_record.valid_to
|
490
|
+
raise ActiveRecord::RecordInvalid.new(after_instance) if after_instance.valid_from_cannot_be_greater_equal_than_valid_to
|
491
|
+
after_instance.transaction_from = current_time
|
492
|
+
|
493
|
+
# 有効なレコードがない場合
|
494
|
+
else
|
495
|
+
# 一番近い未来にある Instance を取ってきて、その valid_from を valid_to に入れる
|
496
|
+
nearest_instance = self.class.where(bitemporal_id: bitemporal_id).valid_from_gt(target_datetime).ignore_valid_datetime.order(valid_from: :asc).first
|
497
|
+
if nearest_instance.nil?
|
498
|
+
message = "Update failed: Couldn't find #{self.class} with 'bitemporal_id'=#{self.bitemporal_id} and 'valid_from' < #{target_datetime}"
|
499
|
+
raise ActiveRecord::RecordNotFound.new(message, self.class, "bitemporal_id", self.bitemporal_id)
|
500
|
+
end
|
501
|
+
|
502
|
+
# 有効なレコードは存在しない
|
503
|
+
current_valid_record = nil
|
504
|
+
|
505
|
+
# 以前の履歴データは有効なレコードを基準に生成するため、存在しない
|
506
|
+
before_instance = nil
|
507
|
+
|
508
|
+
# 以降の履歴データは valid_from と valid_to を調整して保存する
|
509
|
+
after_instance.valid_from = target_datetime
|
510
|
+
after_instance.valid_to = nearest_instance.valid_from
|
511
|
+
after_instance.transaction_from = current_time
|
512
|
+
end
|
513
|
+
|
514
|
+
[current_valid_record, before_instance, after_instance]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
module Uniqueness
|
519
|
+
require_relative "./scope.rb"
|
520
|
+
using ::ActiveRecord::Bitemporal::Scope::ActiveRecordRelationScope
|
521
|
+
|
522
|
+
private
|
523
|
+
|
524
|
+
def scope_relation(record, relation)
|
525
|
+
finder_class = find_finder_class_for(record)
|
526
|
+
return super unless finder_class.bi_temporal_model?
|
527
|
+
|
528
|
+
relation = super(record, relation)
|
529
|
+
|
530
|
+
target_datetime = record.valid_datetime || Time.current
|
531
|
+
|
532
|
+
valid_from = record.valid_from.yield_self { |valid_from|
|
533
|
+
# NOTE: valid_from が初期値の場合は現在の時間を基準としてバリデーションする
|
534
|
+
# valid_from が初期値の場合は Persistence#_create_record に Time.current が割り当てられる為
|
535
|
+
# バリデーション時と生成時で若干時間がずれてしまうことには考慮する
|
536
|
+
if valid_from == ActiveRecord::Bitemporal::DEFAULT_VALID_FROM
|
537
|
+
target_datetime
|
538
|
+
# NOTE: 新規作成時以外では target_datetime の値を基準としてバリデーションする
|
539
|
+
# 更新時にバリデーションする場合、valid_from の時間ではなくて target_datetime の時間を基準としているため
|
540
|
+
# valdi_from を基準としてしまうと整合性が取れなくなってしまう
|
541
|
+
elsif !record.new_record?
|
542
|
+
target_datetime
|
543
|
+
else
|
544
|
+
valid_from
|
545
|
+
end
|
546
|
+
}
|
547
|
+
|
548
|
+
# MEMO: `force_update` does not refer to `valid_datetime`
|
549
|
+
valid_from = record.valid_from if record.force_update?
|
550
|
+
|
551
|
+
valid_to = record.valid_to.yield_self { |valid_to|
|
552
|
+
# レコードを更新する時に valid_datetime が valid_from ~ valid_to の範囲外だった場合、
|
553
|
+
# 一番近い未来の履歴レコードを参照して更新する
|
554
|
+
# という仕様があるため、それを考慮して valid_to を設定する
|
555
|
+
if (record.valid_datetime && (record.valid_from..record.valid_to).cover?(record.valid_datetime)) == false && (record.persisted?)
|
556
|
+
finder_class.ignore_valid_datetime.where(bitemporal_id: record.bitemporal_id).valid_from_gt(target_datetime).order(valid_from: :asc).first.valid_from
|
557
|
+
else
|
558
|
+
valid_to
|
559
|
+
end
|
560
|
+
}
|
561
|
+
|
562
|
+
valid_at_scope = finder_class.unscoped.ignore_valid_datetime
|
563
|
+
.valid_from_lt(valid_to).valid_to_gt(valid_from)
|
564
|
+
.yield_self { |scope|
|
565
|
+
# MEMO: #dup などでコピーした場合、id は存在しないが swapped_id のみ存在するケースがあるので
|
566
|
+
# id と swapped_id の両方が存在する場合のみクエリを追加する
|
567
|
+
record.id && record.swapped_id ? scope.where.not(id: record.swapped_id) : scope
|
568
|
+
}
|
569
|
+
|
570
|
+
# MEMO: Must refer Time.current, when not new record
|
571
|
+
# Because you don't want transaction_from to be rewritten
|
572
|
+
transaction_from = if record.transaction_from == ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_FROM
|
573
|
+
Time.current
|
574
|
+
elsif !record.new_record?
|
575
|
+
Time.current
|
576
|
+
else
|
577
|
+
record.transaction_from
|
578
|
+
end
|
579
|
+
transaction_to = record.transaction_to || ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO
|
580
|
+
transaction_at_scope = finder_class.unscoped
|
581
|
+
.transaction_to_gt(transaction_from)
|
582
|
+
.transaction_from_lt(transaction_to)
|
583
|
+
|
584
|
+
relation.merge(valid_at_scope.with_valid_datetime).merge(transaction_at_scope.with_transaction_datetime)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|