问题
Question regarding the paper_trail gem.
When only associations change, a version record won't be created for the main model. So what's the right way to list all versions for a certain record including its associations?
Should I query something like this? (The bad point is this SQL query might be long and low performance.)
f = "(item_type = 'Place' AND item_id = ?) OR (item_type = 'PlaceName' AND item_id IN (?))"
PaperTrail::Version.where(f, @place.id, @place.names.map { |n| n.id })
Or should I create a version record when only associations changed? I think @DavidHam tried the same thing and asked a similar question but nobody has answered it yet.
回答1:
So, I sort of found a way to do this, but it's not exactly pretty and it doesn't create a new version everytime an association is changed. It does, however, give you an efficient way to retrieve the versions chronologically so you can see what the version looked like before/after association changes.
First, I retrieve all the ids for for the asscoiation versions given the id of that model:
def associations_version_ids(item_id=nil)
if !item_id.nil?
ids = PaperTrail::VersionAssociation.where(foreign_key_id: item_id, foreign_key_name: 'item_id').select(:version_id)
return ids
else
ids = PaperTrail::VersionAssociation.where(foreign_key_name: 'item_id').select(:version_id)
return ids
end
end
Then I get all versions together using the VersionAssociation ids from this function. It will return a chronological array of PaperTrail::Version's. So the information is useful for an activity log, etc. And it's pretty simple to piece back together a version and its associations this way:
def all_versions
if !@item_id.nil?
association_version_ids = associations_version_ids(@item_id)
all_versions = PaperTrail::Version
.where("(item_type = ? AND item_id = ?) OR id IN (?)", 'Item', @item_id, association_version_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
else
assocation_ids = associations_version_ids
all_versions = PaperTrail::Version
.where("item_type = ? OR id IN (?)", 'Item', association_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
end
end
Again, not a perfect answer since there isn't a new version everytime there's a change, but it's manageable.
回答2:
This is more of an approach than a specific answer, but here goes.
In my case, I needed a version history such that any time anyone changed a Child
, they also changed a flag on the `Parent'. But I needed a way to show an audit trail that would show the initial values for all the children, and an audit line for the parent whenever anyone changed a child.
So, much simplified, it's like this:
class Parent < ActiveRecord::Base
has_paper_trail
has_many :children
end
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent
end
So, whenever there's a change on a Child
we need to create a version on the Parent
.
First, try changing Child
as follows:
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent, touch: true
end
This should (should! have not tested) create a timestamp on the Parent
whenever the Child
changes.
Then, to get the state of the :children
at each version of Parent
, you search the Child
's versions for the one where the transaction_id
matches the Parent
.
# loop through the parent versions
@parent.versions.each do |version|
@parent.children.versions.each do |child|
# Then loop through the children and get the version of the Child where the transaction_id matches the given Parent version
child_version = child.versions.find_by transaction_id: version.transaction_id
if child_version # it will only exist if this Child changed in this Parent's version
# do stuff with the child's version
end
This worked in my situation, hope something in here is useful for you.
回答3:
[UPDATED]
I found a better way. You need to update associations inside transaction to make this code work.
class Place < ActiveRecord::Base
has_paper_trail
before_update :check_update
def check_update
return if changed_notably?
tracking_has_many_associations = [ ... ]
tracking_has_has_one_associations = [ ... ]
tracking_has_many_associations.each do |a|
send(a).each do |r|
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
tracking_has_one_associations.each do |a|
r = send(a)
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
end
class Version < PaperTrail::Version
def associated_versions
Version.where(transaction_id: transaction_id) if transaction_id
end
end
[Original Answer]
This is the best way I've found so far. (@JohnSchaum's answer helps me a lot, thanks!)
Before starting, I've added polymorphic_type
column to the versions
table.
class AddPolymorphicTypeToVersions < ActiveRecord::Migration
def change
add_column :versions, :polymorphic_type, :string
end
end
And setup models like this:
# app/models/version.rb
class Version < PaperTrail::Version
has_many :associations, class_name: 'PaperTrail::VersionAssociation'
end
# app/models/link.rb
class Link < ActiveRecord::Base
has_paper_trail meta: { polymorphic_type: :linkable_type }
belongs_to :linkable, polymorphic: true
end
# app/models/place.rb
class Place < ActiveRecord::Base
has_paper_trail
has_many :links, as: :linkable
def all_versions
f = '(item_type = "Place" AND item_id = ?) OR ' +
'(foreign_key_name = "place_id" AND foreign_key_id = ?) OR ' +
'(polymorphic_type = "Place" AND foreign_key_id = ?)'
Version.includes(:associations).references(:associations).where(f, id, id, id)
end
end
And we can now get versions including associations like following:
@place.all_versions.order('created_at DESC')
来源:https://stackoverflow.com/questions/34305935/whats-the-right-way-to-list-all-paper-trail-versions-including-associations