Rails HABTM after_add callback fires before saving primary object
Asked Answered
B

1

5

I have two ActiveRecord models having a HABTM relationship with eachother. When I add an AccessUnit through a form that allows zones to be added by checking checkboxes I get an exception that the AccessUnitUpdaterJob can't be enqueued because the access unit passed can't be serialized (due to the fact that the identifier is missing). When manually calling save on the primary object, the issue is resolved but of course this is a workaround and not a proper fix.

TLDR; it seems the after_add callback is triggered before the main object is saved. I'm actually unsure if this is a bug in Rails or expected behavior. I'm using Rails 5.

The exact error I encounter is:

ActiveJob::SerializationError in AccessUnitsController#create

Unable to serialize AccessUnit without an id. (Maybe you forgot to call save?)

Here's some code so you can see the context of the issue:

class AccessUnit < ApplicationRecord
  has_and_belongs_to_many :zones, after_add: :schedule_access_unit_update_after_zone_added_or_removed, after_remove: :schedule_access_unit_update_after_zone_added_or_removed

  def schedule_access_unit_update_after_zone_added_or_removed(zone)
    # self.save adding this line solves it but isn't a proper solution
    puts "Access unit #{name} added or removed to zone #{zone.name}"

    # error is thrown on this line
    AccessUnitUpdaterJob.perform_later self
  end
end

class Zone < ApplicationRecord
  has_and_belongs_to_many :access_units
end
Bullheaded answered 17/11, 2016 at 21:8 Comment(1)
At least it seems like api.rubyonrails.org/classes/ActiveRecord/Associations/… should specify in the "Association extensions" section whether after_add is triggered before save.Futuristic
C
4

In my point of view it is not a bug. Every thing works as expected . You can create a complex graph of objects before you save this graph. During this creation phase, you can add objects to an association. This is the point in time where you want fire this callback, because it says after_add and not after_save.

For instance:

@post.tags.build name: "ruby" # <= now you add the objects
@post.tags.build name: "rails" # <= now you add the objects
@post.save! # <= now it is to late, for this callback, you added already multiple objects

Maybe with a before_add callback it makes more sense:

class Post
   has_many :tags, before_add: :check_state

   def check_state(_tag)
     if self.published?
        raise CantAddFurthorTags, "Can't add tags to a published Post"
     end
   end
end

@post = Post.new
@post.tags.build name: "ruby" 
@post.published = true
@post.tags.build name: "rails" # <= you wan't to fire the before_add callback now, to know that you can't add this new object 
@post.save! # <= and not here, where you can't determine which object caused the error

You can read a little bit about these callback within the book "The Rails 4 Way"

In your case you have to rethink your logic. Maybe you can use an after_savecallback. My 2 cents: You consider switching from callbacks to service object. Callbacks don't come without a cost. They are not always easy to debug and test.

Caucasoid answered 26/11, 2016 at 12:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.