activerecord-bitemporal 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|