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]}
Related
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
In my model, I have:
after_initialize :populate_neutral_post_option
before_create :make_neutral_option_last
module RequiredOptions
NEUTRAL = "Neutral"
end
def populate_neutral_post_option
if self.new_record? && post_options.map(&:text).exclude?(RequiredOptions::NEUTRAL)
post_options.build(text: RequiredOptions::NEUTRAL)
end
end
def make_neutral_option_last
post_options.rotate!(1)
end
And I get a NoMethodError saying .rotate! is undefined. Plain old .rotate without the bang works fine, but I want the result to be saved in the original array.
The goal is to have this "Neutral" post option appear last in a list. I can call .rotate on the array in the view, but this seems like unnecessary repetitive work when I should probably just alter the original array, right? Any advice?
Running ruby 2.7.0p0
I have a model model, and am trying to render that with as_json. I am calling the :methods option on it like this:
model.as_json(methods: [:foo, :bar])
The output includes a key-value pair for the method foo, but not for bar. It might be the case that bar is not defined correctly while foo is not, I am not sure.
In such case, would as_json silently ignore the application of the undefined methods? If so, is there a way to be notified by an error? Or, could there be any other reason that particular key-value pairs become silently ignored from the as_json output?
I am using Active Model 4.2.7.
An option is override as_json method in your model. Here you have to check if the methods are available in your model, if the method is not defined, remove it from options and do some notify action. This way the not available methods will be ignored.
An approximation code would be
class Model < ApplicationRecord
def as_json(options = {})
if options.key? :methods
if options[:methods].is_a? Array
methods_copy = options[:methods].clone
methods_copy.each do |opt|
if !self.respond_to? opt
# do notify, log, etc
options[:methods].delete opt
end
end
elsif options[:methods].is_a? Symbol
if !self.respond_to? options[:methods]
# do notify, log, etc
options.delete :methods
end
else
# do some actions here
end
end
super(options)
end
end
Then call as_json as usual
model.as_json(methods: [:foo, :bar])
UPDATE
As I misread the question, here an update based on the answer of #sawa
Older versions of ActiveModel don't notify when some method is not defined when methods options is passed.
Upgrading the above code
def as_json(options = {})
Array(options[:methods]).each do |m|
raise "method #{m} doesn't exists" if !self.respond_to?(m)
end
super(options)
end
Methods included in as_json must be public methods. If bar is a private method, it will not be included.
This situation is a subset of the responds_to?issue mentioned in #sawa's answer, and is probably resolved by the same commit linked in the answer.
In such case, would as_json silently ignore the application of the undefined methods?
Yes in older versions of active model, and no since a certain version. To be more precise with older versions of active model, it is not the case that undefined methods are ignored; what are ignored are methods that do not return a truthy value to respond_to?. In the code at: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/serialization.rb, there used to be a line:
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
where hash is the output of this method. Methods specified by the :methods option were ignored when they did not respond. However, this change altered the corresponding line into:
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
which raises a method missing error when the method is not defined.
If so, is there a way to be notified by an error?
The easiest way is to update the active model to at least later than the commit mentioned above.
Is there a way to hook into the save! with a callback?
I am looking for something like:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
before_save :save_subservices! if bang_save?
private
def save_subservices
#subservices.each(&:save)
end
def save_subservices!
#subservices.each(&:save!)
end
end
Where a save! is cascaded and calls save! on the (faux) association subservices.
Technically you can do this, but I would advise not to use this approach in production because it can change in newer rails. And it is just wrong.
You can inspect call stack of your before callback and check if there is save! method.
class CompositeService < Service
before_save :some_callback
def some_callback
lines = caller.select { |line| line =~ /persistence.rb/ && line =~ /save!/ }
if lines.any?
#subservices.each(&:save!)
else
#subservices.each(&:save)
end
end
end
I wonder: is this extra logic even necessary?
If the save method on each of your #subservices obeys the ActiveRecord save semantics, then you probably will get the correct behavior for free.
In other words, make sure your save methods return true or false for success or failure. Then, the composite code becomes as simple as this:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
private
def save_subservices
#subservices.all?(&:save)
end
end
If any of your sub services fail to save, then the save_subservices callback will return false, which will abort the callback chain. This will cause the wrapping save to return false. And in the case of save!, it will raise an exception.
composite.save
# => false
composite.save!
# => ActiveRecord::RecordNotSaved
Look at ActiveRecord autosave attribute:
http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
I'm trying to run all callback methods manually inside a method. For example, I want to run all "before_destroy" methods inside my model.
Is there a way to manually trigger this? For example, something like:
def some_method
# ...
trigger(:before_destroy)
end
which will then run all methods that I have declared with "before_destroy :...."
Any ideas?
If you're happy to run both :before and :after hooks, you can try run_callbacks.
From the docs:
run_callbacks(kind, &block)
Runs the callbacks for the given event.
Calls the before and around callbacks in the order they were set, yields the block (if given one), and then runs the after callbacks in reverse order.
If the callback chain was halted, returns false. Otherwise returns the result of the block, or true if no block is given.
run_callbacks :save do
save
end
class Foo < ActiveRecord::Base
def destroy_method_1
end
def destroy_method_2
end
before_destroy :destroy_method_1, :destroy_method_2
DESTROY_METHODS = [:destroy_method_1, :destroy_method_2]
def some_method
DESTROY_METHODS.each {|m| send(m) }
end
end