Module: Hoodoo::ActiveRecord::ManuallyDated::ClassMethods
- Defined in:
- lib/hoodoo/active/active_record/manually_dated.rb
Overview
Collection of class methods that get defined on an including class via Hoodoo::ActiveRecord::ManuallyDated::included.
Instance Method Summary collapse
-
#manual_dating_enabled ⇒ Object
Activate manually-driven historic dating for this model.
-
#manual_dating_enabled? ⇒ Boolean
If a prior call has been made to #manual_dating_enabled then this method returns
true
, elsefalse
. -
#manually_dated(context) ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are relevant/effective at the date/time in the value of
context.request.dated_at
within the givencontext
. -
#manually_dated_at(date_time = Time.now) ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are relevant/effective at the given date/time.
-
#manually_dated_contemporary ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are ‘current’.
-
#manually_dated_destruction_in(context, ident: context.request.ident, scope: all()) ⇒ Object
Analogous to #manually_dated_update_in and with the same return value and exception generation semantics, so see that method for those details.
-
#manually_dated_historic ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are from the past.
-
#manually_dated_update_in(context, ident: context.request.ident, attributes: context.request.body, scope: all()) ⇒ Object
Update a record with manual historic dating.
Instance Method Details
#manual_dating_enabled ⇒ Object
Activate manually-driven historic dating for this model.
See the module documentation for Hoodoo::ActiveRecord::ManuallyDated for full information on dating, column/attribute requirements and so forth.
When dating is enabled, a before_save
filter will ensure that the record’s created_at
and updated_at
fields are manually set to the current time (“now”), if not already set by the time the filter is run. The record’s effective_start
time is set to match created_at
if not already set and effective_end
is set to Hoodoo::ActiveRecord::ManuallyDated::DATE_MAXIMUM if not already set. The record’s uuid
resource UUID is set to the value of the id
column if not already set, which is useful for new records but should never happen for history-savvy updates performed by this mixin’s code.
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 436 def manual_dating_enabled self.nz_co_loyalty_hoodoo_manually_dated = true # This is the 'tightest'/innermost callback available for creation. # Intentionally have nothing for updates/deletes as the high level # API here must be used; we don't want to introduce any more magic. before_create do now = Time.now.utc.round( SECONDS_DECIMAL_PLACES ) self.created_at ||= now self.updated_at ||= now self.effective_start ||= self.created_at self.effective_end ||= DATE_MAXIMUM %i{ created_at updated_at effective_start effective_end }.each do | attr | value = self.send( attr ) rounded = value.to_time.round( SECONDS_DECIMAL_PLACES ) self.send( "#{ attr }=", rounded ) if rounded != value end end # This is very similar to the UUID mixin, but works on the 'uuid' # column. With manual dating, ActiveRecord's quirks with changing # the primary key column, but still doing weird things with an # attribute and accessor called "id", forces us to give up on any # notion of changing the primary key. Keep "id" unique. This means # the UUID mixin, if in use, is now setting the *real* per row # unique key, while the "uuid" contains the UUID that should be # rendered for the resource representation and will appear in more # than one database row if the record has history entries. Thus, # the validation is scoped to be unique only per "effective_end" # value. # # Since the X-Resource-UUID header may be used and result in an # attribute "id" being specified inbound for new records, we take # any value of "id" if present and use that in preference to a # totally new UUID in order to deal with that use case. validate( :on => :create ) do self.uuid ||= self.id || Hoodoo::UUID.generate() end validates( :uuid, { :uuid => true, :presence => true, :uniqueness => { :scope => :effective_end }, } ) # We must specify an acquisition scope that's based on the "uuid" # column only and *not* the "id" column. acquire_with_id_substitute( :uuid ) # Finally, enable the monkey patch to the Finder module's # '#acquire_in' class method, if need be. if self.include?( Hoodoo::ActiveRecord::Finder ) Hoodoo::Monkey.register( target_unit: self, extension_module: Hoodoo::Monkey::Patch::ActiveRecordManuallyDatedFinderAdditions ) Hoodoo::Monkey.enable( extension_module: Hoodoo::Monkey::Patch::ActiveRecordManuallyDatedFinderAdditions ) end end |
#manual_dating_enabled? ⇒ Boolean
If a prior call has been made to #manual_dating_enabled then this method returns true
, else false
.
511 512 513 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 511 def manual_dating_enabled? return self.nz_co_loyalty_hoodoo_manually_dated == true end |
#manually_dated(context) ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are relevant/effective at the date/time in the value of context.request.dated_at
within the given context
. If this value is nil
then the current time in UTC is used.
Manual historic dating must have been previously activated through a call to #dating_enabled, else results will be undefined.
context
-
Hoodoo::Services::Context instance describing a call context. This is typically a value passed to one of the Hoodoo::Services::Implementation instance methods that a resource subclass implements.
528 529 530 531 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 528 def manually_dated( context ) date_time = context.request.dated_at || Time.now return self.manually_dated_at( date_time ) end |
#manually_dated_at(date_time = Time.now) ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are relevant/effective at the given date/time. If this value is nil
then the current time in UTC is used.
Manual historic dating must have been previously activated through a call to #dating_enabled, else results will be undefined.
date_time
-
(Optional) A Time or DateTime instance, or a String that can be converted to a DateTime instance, for which the “effective dated” scope is to be constructed.
544 545 546 547 548 549 550 551 552 553 554 555 556 557 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 544 def manually_dated_at( date_time = Time.now ) date_time = date_time.to_time.utc.round( SECONDS_DECIMAL_PLACES ) arel_table = self.arel_table() arel_query = arel_table[ :effective_start ].lteq( date_time ). and( arel_table[ :effective_end ].gt( date_time ) # .or( # arel_table[ :effective_end ].eq( nil ) # ) ) where( arel_query ) end |
#manually_dated_contemporary ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are ‘current’. The historic/past records for any given UUID will never be included in the scope.
Manual historic dating must have been previously activated through a call to #dating_enabled, else results will be undefined.
577 578 579 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 577 def manually_dated_contemporary where( :effective_end => DATE_MAXIMUM ) end |
#manually_dated_destruction_in(context, ident: context.request.ident, scope: all()) ⇒ Object
Analogous to #manually_dated_update_in and with the same return value and exception generation semantics, so see that method for those details.
This particular method soft-deletes a record. It moves the ‘current’ entry to being an ‘historic’ entry as in #manually_dated_update_in, but does not then generate any new ‘current’ record. Returns nil
if the record couldn’t be found to start with, else returns the found and soft-deleted / now-historic model instance.
Since no actual “hard” record deletion takes place, traditional ActiveRecord concerns of delete
versus destroy
or of dependency chain destruction do not apply. No callbacks or validations are run when the record is updated (via ActiveRecord’s #update_column). A failure to update the record will result in an unhandled exception. No change is made to the updated_at
column value.
Unnamed parameters are:
context
-
Hoodoo::Services::Context instance describing a call context. This is typically a value passed to one of the Hoodoo::Services::Implementation instance methods that a resource subclass implements. This is used to obtain the record’s UUID unless overridden (see named parameter list).
Additional named parameters are:
ident
-
UUID (32-digit
id
column value) of the record to be updated. If present, thecontext
parameter may benil
. If omitted,context.request.ident
is used. scope
-
ActiveRecord::Relation instance providing the scope to use for database locks and acquiring the record to update. Defaults to #acquisition_scope for the prevailing
ident
value.
756 757 758 759 760 761 762 763 764 765 766 767 768 769 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 756 def manually_dated_destruction_in( context, ident: context.request.ident, scope: all() ) # See #manually_dated_update_in implementation for rationale. # return self.transaction do record = scope.manually_dated_contemporary().lock( true ).acquire( ident ) record.update_column( :effective_end, Time.now.utc ) unless record.nil? record end end |
#manually_dated_historic ⇒ Object
Return an ActiveRecord::Relation instance which only matches records that are from the past. The ‘current’ record for any given UUID will never be included by the scope.
Manual historic dating must have been previously activated through a call to #dating_enabled, else results will be undefined.
566 567 568 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 566 def manually_dated_historic where.not( :effective_end => DATE_MAXIMUM ) end |
#manually_dated_update_in(context, ident: context.request.ident, attributes: context.request.body, scope: all()) ⇒ Object
Update a record with manual historic dating. This means that the ‘current’ / most recent record is turned into a historic entry via setting its effective_end
date, a duplicate is made and any new attribute values are set in this duplicate. This new record is then saved as the ‘current’ version. A transaction containing a database lock over all history rows for the record via its UUID (id
column) is used to provide concurrent access safety.
The return value is complex:
-
If
nil
, the record that was to be updated could not be found. -
If not
nil
, an ActiveRecord model instance is returned. This is the new ‘current’ record, but it might not be saved; validation errors may have happened. You need to check for this before proceeding. This will not be the same model instance found for the original, most recent / current record.
If attempts to update the previous, now-historic record’s effective end date fail, an exception may be thrown as the failure condition is unexpected (it will almost certainly be because of a database connection failure). You might need to call this method from a block with a rescue
clause if you wish to handle those elegantly, but it is probably a serious failure and the generally recommended behaviour is to just let Hoodoo’s default exception handler catch the exception and return an HTTP 500 response to the API caller.
Unnamed parameters are:
context
-
Hoodoo::Services::Context instance describing a call context. This is typically a value passed to one of the Hoodoo::Services::Implementation instance methods that a resource subclass implements. This is used to find the record’s UUID and new attribute information unless overridden (see named parameter list).
Additional named parameters are:
ident
-
UUID (32-digit
id
column value) of the record to be updated. If omitted,context.request.ident
is used. attributes
-
Hash of attributes to write (via ActiveRecord’s
assign_attributes
method) in order to perform the update. If omitted,context.request.body
is used.
If both ident
and attributes
are supplied then the first parameter providing context
may be nil
.
scope
-
ActiveRecord::Relation instance providing the scope to use for database locks and acquiring the record to update. Defaults to #acquisition_scope for the prevailing
ident
value.
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 |
# File 'lib/hoodoo/active/active_record/manually_dated.rb', line 633 def manually_dated_update_in( context, ident: context.request.ident, attributes: context.request.body, scope: all() ) new_record = nil retried_operation = false begin # 'requires_new' => exceptions in nested transactions will cause # rollback; see the comment documentation for the Writer module's # "persist_in" method for details. # self.transaction( :requires_new => true ) do lock_scope = scope.acquisition_scope( ident ).lock( true ) self.connection.execute( lock_scope.to_sql ) original = scope.manually_dated_contemporary().acquire( ident ) break if original.nil? # The only way this can fail is by throwing an exception. # original.update_column( :effective_end, Time.now.utc.round( SECONDS_DECIMAL_PLACES ) ) # When you 'dup' a live model, ActiveRecord clears the 'created_at' # and 'updated_at' values, and the 'id' column - even if you set # the "primary_key=..." value on the model to something else. Put # it all back together again. # # Duplicate, apply attributes, then overwrite anything that is # vital for dating so that the inbound attributes hash can't cause # any inconsistencies. # new_record = original.dup new_record.assign_attributes( attributes ) new_record.id = nil new_record.uuid = original.uuid new_record.created_at = original.created_at new_record.updated_at = original.effective_end # (sic.) new_record.effective_start = original.effective_end # (sic.) new_record.effective_end = DATE_MAXIMUM # Save with validation but no exceptions. The caller examines the # returned object to see if there were any validation errors. # new_record.save() # Must roll back if the new record didn't save, to undo the # 'effective_end' column update on 'original' earlier. # raise ::ActiveRecord::Rollback if new_record.errors.present? end rescue ::ActiveRecord::StatementInvalid => exception # By observation, PostgreSQL can start worrying about deadlocks # with the above. Leading theory is that it's "half way through" # inserting the new row when someone else comes along and waits # on the lock, but that new waiting thread has also ended up # capturing a lock on the half-inserted row (since inserting # involves lots of internal steps and locks). # # In such a case, retry. But only do so once; then give up. # if retried_operation == false && exception..downcase.include?( 'deadlock' ) retried_operation = true # Give other Threads time to run, maximising chance of deadlock # being resolved before retry. # sleep( 0.1 ) retry else raise exception end end # "begin"..."rescue"..."end" return new_record end |