Plugin for the PaperTrail gem to track and reify associations.
PR's will happily be accepted
This gem was extracted from PaperTrail in v9.2 to simplify things in PaperTrail and association tracking seperately.
At this time, paper_trail
has a runtime dependency on this gem and will keep running the existing tests related
to association tracking. This arrangement will be maintained for a few years, if practical.
A little history lesson, discussed as early as 2009, and first implemented in late 2014, association tracking was part of PT core until 2018 as an experimental feature and was use at your own risk. This gem now maintains a list of known issues and we hope the community can help remove some of them via PR's.
# Gemfile
gem 'paper_trail' # Requires v9.2+
gem 'paper_trail-association_tracking'
This plugin currently can restore three types of associations: Has-One, Has-Many, and Has-Many-Through. In order to do this, you will need to do two things:
- Create a
version_associations
table - Set
PaperTrail.config.track_associations = true
(e.g. in an initializer)
Both will be done for you automatically if you install PaperTrail with the
--with_associations
option
(e.g. rails generate paper_trail:install --with-associations
)
If you want to add this functionality after the initial installation, you will
need to create the version_associations
table manually, and you will need to
ensure that PaperTrail.config.track_associations = true
is set.
PaperTrail will store in the version_associations
table additional information
to correlate versions of the association and versions of the model when the
associated record is changed. When reifying the model, PaperTrail can use this
table, together with the transaction_id
to find the correct version of the
association and reify it. The transaction_id
is a unique id for version records
created in the same transaction. It is used to associate the version of the model
and the version of the association that are created in the same transaction.
To restore Has-One associations as they were at the time, pass option has_one: true
to reify
. To restore Has-Many and Has-Many-Through associations, use
option has_many: true
. To restore Belongs-To association, use
option belongs_to: true
. For example:
class Location < ActiveRecord::Base
belongs_to :treasure
has_paper_trail
end
class Treasure < ActiveRecord::Base
has_one :location
has_paper_trail
end
treasure.amount # 100
treasure.location.latitude # 12.345
treasure.update_attributes amount: 153
treasure.location.update_attributes latitude: 54.321
t = treasure.versions.last.reify(has_one: true)
t.amount # 100
t.location.latitude # 12.345
If the parent and child are updated in one go, PaperTrail-AssociationTracking can use the
aforementioned transaction_id
to reify the models as they were before the
transaction (instead of before the update to the model).
treasure.amount # 100
treasure.location.latitude # 12.345
Treasure.transaction do
treasure.location.update_attributes latitude: 54.321
treasure.update_attributes amount: 153
end
t = treasure.versions.last.reify(has_one: true)
t.amount # 100
t.location.latitude # 12.345, instead of 54.321
By default, PaperTrail-AssociationTracking excludes an associated record from the reified parent
model if the associated record exists in the live model but did not exist as at
the time the version was created. This is usually what you want if you just want
to look at the reified version. But if you want to persist it, it would be
better to pass in option mark_for_destruction: true
so that the associated
record is included and marked for destruction. Note that mark_for_destruction
only has an effect on associations marked with autosave: true
.
class Widget < ActiveRecord::Base
has_paper_trail
has_one :wotsit, autosave: true
end
class Wotsit < ActiveRecord::Base
has_paper_trail
belongs_to :widget
end
widget = Widget.create(name: 'widget_0')
widget.update_attributes(name: 'widget_1')
widget.create_wotsit(name: 'wotsit')
widget_0 = widget.versions.last.reify(has_one: true)
widget_0.wotsit # nil
widget_0 = widget.versions.last.reify(has_one: true, mark_for_destruction: true)
widget_0.wotsit.marked_for_destruction? # true
widget_0.save!
widget.reload.wotsit # nil
Associations have the following known issues, in order of descending importance. Use in Production at your own risk.
PR's for these issues will happily be accepted
If you notice anything here that should be updated/removed/edited feel free to create an issue.
-
PaperTrail-AssociationTracking only reifies the first level of associations.
-
Sometimes the has_one association will find more than one possible candidate and will raise a
PaperTrailAssociationTracking::Reifiers::HasOne::FoundMoreThanOne
error. For example, seespec/models/person_spec.rb
- If you are not using STI, you may want to just assume the first result (of multiple) is the correct one and continue. PaperTrail <= v8 did this without error or warning. To do so add the following line to your initializer:
PaperTrail.config.association_reify_error_behaviour = :warn
. Valid options are:[:error, :warn, :ignore]
- When using STI, even if you enable
:warn
you will likely still end up recieving anActiveRecord::AssociationTypeMismatch
error.
- If you are not using STI, you may want to just assume the first result (of multiple) is the correct one and continue. PaperTrail <= v8 did this without error or warning. To do so add the following line to your initializer:
-
Not compatible with transactional tests, aka. transactional fixtures. - PT Issue #542
-
Requires database timestamp columns with fractional second precision.
- Sqlite and postgres timestamps have fractional second precision by default. MySQL timestamps do not. Furthermore, MySQL 5.5 and earlier do not support fractional second precision at all.
- Also, support for fractional seconds in MySQL was not added to rails until ActiveRecord 4.2 (rails/rails#14359).
-
PaperTrail-AssociationTracking can't restore an association properly if the association record can be updated to replace its parent model (by replacing the foreign key)
-
Currently PaperTrail-AssociationTracking only supports a single
version_associations
table. Therefore, you can only use a single table to store the versions for all related models. Sorry for those who use multiple version tables. -
PaperTrail-AssociationTracking relies on the callbacks on the association model (and the :through association model for Has-Many-Through associations) to record the versions and the relationship between the versions. If the association is changed without invoking the callbacks, Reification won't work. Below are some examples:
Given these models:
class Book < ActiveRecord::Base has_many :authorships, dependent: :destroy has_many :authors, through: :authorships, source: :person has_paper_trail end class Authorship < ActiveRecord::Base belongs_to :book belongs_to :person has_paper_trail # NOTE end class Person < ActiveRecord::Base has_many :authorships, dependent: :destroy has_many :books, through: :authorships has_paper_trail end
Then each of the following will store authorship versions:
@book.authors << @dostoyevsky @book.authors.create name: 'Tolstoy' @book.authorships.last.destroy @book.authorships.clear @book.author_ids = [@solzhenistyn.id, @dostoyevsky.id]
But none of these will:
@book.authors.delete @tolstoy @book.author_ids = [] @book.authors = []
Having said that, you can apparently get all these working (I haven't tested it myself) with this patch:
# config/initializers/active_record_patch.rb class HasManyThroughAssociationPatch def delete_records(records, method) method ||= :destroy super end end ActiveRecord::Associations::HasManyThroughAssociation.prepend(HasManyThroughAssociationPatch)
See PT Issue #113 for a discussion about this.
At this time during reify
any STI has_one
associations will raise a PaperTrailAssociationTracking::Reifiers::HasOne::FoundMoreThanOne
error. See PT Issue #594
Something to note though, is while the PaperTrail gem supports Single Table Inheritance, I do NOT recommend STI ever. Your better off rolling your own solution rather than using STI.
See the paper_trail contribution guidelines
Plugin authored by Weston Ganger & Jared Beck
Maintained by Weston Ganger & Jared Beck
Associations code originally contributed by Ben Atkins, Jared Beck, Andy Stewart & more