-
-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detaching a record via an association calls delete_all, skipping ActiveRecord callbacks, instead of destroy. #2512
Comments
Hello @carbonatedcoder Thanks for reporting this issue. We use indeed We're open to suggestions here. Related PRs: |
@Paul-Bob, thanks for the reply, much appreciated! I apologize if I misunderstood, but continuing the example from my original post, calling Given that, it still seems like It seems like Avo should defer to what users define in the models to happen on destroy as users may have custom definitions or ActiveRecord callback methods that need to be respected upon detachment. By calling |
Agree but it does (not both, only the detached record). Check this: https://edgeapi.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-destroy
Notice that we don't do the |
@Paul-Bob, thanks for the additional context. Is there a suggested way (or perhaps maybe an option that we could specify on the field/resource) to handle the situation outlined in the original post where we need to make sure that ActiveRecord callbacks (or validations), defined in the models, are hit upon a record getting removed via a detachment? I'd imagine this would be a relatively common need for Avo users. For example, when detaching a Thanks! |
Just restating the scenario here to see if I've got it right: class User < ApplicationRecord
has_many :facility_users
has_many :facilities, through: :facility_users
end
class Facility < ApplicationRecord
has_many :facility_users
has_many :users, through: :facility_users
end
class FacilityUser < ApplicationRecord
belongs_to :user
belongs_to :facility
before_destroy :custom_cleanup
private
def custom_cleanup
puts "Running custom cleanup for FacilityUser #{id}"
end
end Given this scenario, when you click "Detach" on a facility associated with a user, I think the current behavior (if I'm understanding right) is based off of this code: @model.send(association_name).delete @attachment_model If I'm reading this correctly, I think that ends up as What do you think about avoiding the association altogether and focusing on destroying the attachment model itself? @attachment_model.destroy |
@carbonatedcoder just to clarify, we definitely understand the problem and want to add support for detach callbacks, any PR in that direction is welcome! I did some research and can't find a straight forward method on rails that does the detach, runs the callbacks and respect the |
@coffenbacher you're right, in this situation destroying the join record should do the trick but there are situation without join tables and then we cant just destroy the attachment record. We need to tailor a solution that fits for all use cases |
Ah, yes great point @Paul-Bob, zooming out to other scenarios I'm thinking through this... if reflection_class == "HasManyReflection"
@model.send(association_name).delete @attachment_model
else
@model.send("#{association_name}=", nil)
end Am I correct in understanding in pseudocode this is basically like this? I'm trying to think through what the scenarios are here... if (has_many_through_table? || has_many_directly?)
@model.send(association_name).delete @attachment_model
elsif (belongs_to?)
@model.send("#{association_name}=", nil)
end Maybe we can add a third path if there's a way to identify the scenarios in the reflections? if has_many_through_table?
@attachment_model.destroy
elsif has_many_directly?
@model.send(association_name).delete @attachment_model
elsif belongs_to?
@model.send("#{association_name}=", nil)
end |
Yes @coffenbacher my point is that it will work only on has many through, the solution should be implemented so callbacks get triggered even if is a direct has many. And we don't want to end up with a solution with so many ifs, checking each relation type, if dependent is destroy or nullify etc... I think that's rails responsibility. |
Just to ensure that we're all on the same page: # Break the association without running callbaks
@record.send(association_name).delete @attachment_record
# Break the association run callbaks and destroy @attachment_record even if dependent is nullify
@record.send(association_name).destroy @attachment_record What we want: @record.send(association_name).magic_method @attachment_record Where the Wonder if we should create a rails issue to aboard this question |
Edit - woops, sorry, responded out of order here without seeing your latest comment. Your If not, and I don't know how hard things are to get added to Rails (if this is a necessary add), here's what I had written prior about that scenario: -- Yeah totally agree, I think leveraging the Rails behavior whenever possible is ideal. In this case, however, I think there's a semantic mismatch between the behavior that makes sense when we click Avo's 'Detach' button and the tools that Rails exposes, at least that I'm aware of. Those docs you shared for the Rails association delete/destroy is the closest I know of, but the behavior on those methods is rough enough that I think it can basically be considered a bug when connected to the concept of 'Detach'. The current implementation is as good as it gets with the Rails basics I think, but as we ran into here, it's not as good as it could be. It's somewhat surprising that they don't have a better match for 'Detach', but I can kind of see why after thinking about it. In application code, it rarely comes up because it's always so specific what people want to do - destroy a through record, destroy a child record, nullify a belongs_to relationship etc... so the ActiveRecord tools work there. I think given Avo's position as a foundational tool, having a small chunk of logic on top of Rails supporting the Avo conception of 'Detach' may make sense?? I think the full tree might look like this? Just as an example trying to wrap my head around the intended behavior in the different scenarios, I'm sure there would be a cleaner way to represent it. if has_many_through_table?
@attachment_model.destroy
elsif has_many_directly? && detachable?
@attachment_model.update("#{reverse_association_name}=", nil) # pseudocode
elsif has_many_directly? && !detachable?
raise 'Cannot detach, must destroy attached record'
elsif belongs_to?
@model.send("#{association_name}=", nil)
end |
This issue has been marked as stale because there was no activity for the past 15 days. |
I just bumped into this. I'm using a through model association. It would be great if something like @coffenbacher suggest – calling destroy for such cases to run callbacks. However, it can be bypassed now by adding the model itself as Avo resource and deleting the association there, not great though. However I believe this should be mentioned in the docs as it is potentially destructive in the terms of data integrity. I (as everbody?) expected Avo to use exclusively destroys and not deletes. Also I just discovered that it is even more complex issue in Rails, as there is a difference between
Probably not creating/deleting the join model directly ever is the correct approach. |
ApproachAdd one conditional check ( When that conditional check is valid find the join record and apply I have some code that isn't working in all cases, but it might provide some inspiration. # spec/dummy/app/models/team_membership.rb
# == Schema Information
#
# Table name: team_memberships
#
# id :bigint not null, primary key
# team_id :bigint not null
# user_id :bigint not null
# level :string
# created_at :datetime not null
# updated_at :datetime not null
#
class TeamMembership < ApplicationRecord
belongs_to :team
belongs_to :user
before_destroy :raise_here
def name
"#{team&.name} - #{user&.name}"
end
def raise_here
raise "here"
end
end # app/controllers/avo/associations_controller.rb
def destroy
association_name = BaseResource.valid_association_name(@record, @field.for_attribute || params[:related_name])
if reflection.class.eql?(ActiveRecord::Reflection::ThroughReflection)
join_table = reflection.join_table
source_foreign_key = reflection.source_reflection.foreign_key
through_foreign_key = reflection.through_reflection.foreign_key
join_record = join_table.camelize.singularize.constantize.find_by(source_foreign_key => @record.id, through_foreign_key => @attachment_record.id)
join_record.destroy!
elsif reflection_class == "HasManyReflection"
@record.send(association_name).delete @attachment_record
else
@record.send(:"#{association_name}=", nil)
end
respond_to do |format|
format.html { redirect_to params[:referrer] || resource_view_response_path, notice: t("avo.attachment_class_detached", attachment_class: @attachment_class) }
end
end
def reflection
@reflection ||= @record._reflections.with_indifferent_access[association_from_params]
end @binarygit let's talk about this one tomorrow. |
Describe the bug
Detaching an association record calls
delete_all
instead ofdestroy
ordestroy_all
. This skips any ActiveRecord callbacks that may exist on the association join record.Steps to Reproduce
Facility
.facilities
on theuser
resource.facility
from auser
. The callbackbefore_destroy
is not hit withinFacilityUser
. Rails console logs also show FacilityUser Delete All instead of something like FacilityUser Destroy.Expected behavior & Actual behavior
Although
delete_all
is more performant, we would expectdestroy
ordestroy_all
to be called upon detaching so that any ActiveRecord callbacks are hit. Is there a reason that deletion was chosen?Models and resource files
System configuration
Avo version: 3.4.1
Rails version: 7.0.8
Ruby version: 3.2.2
License type:
Are you using Avo monkey patches, overriding views or view components?
Additional context
Impact
Urgency
The text was updated successfully, but these errors were encountered: