Rails HABTM after_add callback fires before saving primary object - ruby-on-rails

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

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.

Related

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

Updating association without saving it

I have a model:
class A < ActiveRecord::Base
has_many :B
end
And I want to reset or update A's B association, but only save it later:
a = A.find(...)
# a.bs == [B<...>, B<...>]
a.bs = []
#or
a.bs = [B.new, B.new]
# do some validation stuff on `a` and `a.bs`
So there might be some case where I will call a.save later or maybe not. In the case I don't call a.save I would like that a.bs stay to its original value, but as soon as I call a.bs = [], the old associations is destroyed and now A.find(...).bs == []. Is there any simple way to set a record association without persisting it in the database right away? I looked at Rails source and didn't find anything that could help me there.
Thanks!
Edit:
I should add that this is for an existing application and there are some architecture constraint that doesn't allow us to use the the regular ActiveRecord updating and validation tools. The way it works we have a set of Updater class that take params and assign the checkout object the value from params. There are then a set of Validater class that validate the checkout object for each given params. Fianlly, if everything is good, we save the model.
In this case, I'm looking to update the association in an Updater, validate them in the Validator and finally, persist it if everything check out.
In summary, this would look like:
def update
apply_updaters(object, params)
# do some stuff with the updated object
if(validate(object))
object.save(validate: false)
end
Since there are a lot of stuff going on between appy_updaters and object.save, Transaction are not really an option. This is why I'm really looking to update the association without persisting right away, just like we would do with any other attribute.
So far, the closest solution I've got to is rewriting the association cache (target). This look something like:
# In the updater
A.bs.target.clear
params[:bs].each{|b| A.bs.build(b)}
# A.bs now contains the parameters object without doing any update in the database
When come the time to save, we need to persist cache:
new_object = A.bs.target
A.bs(true).replace(new_object)
This work, but this feel kind of hack-ish and can easily break or have some undesired side-effect. An alternative I'm thinking about is to add a method A#new_bs= that cache the assigned object and A#bs that return the cached object if available.
Good question.
I can advice to use attributes assignment instead of collection manipulation. All validations will be performed as regular - after save or another 'persistent' method. You can write your own method (in model or in separated validator) which will validate collection.
You can delete and add elements to collection through attributes - deletion is performed by additional attribute _destroy which may be 'true' or 'false' (http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), addition - through setting up parent model to accept attributes.
As example set up model A:
class A < ActiveRecord::Base
has_many :b
accepts_nested_attributes_for :b, :allow_destroy => true
validates_associated :b # to validate each element
validate :b_is_correct # to validate whole collection
def b_is_correct
self.bs.each { |b| ... } # validate collection
end
end
In controller use plain attributes for model updating (e.g update!(a_aparams)). These methods will behave like flat attribute updating. And don't forget to permit attributes for nested collection.
class AController < ApplicationController
def update
#a = A.find(...)
#a.update(a_attributes) # triggers validation, if error occurs - no changes will be persisted and a.errors will be populated
end
def a_attributes
params.require(:a).permit([:attr_of_a, :b_attributes => [:attr_of_b, :_destroy]])
end
end
On form we used gem nested_form (https://github.com/ryanb/nested_form), I recommend it. But on server side this approach uses attribute _destroy as mentioned before.
I finally found out about the mark_for_destruction method. My final solution therefor look like:
a.bs.each(&:mark_for_destruction)
params[:bs].each{|b| a.bs.build(b)}
And then I can filter out the marked_for_destruction? entry in the following processing and validation.
Thanks #AlkH that made me look into how accepts_nested_attributes_for was working and handling delayed destruction of association.

What is the difference between `before_create` and `after_create` and when to use which?

I know that before_create is called before the object gets commuted to the database and after_create gets called after.
The only time when before_create will get called and after_create while not is if the object fails to meet data base constants (unique key, etc.). Other that that I can place all the logic from after_create in before_create
Am I missing something?
In order to understand these two callbacks, firstly you need to know when these two are invoked. Below is the ActiveRecord callback ordering:
(-) save
(-) valid
(1) before_validation
(-) validate
(2) after_validation
(3) before_save
(4) before_create
(-) create
(5) after_create
(6) after_save
(7) after_commit
you can see that before_create is called after after_validation, to put it in simple context, this callback is called after your ActiveRecord has met validation. This before_create is normally used to set some extra attributes after validation.
now move on to after_create, you can see this is created after the record is stored persistently onto DB. People normally use this to do things like sending notification, logging.
And for the question, when should you use it? The answer is 'you should not use it at all'. ActiveRecord callbacks are anti-pattern and seasoned Rails developer consider it code-smell, you can achieve all of that by using Service object to wrap around. Here is one simple example:
class Car < ActiveRecord::Base
before_create :set_mileage_to_zero
after_create :send_quality_report_to_qa_team
end
can be rewritten in
# app/services/car_creation.rb
class CarCreation
attr_reader :car
def initialize(params = {})
#car = Car.new(params)
#car.mileage = 0
end
def create_car
if car.save
send_report_to_qa_team
end
end
private
def send_report_to_qa_team
end
end
If you have simple app, then callback is okay, but as your app grows, you will be scratching your head not sure what has set this or that attribute and testing will be very hard.
On second thought, I still think you should extensively use callback and experience the pain refactoring it then you'll learn to avoid it ;) goodluck
The before_create callback can be used to set attributes on the object before it is saved to the database. For example, generating a unique identifier for a record. Putting this in an after_create would require another database call.
before_create:
will be called before saving new object in db. When this method will return false it will prevent the creation by rolling back.
So when you need to do something like check something before saving which is not appropriate in validations you can use them in before_create.
For example: before creation of new Worker ask Master for permission.
before_create :notify_master
def notify_master
# notify_master via ipc and
# if response is true then return true and create this successfully
# else return false and rollback
end
Another use is as Trung LĂȘ suggested you want to format some attribute before saving
like capitalizing name etc.
after_create:
Called after saving object in database for first time. Just when you don't want to interrupt creation and just take a note of creation or trigger something after creation this is useful.
for example: After creating new user with role mod we want to notify other mods
after_create :notify_mod, :is_mod?
def notify_mod
# send notification to all other mods
end
EDIT: for below comment
Q: What's the advantage of putting notify_mod in after_create instead of before_create?
A: Sometimes while saving the object in database it can rollback due to database side validations or due to other issues.
Now if you have written notify_mod in before create then it will be processed even if the creation is not done. No doubt it will rollback but it generates overhead. so it's time consuming
If you have placed it in after_create then notify_mod will only execute if the record is created successfully. Thus decreasing the overhead if the rollback takes places.
Another reason is that it's logical that notification must be sent after user is created not before.

STI, delegate and becomes

I have some STI setup like this:
class Document < ActiveRecord::Base
attr_accessible :name, description
# Basic stuff omitted
end
class OriginalDocument < Document
has_many :linked_documents, foreign_key: :original_document_id, dependent: :destroy
end
class LinkedDocument < Document
belongs_to :original_document
# Delegation, because it has the same attributes, except the name
delegate :description, to: :original_document
end
Now I want to dup the LinkedDocument and store it as an OriginalDocument, with its own name and keep the attribute values on duplication. However, my approachs are failing because somewhere, the duplicate still wants to access its delegated methods in the after_* callbacks.
class LinkedDocument < Document
def unlink_from_parent
original = self.original_document
copy = self.becomes OriginalDocument
copy.original_document_id = nil
copy.description = original.description
copy.save
end
end
This throws a RuntimeError: LinkedDocument#description delegated to original_document.description, but original_document is nil.
Doing an additional copy.type = 'OriginalDocument' to enforce things won't work, since the save query involves the type; UPDATE documents SET [...] WHERE documents.type IN('OriginalDocument') [...]. This fails, because at the time of the transaction, the object still is of type LinkedDocument.
What would be a clean way to copy an object and let it become another one? I thought of calling update_column for type and every attribute I want to copy over, but before doing it that inelegant way, I wanted to ask here.
I am going to add my solution here, in case no one has a better one. Hopefully, it will help someone.
To let the object become another without having wrong queries because the where clause is checking for the wrong type, I manually updated the type column without invoking any callbacks before calling become.
# This is for rails3, where +update_column+ does not trigger
# validations or callbacks. For rails4, use
#
# self.update_columns {type: 'OriginalDocument'}
#
self.update_column :type, 'OriginalDocument'
document = self.becomes OriginalDocument
Now for the assignments, there were two problems: First, the attribute setters somehow may trigger an exception because of the delegations. Second, the attributes I wanted to mass-assign were not listed in e.g. attr_accessible intentionally because they were internal attributes. So I resorted to a loop with an ugly update_column statement producing way too much queries (since rails3 has no update_columns).
original.attributes.except('id', 'name', 'original_document_id').each do |k,v|
document.update_column k.to_sym, v
end

silently skip add with before_add association callback instead of raising an exception?

I'm trying to do this
has_many :roles, :before_add => :enforce_unique
def enforce_unique(assoc)
false if exists? assoc
end
From the docs: "If a before_add callback throws an exception, the object does not get added to the collection". The using false above does not prevent the add, so I'm forced to do this:
def enforce_unique(assoc)
raise if exists? assoc
end
This way, it's true that it doesn't get added, but it also raises an exception that has to be handled. Not very useful to me here. I would prefer this to behave more like regular AR callback before_save, where returning FALSE also prevents the save (or add) but doesn't raise an exception.
In this case above, I would prefer this to just not add the assoc silently. Is there a way to do this? I missing something? Or is raising an exception the only option here?
The way to solve it I think is to use throw and catch, which in Ruby are meant for flow control. Raising an exception is not a good fit, since this isn't an exceptional circumstance.
I ended up doing:
catch(:duplicate) do
association.create({})
end
And then in the before_add callback, I did:
if(Class.where({}).first)
throw :duplicate
end
More on throw/catch here:
http://rubylearning.com/blog/2011/07/12/throw-catch-raise-rescue-im-so-confused/
If the association isn't polymorphic you can do something like:
validates_uniqueness_of :name_of_model
inside of Role where name_of_model us what you are associating with
this question is a bit old, but i came across the same problem recently. here is how i solved it:
def enforce_unique |obj, x|
v = obj.roles
if i = v.index(x)
v.slice! i
end
end
Since this question is about saving rather than preventing it being included in the list temporarily (eg by a controller that is not interested in controlling its models) you could try overriding save in the related model and just not save it if the role exists:
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
def save
super unless self.new_record? && user.has_existing_role?(self)
end
end
Sidenote: I don't buy the skinny controller argument when used with the Active Record pattern as business logic has to be put somewhere. With a business domain poor pattern like Active Record (not referring to the Ruby AR gem specifically) it really needs to exist a layer above (ie in the controller layer), you may use service objects or the decorator pattern as a means to achieve this.
Another approach would be to override update methods like << for the association and silently drop the role if it matches an existing one. Details on overriding association methods are in the ActiveRecord Association Class Methods Documentation

Resources