Есть ли цикличность Rails 4:: разрушить обходное решение?

В качестве примера для круговой dependent: :destroy проблемы:

class User < ActiveRecord::Base
  has_one: :staff, dependent: :destroy
end

class Staff < ActiveRecord::Base
  belongs_to :user, dependent: :destroy
end

Если я вызываю user.destroy, связанный с ним staff также должен быть уничтожен. И наоборот, вызов staff.destroy должен также уничтожить связанный user.

Это отлично работало в Rails 3.x, но поведение изменилось в Rails 4.0 (и продолжается в 4.1), так что цикл формируется и, в конце концов, вы получаете сообщение об ошибке "слишком высокий уровень стека". Одним из очевидных способов решения проблемы является создание пользовательского обратного вызова с помощью before_destroy или after_destroy для ручного уничтожения связанных объектов вместо использования механизма dependent: :destroy. Даже проблема в GitHub, открытая для этого, имела пару человек, рекомендующих это обходное решение.

К сожалению, я даже не могу заставить это обходное решение работать. Это то, что у меня есть:

class User < ActiveRecord::Base
  has_one: :staff

  after_destroy :destroy_staff

  def destroy_staff
    staff.destroy if staff and !staff.destroyed?
  end
end

Причина, по которой это не работает, заключается в том, что staff.destroyed? всегда возвращает false. Таким образом, он образует цикл.

Ответ 1

Если одна сторона цикла имеет только один обратный вызов, вы можете заменить один из dependent: :destroy на dependent: :delete

class User < ActiveRecord::Base
  # delete prevents Staff :destroy callback from happening
  has_one: :staff, dependent: :delete
  has_many :other_things, dependent: :destroy
end

class Staff < ActiveRecord::Base
  # use :destroy here so that other_things are properly removed
  belongs_to :user, dependent: :destroy
end

Работал отлично для меня, если одной стороне не нужны другие обратные вызовы.

Ответ 2

Я тоже столкнулся с этой проблемой и придумал решение, которое не очень красиво, но работает. По сути, вы просто используете destroy_user, который похож на destroy_staff.

class User < ActiveRecord::Base
  has_one: :staff

  after_destroy :destroy_staff

  def destroy_staff
    staff.destroy if staff && !staff.destroyed?
  end
end

class Staff < ActiveRecord::Base
  belongs_to :user

  after_destroy :destroy_user

  def destroy_user
    user.destroy if user && !user.destroyed?
  end
end