TemporalTables
Easily recall what your data looked like at any point in the past! TemporalTables sets up and maintains history tables to track all temporal changes to to your data.
Installation
Add this line to your application's Gemfile:
gem 'temporal_tables'
And then execute:
$ bundle
Or install it yourself as:
$ gem install temporal_tables
Usage
Schema
In your rails migration, specify that you want a table to have its history tracked:
create_table :people, temporal: true do |t|
...
end
This will create a history table called "people_h" which will maintain a history of all changes to any records in people with the use of triggers on any create/update/delete operation.
Any subsequent schema changes to people will be reflected automatically in people_h.
# Nothing extra required -- people_h will automatically get an "name" column too!
add_column :people, :name, :string
To track the history of a pre-existing table, just call add_temporal_table
:
add_temporal_table :people
Querying
For the below queries, we'll assume the following schema:
class Person < ActiveRecord::Base
belongs_to :coven, optional: true
has_many :warts
def to_s
parts = [name]
parts << "from #{coven.name}" if coven
parts.join ' '
end
end
class Coven < ActiveRecord::Base
has_many :members, class_name: "Person"
def to_s
name
end
end
class Wart < ActiveRecord::Base
belongs_to :person
scope :very_hairy, -> { where(arel_table[:num_hairs].gteq(3)) }
def to_s
"wart on #{location} with #{pluralize num_hairs, 'hair'}"
end
end
You can query the history tables by calling history
on the class.
Person #=> Person(id: :integer, name: :string)
Person.history #=> PersonHistory(history_id: :integer, id: :integer, name: :string, eff_from: :datetime, eff_to: :datetime)
You can easily get a history of all changes to a record.
Person.history.where(id: 1).map { |p| "#{p.eff_from}: #{p.to_s}" }
# => [
# "1974-01-14: Emily",
# "2003-11-03: Grunthilda from Delta Gamma Gamma"
# ]
A convenient history
method is mixed into ActiveRecord classes. The above could be rewritten as:
grunthilda = Person.find(1)
grunthilda.history.map { |p| "#{p.eff_from}: #{p.to_s}" }
You can query for records as they were at any point in the past by calling at
.
Person.history.at(2.years.ago).where(id: 1).first.name #=> "Grunthilda"
# alternatively:
grunthilda = Person.find(1)
grunthilda.history.at(2.years.ago).first.name #=> "Grunthilda"
Associations work too.
grunthilda = Person.history.at(20.years.ago).find_by_name("Grunthilda")
grunthilda.warts.count #=> 2
grunthilda = Person.history.at(1.year.ago).find_by_name("Grunthilda")
grunthilda.warts.count #=> 13
grunthilda.warts.first.class.name #=> "WartHistory"
And scopes also!
grunthilda = Person.history.at(1.year.ago).find_by_name("Grunthilda")
grunthilda.warts.count #=> 13
grunthilda.warts.very_hairy.count #=> 7
Instance methods are inherited.
grunthilda.to_s #=> "Grunthilda from Delta Gamma Gamma"
grunthilda.class.name #=> "PersonHistory"
grunthilda.class.superclass.name #=> "Person"
Config
You can configure temporal_tables in an initializer.
Create temporal tables for all tables by default (default = false)
TemporalTables.create_by_default = true
Don't create temporal tables for these tables. (default = %wsessions ar_internal_metadata)
TemporalTables.skip_temporal_table_for :table_one, :table_two
Add an updated_by
column to all temporal tables to track who made any changes, which is quite useful for auditing. Defaults to a :string field. The block is called when records are saved to determine the value to place within the updated_by
field. updated_by
fields are only auto-created if this is configured.
TemporalTables.add_updated_by_field(:integer) { User.current_user&.id }
Development
To add a new version of rails to the specs, create corresponding Gemfiles in the gemfiles/
directory, and add references to them in .travis.yml
.
To run specs, run rake matrix:install
and rake matrix:spec
.
Copyright
See LICENSE for more details.