Ищу советы по оптимизации ActiveRecord :: destroy_all - PullRequest
0 голосов
/ 27 марта 2020

Rails 5 +

Я знаю, что destroy_all создает экземпляр каждой модели и запускает на ней destroy и что delete_all быстрее, но удаление не относится:

  • before_destroy, around_destroy и after_destroy обратные вызовы
  • dependent настройки отношений

Предполагая, что этот список является всеобъемлющим, разве мы не сможем сэкономить время с помощью destroy_all, проверив эти свойства модели и, если нет обратных вызовов, просто обратиться к нужным отношениям?

edit: я ищу способ изменить поведение по умолчанию destroy_all так что это умнее и не создает вслепую все объекты и цепные вызовы зависимых отношений. Если у нас есть отношения A с зависимой B (1: 1), а A большой (1 мил), это много объектов для создания и уничтожения. Да, знание приложения / домена c означает, что вы можете просто позвонить delete_all, но если кто-то изменит модель и добавит отношения, delete_all просто станет очень опасным. Если мы оптимизируем destroy_all для некоторого размышления, мы можем уменьшить простое отношение dependent: delete до двух delete_all вызовов (A и B) из одного destroy_all отношения A, где исходный destroy_all будет равен 2 миллион экземпляров объектов и посещений БД.

# Pseudocode
# Let the model in question be `User`

ids = self.pluck(:id)
if model.has_destroy_callbacks  # I imagine there's some fancy introspection stuff I can use 
  original_destroy_all
  return
else
  # Check Restrict type
  model.restrict_relationships.each do |rel|
    other_models = some_cute_query
    raise_exception_or_add_error if other_models.any?
  end

  # Add some check here to make sure we didn't miss any unknown dependency type

  # Normal relationships
  model.non_restrict_relationships.each do |rel|
    dep_type = rel.dependent_type
    if dep_type == :destroy
      rel.where(model_id: ids).destroy_all
    elsif dep_type == :delete
      rel.where(model_id: ids).delete_all
    elsif dep_type == :nullify
      rel.where(model_id: ids).update_all(model_name_id: nil)
    end
  end
end
self.delete_all # i.e. the collection that was gonna get destroyed

Мне нужны проверки работоспособности, если я упускаю что-то очевидное, почему это не сработает. Я также ищу предложения о том, как я могу включить это в ActiveRecord. Кроме того, можете ли вы специально переопределить destroy_all для коллекций / отношений в указанных c моделях?

1 Ответ

1 голос
/ 27 марта 2020

Цепочка обратных вызовов доступна с помощью метода _*_callbacks на объекте. Обратные вызовы активной модели поддерживают :before, :after и :around в качестве значений для свойства kind. Свойство kind определяет, в какой части цепочки выполняется обратный вызов.

Чтобы найти все обратные вызовы в цепочке обратных вызовов before_save:

Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }

Так что в этом случае это _destroy_callbacks, но я бы заставил метод вызвать исключение и залог вместо вызова destroy_all, если ваша цель - проверка работоспособности .

raise SomeKindOfError if model._destroy_callbacks.any? 

Это гораздо более полезно с точки зрения отладки и использования, чем просто скрывать проблему.

Получить все ассоциации модели можно с помощью .reflect_on_all_associations, которая предоставляет вам объекты AssocationReflection. Оттуда вы можете получить параметры ассоциации.

Но ...

Это пахнет "умным" кодом. Когда вы достигаете точки, где использование разрушающих обратных вызовов является проблемой производительности, возникают большие проблемы, чем просто выбор между delete_all или destroy_all, и автоматический выбор на самом деле вообще не решает проблему.

...