Support mixin for models subclassed from ActiveRecord::Base providing as-per-API-standard dating support.
The facilities provided here are powerful but relatively complex, so please read through this documentation section in full to understand everything you need to do.
Overview
This mixin adds finder methods to the model it is applied to (see Hoodoo::ActiveRecord::Dated::ClassMethods#dated and Hoodoo::ActiveRecord::Dated::ClassMethods#dated_at). These finders require two database tables in order to function correctly - the primary table (the model table) and a history table. When a record is updated it should be moved to the history table and a new record inserted with the new values. When a record is deleted it should be moved to the history table. This can be done manually with application code, or by things like SQL triggers (see later).
Dating is only enabled if the including class explicitly calls the Hoodoo::ActiveRecord::Dated::ClassMethods#dating_enabled method.
Database table requirements
In all related tables, all date-time values must be stored as UTC.
The primary table must have a unique column named id
and two timestamp columns named updated_at
and
created_at
which both need to be set by the application code
(the ActiveRecord
timestamps
macro in a migration file defines appropriate
columns).
The history table requires the same columns as the primary table with two differences:
-
The history table's
id
column must be populated with any unique value whilst the history table'suuid
column must be populated with the primary table'sid
value. -
The history table must have two additional columns,
effective_start
andeffective_end
. Theeffective_start
column determines when the history entry becomes effective (inclusive) whilst theeffective_end
determines when the history entry was effective to (exclusive). A record is considered to be effective at a particular time if that time is the same or after theeffective_start
and before theeffective_end
.The
effective_start
must be set to theeffective_end
of the last record with sameuuid
, or to thecreated_at
of the record if there is no previous records with the sameuuid
.The
effective_end
must be set to the current time (UTC) when deleting a record or to the updated record'supdated_at
when updating a record.
Additionally there are two constraints on the history table that must not be broken for the finder methods to function correctly:
-
When adding a record to the history table its
effective_end
must be after all other records in the history table with the sameuuid
. -
When inserting a new record to the primary table its
id
must not exist in the history table.
The history table name defaults to the name of the primary table
concatenated with _history_entries
. This can be overriden when
calling Hoodoo::ActiveRecord::Dated::ClassMethods#dating_enabled.
Example:
class Post < ActiveRecord::Base
include Hoodoo::ActiveRecord::Dated
dating_enabled( history_table_name: 'historical_posts' )
end
Migration assistance
Compatible database migration generators are included in
service_shell
. These migrations create the history table and
add database triggers (PostgreSQL specific) which will handle the creation
of the appropriate history entry when a record is deleted or updated
without breaking the history table constraints. See github.com/LoyaltyNZ/service_shell/blob/master/bin/generators/effective_date.rb
for more information.
Model instance creation
It is VERY IMPORTANT that you use method Hoodoo::ActiveRecord::Creator::ClassMethods#new_in to create new resource instances when using dating. You could just manually read the `context.request.dated_from` value to ensure that an appropriate creation time is set; presently, `created_at` and `updated_at` are set from the `dated_from` value. However, using `new_in` for this isolates your code from any possible under-the-hood implementation changes therein and future-proofs your code.
Instantiates this module when it is included.
Example:
class SomeModel < ActiveRecord::Base
include Hoodoo::ActiveRecord::Dated
# ...
end
model
-
The ActiveRecord::Base descendant that is including this module.
Source: show
# File lib/hoodoo/active/active_record/dated.rb, line 123 def self.included( model ) model.class_attribute( :nz_co_loyalty_hoodoo_dated_with, { :instance_predicate => false, :instance_accessor => false } ) instantiate( model ) unless model == Hoodoo::ActiveRecord::Base super( model ) end
When instantiated in an ActiveRecord::Base subclass, all of the Hoodoo::ActiveRecord::Dated::ClassMethods methods are defined as class methods on the including class.
model
-
The ActiveRecord::Base descendant that is including this module.
Source: show
# File lib/hoodoo/active/active_record/dated.rb, line 143 def self.instantiate( model ) model.extend( ClassMethods ) end