Как получить предыдущие значения связанных записей?

При работе со связями "многие ко многим" мне нужно вести файл журнала, в котором записываются измененные значения. Использование обратных вызовов before_save и after_save отлично работает для самой главной модели (has_many), но в обратном вызове before_save связанные записи (assign_to) уже обновляются! Кажется довольно странным видеть, что некоторые части данных уже были обновлены до вызова обратного вызова before_save.

Кроме того, использование обратных вызовов в связанной модели показывает, что не было выполнено before_destroy. Только before_save вызывается и показывает новые значения. Я также попробовал:prepend =>:true, но это не дало других результатов.

При включении ведения журнала SQL до фактического сохранения в основной (has_many) модели я вижу, что Rails извлекает связанные записи, определяет различия и удаляет лишние записи. Before_destroy связанной модели не вызывается. Затем он вызывает before_save связанной модели и вставляет новые (если есть) и фиксирует транзакцию. Все это делается ТОЛЬКО ДО before_save основной модели.

Кто-нибудь знает, как получить связанные записи, прежде чем они будут изменены? Я ожидал, что before_destroy соответствующей модели будет вызван и позволит мне обработать это.

3 ответа

Решение

Для ясности, давайте дадим некоторую расширенную информацию:

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

Изменения в записи книги отлично записываются. Но нет способа получить измененные связанные значения для author_ids. Обратный вызов before_destroy в Title не был вызван, after_save был.

Я проверил это, включив ведение журнала SQL непосредственно перед назначением новых author_ids отредактированной записи. Я мог видеть, что Rails определяет различия между существующими и новыми связанными значениями, удаляет излишки из таблицы Titles и вставляет дополнительные (если они есть)

Я решил это, переместив регистрацию изменений в заголовках в контроллер Books, сравнив старые значения с новыми:

     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!

Как я уже сказал, это работает, но ИМХО это не отражает способ работы Rails. Я бы ожидал получить предыдущие author_ids таким же образом, как и любой другой атрибут Book.

Используйте before_update, и вы можете получить доступ к старым значениям, используя _was

before_update :record_values


def record_values
  p "oldvalue:  #{self.field_was} Newvalue: #{self.field}"
end

Ваш вопрос немного неясен, но позвольте мне привести вам пример того, что я думаю, вы пытаетесь сделать.

Код ниже прекрасно работает для меня:

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

Это приводит к следующему выводу при взаимодействии:

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">

Как вы можете видеть, в этом журнале все изменения. Как я уже сказал, вопрос немного неясен, но я надеюсь, что это поможет вам в дальнейшем.

Имейте в виду, что некоторые методы Rails не вызывают обратные вызовы. Например: delete, update_all, update_column и некоторые другие.

Чтобы узнать больше об изменениях, взгляните на: ActiveModel:: Dirty

Другие вопросы по тегам