When working with many-to-many relationships, I need to maintain a log file recording the changed values. Using the before_save and after_save callbacks works fine for the main (has_many) model itself but in the before_save callback the associated (belongs_to) records appear to be updated already! It seems rather odd to see that some parts of the data already have been updated before the 'before_save' callback is called.
Also, using callbacks in the associated model reveals that there is no before_destroy executed. Only the before_save gets called and shows the new values. I also tried the :prepend => :true option but that didn't gave other results.
When turning on SQL logging before the actual save in the main (has_many) model, I can see Rails is fetching the associated records, determines the differences and deletes the surplus record(s). The before_destroy of the associated model is not called. It then calls the before_save of the associated model and inserts the new ones (if any) and commits the transaction. This is all done JUST BEFORE the before_save of the main model.
Does anyone know how to fetch the associated records before they are changed? I would expect the before_destroy of the associated model would get called and let me handle it.
For clarity sake, lets give some extended information:
class Book < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :authors, :through => :titles
accepts_nested_attributes_for :authors
before_save :pre_save
after_save :post_save
before_destroy :pre_delete
def pre_save
@nr = self.new_record?
end
def pre_save
changed_values = []
if @nr
changed_values.push "New record created"
else
self.changes.each do |field, cvs|
changes.push("#{field} : #{cvs[0]} => #{cvs[1]}")
end
end
if changes.length > 0
BookLog.create(:book_id => self.id, :changed_values => changes.join(', '))
end
end
def pre_delete
BookLog.create(:book_id => self.id, :changed_values => "Deleted: #{self.name}")
end
end
class Title < ActiveRecord::Base
unloadable
belongs_to :book
belongs_to :author
end
class Author < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :books, :through => :titles
accepts_nested_attributes_for :books
end
class BooksController < ApplicationController
def edit
book = Book.find(params[:book][:id])
book.name = .....
===> Here the old values are still available <====
book.author_ids = params[:book][:author_ids]
===> Now the new values are written to the database! <====
book.save!
end
end
Changes to the Book record are perfectly logged. But there is no way to fetch the changed associated values for author_ids. A before_destroy callback in Title was not called, the after_save was.
I checked this with enabling the SQL logging just before the assignment of the new author_ids to the edited record. I could see that Rails determines the differences between the existing and new associated values, deletes the surplus form the Titles table and insert the extra ones (if any)
I solved it by moving the logging for the changes in Titles to the Books controller by comparing the old with the new values:
o_authors = book.author_ids
n_authors = params[:book][:author_ids].collect {|c| c.to_i}
diff = o_authors - n_authors | n_authors - o_authors
if !diff.empty?
changed_values = []
(o_authors - n_authors).each do |d|
changed_values.push("Removed Author: #{Author.find(d).name}")
end
(n_authors - o_authors).each do |d|
changed_values.push("Added Author: #{Author.find(d).name}")
end
BookLog.create(:book_id => book.id, :changed_values => changed_values)
end
book.author_ids = params[:book][:author_ids]
book.save!
Like I said, it works but IMHO it does not reflect the Rails way of doing things. I would have expected to get the previous author_ids in the same way as any other Book attribute.