ActiveRecord before_update callback understanding - ruby-on-rails

Do I understand correctly that if I execute
model = Model.create(some_attr: "attr_value")
model.update(some_attr: "new_attr_value")
and for Model I have
before_update :before_update_callback
def before_update_callback
some_attr
end
that callback will return "new_attr_value", since the internal ruby object (variable "model") changed before the callback was called.

Yes. Thats exactly what will happen.
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
assign_attributes(attributes)
save
end
end
See:
https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/Persistence.html#method-i-update
https://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html

Related

Generating a slug after Active Record object creation in rails

I'm trying to generate a unique slug for an object after the object is created using the after_commit callback.
after_commit :create_slug, on: :create
def create_slug
self.slug = generate_slug
self.save
end
When I try to save the object I get a "stack level too deep" error. I'm assuming because I'm saving the object and it's called the after_commit callback again and again.
What's the best way to generate and save the unique slug in this situation?
I recommend using the after_validation callback on create rather than the after_commit. You will be calling multiple transactions, which is not the intention of this callback. What I would do is this:
after_validation(on: :create) do
self.slug = generate_slug
end
Also make sure there are no save actions going on inside the generate_slug. That method should simply be returning a value to insert into the slug attribute.
Use a method that does not trigger callbacks like: update_column
def create_slug
update_column('slug ', generate_slug)
end

Rails 4 after_save previous_changes not working

I have an after_save callback on a model, and I'm calling previous_changes to see if an attribute (is_complete) changed. Even when the attribute changes, previous_changes returns an empty hash.
Here's the callback:
after_save do |record|
puts "********************"
puts record.previous_changes.to_s
puts record.is_complete
puts "********************"
end
and here's what I get in the log:
********************
{}
true
********************
********************
{}
false
********************
If the value of is_complete changed from true to false, it should be in the previous_changes hash. The update is being done via a normal save! and I'm not reloading the object.
--- UPDATE ---
I hadn't considered this when I posted the question, but my model uses the awesome_nested_set gem, and it appears that this is reloading the object or somehow interfering with the after_save callback. When I comment out acts_as_nested_set, the callback appears to be working fine.
--- UPDATE 2 ---
Fixed this with an around_save callback, which first determines if the attribute changed, then yields, then does the stuff I need it to do after the change has been made in the DB. The working solution looks like this:
around_save do |record, block|
is_complete_changed = true if record.is_complete_changed?
block.call
if is_complete_changed
** do stuff **
end
end
According to ActiveModel::Dirty source code
From line 274
def changes_applied # :doc:
#previously_changed = changes
#changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
So the changes will be set to #previously_changed after changes_applied was called, and changes_apply was call when save was called, that means AFTER DOING PERSISTENT WORK (line 42)
In summary, previous_changes only has values when the record was actually saved to persistent storage (DB)
So in your callback, you may use record.changed_attributes, and outside use previously_changed, it will work fine!
I did not dig super deep, but from the first sight into ActiveModel::Dirty you can see, that in method previous_changes:
def previous_changes
#previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
end
#previously_changed is not defined anywhere (except for here, which uses the changes method I speak of below), thus you get the empty (nice and with indifferent access though :D) hash all the time.
What you really want to use, is a changes method:
def changes
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
end
It would return your expected
#=> {"is_complete"=>[true, false]}

Callback conflict

Both of my callback methods have update_attributes in them. So it looks like when calculate_rating runs it also calls modify_rating. I only want calculate_rating to run for creating a new record and modify_rating to run only when editing and updating a record through a form.
after_create :calculate_rating
before_update :modify_rating
def calculate_rating
end
def modify_rating
end
From the fine manual for update_attributes:
Updates the attributes of the model from the passed-in hash and saves the record [...]
So when you call update_attributes, it will try to save the object and that means that update_attributes is not appropriate for either of the callbacks you're using; update_attributes is meant to be used by controllers for mass assignment and the like.
You could replace the update_attributes call with simple assignments:
def calculate_rating
self.attr1 = 11
self.attr2 = 23
#...
end

custom attr_reader in rails

Mostly in rails if you write my_obj.attr it looks up attr in the database and reports it back. How do you create a custom def attr method that internally queries the database for attr, modifies it and returns? In other words, what's the missing piece here:
# Within a model. Basic attr_reader method, will later modify the body.
def attr
get_from_database :attr # <-- how do I get the value of attr from the db?
end
Something like that:
def attr
value = read_attribute :attr
value = modify(value)
write_attribute :attr, value
save
value
end
Neutrino's method is good if you want to save a modified value back to the database each time you get the attribute. This not recommended since it will execute an extra database query every time you try to read the attribute even if it has not changed.
If you simply want to modify the attribute (such as capitalizing it for example) you can just do the following:
def attr
return read_attribute(:attr).capitalize #(or whatever method you wish to apply to the value)
end

Ruby on Rails: "after_create" and validations

I have a record that needs to be validated before doing some action. Am I required to use a "valid?" method if I'm doing it with after_create?
For example, I have in my User model:
def after_create
if valid?
...
end
end
I thought it wasn't necessary to put in the valid method, but my application is telling me otherwise. Any idea?
You do not need the if valid? declaration there because after_create gets called after the record has already been validated (and created).
What do you mean your application is telling you otherwise?
Also, for the callback methods, you should use something like:
after_create :call_my_method
private
def call_my_method
# Do cool stuff
end

Resources