0

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.

GTeley
  • 23
  • 9
  • U mean before updating you want old_value but you are seeing new_Value right? – krishnar Oct 25 '17 at 12:56
  • @krishnar: Yes, As the associating is recorded in its own table, the old values are records that appear to be deleted from the associated table before I can get my hands on it. – GTeley Oct 25 '17 at 13:11
  • Well, I've found one way to get the old values: In the controller save method just before assigning the new values to the existing record. But this is IMHO not the intended Rails way to do it! – GTeley Oct 25 '17 at 13:17

3 Answers3

0

Use before_update and you can access old values using _was

before_update :record_values


def record_values
  p "oldvalue:  #{self.field_was} Newvalue: #{self.field}"
end
krishnar
  • 2,537
  • 9
  • 23
  • As I said: getting old values for the main model is no problem. Getting the associated (has_many) record is the problem. If I assign new values to the associated fields in the model controller, just before calling save, Rails updates the associated table(s) before letting me get them in the before_save callback. – GTeley Oct 25 '17 at 13:37
  • An *has_many* relation still has a *belongs_to* on the other side. What happens if you set up a *before_save* there? – 3limin4t0r Oct 25 '17 at 14:31
0

Your question is a bit unclear, but let me provide you with an example of what I think you try to do.

The code below works fine for me:

class Person < ApplicationRecord
  has_many :addresses

  validates_presence_of :name

  before_save { puts "before_save of person - changes: #{changes}" }
  before_destroy { puts "before_destroy of person with id: #{id}" }
end

class Address < ApplicationRecord
  belongs_to :person, required: true

  validates_presence_of :name

  before_save { puts "before_save of address - changes: #{changes}" }
  before_destroy { puts "before_destroy of address with id: #{id}" }
end

This results in the following output when interacting:

person = Person.create(name: 'Johan Wentholt')
# before_save of person - changes: {"name" =>[nil, "Johan Wentholt"]}
#=> #<Person id: 2, name: "Johan Wentholt", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:04:27">

person.addresses.create(name: 'Address #1')
# before_save of address - changes: {"person_id"=>[nil, 2], "name"  =>[nil, "Address #1"]}
#=> #<Address id: 7, person_id: 2, name: "Address #1", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:06:38">

person.addresses.last.update(name: 'Address without typo')
# before_save of address - changes: {"name"=>["Address #1", "Address without typo"]}
#=> true

person.update(name: 'Kaasboer')
# before_save of person - changes: {"name"=>["Johan Wentholt", "Kaasboer"]}
#=> true

person.addresses.last.destroy
# before_destroy of address with id: 7
#=> #<Address id: 7, person_id: 2, name: "Address without typo", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:08:51">

person.destroy
# before_destroy of person with id: 2
#=> #<Person id: 2, name: "Kaasboer", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:10:46">

As you can see this logs all changes. Like I said, the question is a bit unclear, but I hope this helps you further.

Keep in mind that some Rails methods don't trigger callbacks. For example: delete, update_all, update_column and some others.

For more about changes take a look at: ActiveModel::Dirty

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • Thanks for the reply. Maybe I wasn't clear enough. My problem is with many-to-many relationships who are recorded in an relation table. The detection of changes works fine in the has_many model, but not for the belongs_to model (has_many, :through) I solved it, sort of, in the main model controller by logging the changes before the new values are assigned to the related_to ids. – GTeley Oct 25 '17 at 18:00
0

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.

GTeley
  • 23
  • 9