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:

  1. The history table's id column must be populated with any unique value whilst the history table's uuid column must be populated with the primary table's id value.

  2. The history table must have two additional columns, effective_start and effective_end. The effective_start column determines when the history entry becomes effective (inclusive) whilst the effective_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 the effective_start and before the effective_end.

    The effective_start must be set to the effective_end of the last record with same uuid, or to the created_at of the record if there is no previous records with the same uuid.

    The effective_end must be set to the current time (UTC) when deleting a record or to the updated record's updated_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:

  1. When adding a record to the history table its effective_end must be after all other records in the history table with the same uuid.

  2. 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.

Namespace
Methods
I
Class Public methods
included( model )

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.

# 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
instantiate( model )

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.

# File lib/hoodoo/active/active_record/dated.rb, line 143
def self.instantiate( model )
  model.extend( ClassMethods )
end