AttributesHistory
Date-granular history for specified model fields. Compact & easy to query.
Usage
Include the gem:
gem 'attributes_history'
Call has_attributes_history for: [attributes], with_model: AttributesLog
where
attributes
are the ones you want to track and AttributesLog
will contain the
history entries. Your AttributesLog
model should have a field recorded_on
which tracks the date when those attributes changed to the values in the next
recorded entry (or to the current attribute values).
Example
Here's an example of how you could track the status
and pledge
fields
for a ministry donor contact in a PartnerStatusLog
table. This would then allow
you to easily query the ministry partner's status and commitment information
over time.
class Contact < ActiveRecord::Base
has_attributes_history for: [:status, :pledge], with_model: PartnerStatusLog
end
Here would be the ParterStatusLog
model and relevant migrations:
class PartnerStatusLog < ActiveRecord::Base
end
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts do |t|
t.string :name
t.string :status
t.decimal :pledge
end
end
end
class CreatePartnerStatusLogs < ActiveRecord::Migration
def change
create_table :partner_status_logs do |t|
t.integer :contact_id, null: false
t.date :recorded_on, null: false
t.string :status
t.decimal :pledge
end
add_index :partner_status_logs, :contact_id
add_index :partner_status_logs, :recorded_on
end
end
Retrieving past values with attribute_on_date
methods
To make retrieving previous values easy, attributes_history
defines an
attribute_on_date(attribute, date)
method, as well as specific
#{attribute}_on_date
method for each of your histroy-tracked attributes
which returns that value on the specified date based on the log.
For instance, in the ministry partner history example, there would
be methods status_on_date
and pledge_on_date
that would return the
status
or pledge
for a contact for that given date. You could also call
attribute_on_date(:pledge, date)
to get the pledge value for a given date.
The log is granular by date and so it makes the assumption that a change
any time during a date is effective for the whole of that date. The
attribute_on_date
methods will use caching so if you look up multiple fields on
the same date only one query will be performed.
Querying the log table directly
You can also query the history log table directly. The recorded_on
field in the
table represents the date that set of attributes was replaced by a new
set, either in a subsequent history record, or in the object itself.
So to look up the version for a particular date, do a query like this:
current_version = contact.partner_status_logs
.where('recorded_on > ?', date).order(:recorded_on).first || contact
That will give either a PartnerStatusLog
instance for the past, or the current
Contact
instance for the present record, both of which will respond to the
history-tracked attributes of status
and pledge
.
This is similar to how paper_trail works in that the versions represent past data, and only the current regular model record (contact in this case) has the current state.
Multiple has_attributes_history
calls per class
If you want to have some attributes (or groups of attributes) stored in
different tables grouped by semantic meaning or because their size or rate of
change is different, you can specify multiple has_attributes_history
calls per
class. The lists of attributes for the different calls can't have any
overlapping attributes though or that will confuse the lookup logic.
Here's an example of tracking notes
in a separate log from status
and
pledge
:
class Contact < ActiveRecord::Base
has_attributes_history for: [:status, :pledge], with_model: PartnerStatusLog
has_attributes_history for: [:notes], with_model: ContactNotesLog
end
Enabling and disabling
By default the history logging is enabled once you set it up, but you can
disable it by setting AttributesHistory.enabled = false
(and reset it back to
true
also).
Testing with RSpec
For testing with RSpec, you can require 'attributes_history/rspec'
which will
disable attribute history by default in your specs unless you specify
versioning: true
in the spec metadata, or you explicitly set
AttributesHistory.enabled = true
.
Designed to complement (not replace) a full audit trail
This is intended to augment a full audit trail solution like paper_trail. The advantage of a full audit trail is that you track every change in a consistent way across models.
But it's possible for the full audit trail to become large and it's often stored in
a less easily queryable way (object data stored in a generic object
field as
YAML/JSON).
If you use auto-saving or make single attribute changes easy, then you may get a lot of updates in the same day which semantically represent a single update.
This attributes_history
gem allows you to choose a subset of fields for a
particular model that will be tracked with at most one new version per day to
limit growth and make time-series displaying of the versions easier. And is
designed to store the versions in specialized table(s) per model with fields that
parallel those in the model itself so you can more easily query the historical
fields.
Acknowledgement and License
Credit to Spencer Oberstadt for coming up with the idea of versioning our partner status log with date level granularity to keep it compact and easy to query.
AttributesHistory is MIT Licensed.