简体   繁体   中英

Seeking advice on optimizing ActiveRecord::destroy_all

Rails 5+

I'm aware that destroy_all instantiates every model and runs destroy on it and that delete_all is faster, but deleting doesn't respect:

  • before_destroy , around_destroy and after_destroy callbacks
  • dependent settings on relationships

Assuming that list is comprehensive, shouldn't we be able to save time with destroy_all by checking these properties of the model and if there are no callbacks, just address the relationships as needed?

edit: I'm looking for a way to modify the default destroy_all behaviour so that it's smarter and doesnt blindly instantiate all objects and chain calls to dependent relationships. If we have relationship A with dependent B (1:1), and A is large (1 mil) that's a lot of objects to instantiate and destroy. Yes, application/domain specific knowledge means you can just call delete_all but if someone changes the model and adds relationships, that delete_all just became very dangerous. If we optimize destroy_all to do some thinking, we can reduce a simple dependent: delete relationship to two delete_all calls (A and B) from a single destroy_all on relation A, where the original destroy_all would be 2 million object instantiations and DB hits.

# 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

What I'm looking for is sanity checks on if I'm missing something obvious as to why this won't work. I'm also looking for suggestions on how I can shimmy this into ActiveRecord. Also, can you specifically override the destroy_all for collections/relationships on specific models?

The callback chain is accessible via the _*_callbacks method on an object. Active Model Callbacks support :before , :after and :around as values for the kind property. The kind property defines what part of the chain the callback runs in.

To find all callbacks in the before_save callback chain:

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

So in this case its _destroy_callbacks but I would make the method raise an exception and bail instead of calling destroy_all if your goal is a sanity check.

raise SomeKindOfError if model._destroy_callbacks.any? 

That's far more helpful in terms of debugging and usage instead of just burying the problem.

Getting all the associations of model can be done via .reflect_on_all_associations which gives you AssocationReflection objects. From there you can get the options of the association.

But...

This reeks of "clever" code. When you get to the point where using destroy callbacks is a performance problem there are bigger problems then just choosing between delete_all or destroy_all and automatically choosing does not really address the problem at all.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM