I have a model that won't update properly with update_attributes, but will update using update_column. I'm assuming this is because a callback is interfering. Unfortunately, it's not throwing any errors, so I can't figure out where exactly the problem is coming from.
Is there a way to trace callbacks so I can go through them, one by one, until I find the culprit?
The API documentation shows how you can access the callback chain.
Here's some one liners that you can use in your console that should give you the idea:
# Print before_validate callbacks
Post._validate_callbacks.select { |cb| cb.kind.eql? :before }.each { |cb| puts cb.filter }
# Print after_update callbacks
Post._update_callbacks.select { |cb| cb.kind.eql? :after }.each { |cb| puts cb.filter }
Remember that updates to models will also call save so it's a good idea to trawl through them all.
Check to see that the params you are passing to the update_attributes() method are mass assignable.
They should be defined as :attr_accessible in your rails model otherwise they will be stripped out before saving.
class Widget < ActiveRecord::Base
attr_accessible :name
end
More info here http://guides.rubyonrails.org/security.html
Related
I'm using Rails 5. I have a model that looks like this
class CryptoIndexCurrency < ApplicationRecord
belongs_to :crypto_currency
end
I have a service method where I want to populate this table with records, which I do like so
CryptoIndexCurrency.delete_all
currencies.each do |currency|
cindex_currency = CryptoIndexCurrency.new({:crypto_currency => currency})
cindex_currency.save
end
The problem is the above is not very transactional, in as far as if something happens after the first statement, the "delete_all" will have executed but nothing else will have. What is the proper way to create a transaction here and equally as important, where do I place that code? Would like to know the Rails convention here.
I think you can just do:
CryptoIndexCurrency.transaction do
CryptoIndexCurrency.delete_all
CryptoIndexCurrency.create(currencies.map{ |c| {crypto_currency: c} })
end
If you are using Activerecord you can use the builtin transaction mechanism. Otherwise, one way would be to make sure you validate all your data and only save when everything is valid. Take a look at validates_associate and the like.
That said, if your process is inherently non validatable/nondeterministic (eg. you call external APIs to validate a payment) then the best is to ensure you have some cleaning methods that take care of your failure
If you have deterministic failures:
def new_currencies_valid?(currencies)
currencies.each do
return false if not currency.valid?(:create)
end
true
end
if new_currencies_valid?(new_currencies)
Currency.delete_all # See note
new_currencies.each(&:save)
end
A sidenote : unless you really understand what you are doing, I suggest calling destroy_all which runs callbacks on deletion (such as deleting dependent: :destroy) associations
I have a TreatmentEvent model. Here are the relevant parts:
class TreatmentEvent < ActiveRecord::Base
attr_accessible :taken #boolean
attr_accessible :reported_taken_at #DateTime
end
When I set the taken column, I want to set reported_taken_at if taken is true. So I tried an after_save callback like so:
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
I think update_attribute calls save, so that's causing the stack level too deep error. But using the after_commit callback is causing this to happen, too.
Is there a better way to conditionally update one column when another changes? This answer seems to imply you should be able to call update_attributes in an after_save.
Edit
This also happens when using update_attributes:
def set_reported_taken_at
self.update_attributes(reported_taken_at: Time.now) if self.taken?
end
As a note, stack level too deep generally means an infinite loop
--
In your case, the issue will almost certainly be caused by:
after_commit :set_reported_token_at
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
--
The problem is after_commit is going to try and save the reported_taken_at even if you've just saved a record. So you're going to go over the record again and again and again and again...
Often known as a recursive loop - it's used a lot in native development, but for request (HTTP) based apps, it's bad as it leads to a never-ending processing of your request
Fix
Your fix should be like this:
#model
before_save :set_reported_token_at
def set_reported_taken_at
self.reported_taken_at = Time.now if taken? #-> assuming you have a "taken" method
end
Can't you use a before_save? You can see if the other field value has changed and if so update this field. That way you just have one DB call.
I've been going through the rails source for a while now, and I don't think there's a better way of getting the list of all callbacks other than: ActiveRecord::Callbacks::CALLBACKS – which is a constant list.
Meaning if you're using a gem like devise_invitable that adds a new callback called :invitation_accepted with the score :after and :before then ActiveRecord::Callbacks::CALLBACKS will not work.
Do you know of an easy fix, other than opening up rails modules and making sure there's an internal list of call-backs per model class?
You can call Model._save_callbacks to get a list of all callbacks on save.
You can then filter it down to what kind you need e.g. :before or :after like this:
Model._save_callbacks.select {|cb| cb.kind == :before}
Works the same for Model._destroy_callbacks etc.
The docs say "There are nineteen callbacks in total"... but they don't seem to say what all of those 19 actually are!
For those who Googled "list of all ActiveRecord callbacks" like I did, here's the list (found by using ActiveRecord::Callbacks::CALLBACKS as described in the question):
:after_initialize
:after_find
:after_touch
:before_validation
:after_validation
:before_save
:around_save
:after_save
:before_create
:around_create
:after_create
:before_update
:around_update
:after_update
:before_destroy
:around_destroy
:after_destroy
:after_commit
:after_rollback
Note that if you simply want to trigger callbacks, you can use the #run_callbacks(kind) method:
o = Order.find 123 # Created with SQL
o.run_callbacks(:create)
o.run_callbacks(:save)
o.run_callbacks(:commit)
If you're working in a Rails version prior to the ._save_callbacks method, you can use the following:
# list of callback_chain methods that return a CallbackChain
Model.methods.select { |m| m.to_s.include? "callback" }.sort
# get all methods in specific call back chain, like after_save
Model.after_save_callback_chain.collect(&:method)
I am going to propose most universal solution.
It works even when gems are declaring custom callbacks e.g. paranoia and after_real_destroy
To list all callbacks
Model.methods.select { |m| m.to_s.include? "callback" }.sort
Then you can get given callbacks if you type method name e.g.
Model._update_callbacks
Model._real_destroy_callbacks
Model._destroy_callbacks
If you list all callbacks, then you can find callback you need by checking #filter instance variable e.g.
require 'pp'
Activity._destroy_callbacks.each_with_index { |clbk,index| puts "#{index}-------\n#{clbk.pretty_inspect}" } ; nil
# [...]
#<ActiveSupport::Callbacks::Callback:0x00007ff14ee7a968
#chain_config=
{:scope=>[:kind, :name],
:terminator=>
#<Proc:0x00007ff13fb825f8#/Users/mypc/.rbenv/versions/2.3.7/lib/ruby/gems/2.3.0/gems/activemodel-4.1.16/lib/active_model/callbacks.rb:103 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
#filter=
#<Proc:0x00007ff14ee7ac10#/Users/mypc/.rbenv/versions/2.3.7/lib/ruby/gems/2.3.0/gems/activerecord-4.1.16/lib/active_record/associations/builder/association.rb:135 (lambda)>,
#if=[],
#key=70337193825800,
#kind=:before,
#name=:destroy,
#unless=[]>
4-------
#<ActiveSupport::Callbacks::Callback:0x00007ff14ee3a228
#chain_config=
{:scope=>[:kind, :name],
:terminator=>
#<Proc:0x00007ff13fb825f8#/Users/mypc/.rbenv/versions/2.3.7/lib/ruby/gems/2.3.0/gems/activemodel-4.1.16/lib/active_model/callbacks.rb:103 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
#filter=:audit_destroy,
#if=[],
#key=:audit_destroy,
#kind=:before,
#name=:destroy,
#unless=[]>
5-------
For after_commit callbacks, call Model._commit_callbacks.
Be mindful, however, that there's a known bug in Rails (still present in Rails 5.2.2) that after_commit callbacks are not run in the order they are declared in the model, even tough they appear in the correct order in that _commit_callbacks call.
More info: Execution order of multiple after_commit callbacks (Rails) and https://github.com/rails/rails/issues/20911
I have a "event" model that has many "invitations". Invitations are setup through checkboxes on the event form. When an event is updated, I wanted to compare the invitations before the update, to the invitations after the update. I want to do this as part of the validation for the event.
My problem is that I can't seem to access the old invitations in any model callback or validation. The transaction has already began at this point and since invitations are not an attribute of the event model, I can't use _was to get the old values.
I thought about trying to use a "after_initialize" callback to store this myself. These callbacks don't seem to respect the ":on" option though so I can't do this only :on :update. I don't want to run this every time a object is initialized.
Is there a better approach to this problem?
Here is the code in my update controller:
def update
params[:event][:invited_user_ids] ||= []
if #event.update_attributes(params[:event])
redirect_to #event
else
render action: "edit"
end
end
My primary goal is to make it so you can add users to an event, but you can't not remove users. I want to validate that the posted invited_user_ids contains all the users that currently are invited.
--Update
As a temporary solution I made use for the :before_remove option on the :has_many association. I set it such that it throws an ActiveRecord::RollBack exception which prevents users from being uninvited. Not exactly what I want because I can't display a validation error but it does prevent it.
Thank you,
Corsen
Could you use ActiveModel::Dirty? Something like this:
def Event < ActiveRecord::Base
validates :no_invitees_removed
def no_invitees_removed
if invitees.changed? && (invitees - invitees_was).present?
# ... add an error or re-add the missing invitees
end
end
end
Edit: I didn't notice that the OP already discounted ActiveModel::Dirty since it doesn't work on associations. My bad.
Another possibility is overriding the invited_user_ids= method to append the existing user IDs to the given array:
class Event < ActiveRecord::Base
# ...
def invited_user_ids_with_guard=(ids)
self.invited_user_ids_without_guard = self.invited_user_ids.concat(ids).uniq
end
alias_method_chain :invited_user_ids=, :guard
end
This should still work for you since update_attributes ultimately calls the individual attribute= methods.
Edit: #corsen asked in a comment why I used alias_method_chain instead of super in this example.
Calling super only works when you're overriding a method that's defined further up the inheritance chain. Mixing in a module or inheriting from another class provides a means to do this. That module or class doesn't directly "add" methods to the deriving class. Instead, it inserts itself in that class's inheritance chain. Then you can redefine methods in the deriving class without destroying the original definition of the methods (because they're still in the superclass/module).
In this case, invited_user_ids is not defined on any ancestor of Event. It's defined through metaprogramming directly on the Event class as a part of ActiveRecord. Calling super within invited_user_ids will result in a NoMethodError because it has no superclass definition, and redefining the method loses its original definition. So alias_method_chain is really the simplest way to acheive super-like behavior in this situation.
Sometimes alias_method_chain is overkill and pollutes your namespace and makes it hard to follow a stack trace. But sometimes it's the best way to change the behavior of a method without losing the original behavior. You just need to understand the difference in order to know which is appropriate.
Is there a way to skip callbacks and validation by doing something along these lines in Rails 3?
Object.save(:validate => false, :skip_callbacks => true)
Thanks!
Object.save(:validate => false)
works as you would expect. So far as I know you cannot turn off callbacks (unless you return false from a before_ callback, but that then aborts the transaction).
I encountered something like that before and I used this:
Model.send(:create_without_callbacks)
Model.send(:update_without_callbacks)
Skipping callbacks is a bit tricky. Some plugins and adapters add their own "essential" callbacks (acts_as_nested_set, oracle_enhanced_adapter as examples).
You could use the skip_callback and set_callback methods in checking which ones you'd be able to skip.
Some custom class methods could help:
def skip_all_callbacks(klass)
[:validation, :save, :create, :commit].each do |name|
klass.send("_#{name}_callbacks").each do |_callback|
# HACK - the oracle_enhanced_adapter write LOBs through an after_save callback (:enhanced_write_lobs)
if (_callback.filter != :enhanced_write_lobs)
klass.skip_callback(name, _callback.kind, _callback.filter)
end
end
end
end
def set_all_callbacks(klass)
[:validation, :save, :create, :commit].each do |name|
klass.send("_#{name}_callbacks").each do |_callback|
# HACK - the oracle_enhanced_adapter write LOBs through an after_save callback (:enhanced_write_lobs)
if (_callback.filter != :enhanced_write_lobs)
klass.set_callback(name, _callback.kind, _callback.filter)
end
end
end
end
For skipping callbacks in Rails 3, you can use update_all for your given purpose.
Source: update_all
The full list for skipping callbacks are here:
decrement
decrement_counter
delete
delete_all
find_by_sql
increment
increment_counter
toggle
touch
update_column
update_all
update_counters
Source: Skipping Callbacks
If you are trying to update the record skipping all callbacks and validations you could use update_columns passing the attributes hash. This method will update the columns direct on database.
For example:
object.update_columns(name: 'William')
If you want to create a new object, unfortunately I think there is no method to skip both validations and callbacks. save(:validate => false) works for validations. For callbacks you could use skip_callback but be careful, your code probably will not be thread-safe.
http://guides.rubyonrails.org/active_record_validations_callbacks.html details a small list of methods that avoid callbacks and validations - none of these include 'save' though.
However, the point of validations and callbacks is to enforce business logic. If you're avoiding them - you should ask yourself why.
See also: How can I avoid running ActiveRecord callbacks?