Technology

SAVE AN OBJECT SKIPPING CALLBACKS IN RAILS 3 APPLICATION

Many times, we need to skip validation as well as callbacks while saving the object, due to the requirement of a Ruby on Rails application. In Rails 3.x, to skip validation we can use (:validate => false) and to skip callbacks we tend to use ActiveSupport::Callbacks::ClassMethods#skip_callback (http://apidock.com/rails/ActiveSupport/Callbacks/ClassMethods/skip_callback).

ActiveSupport::Callbacks::ClassMethods#skip_callback

This method can get us into trouble, as it is not thread-safe. So to skip callbacks, there is no option as (:callbacks => false) or (:skip_callbacks => true) . In order to achieve similar functionality of skipping callbacks following gems/code snippets are widely referred:

  1. gem ‘skip_activerecord_callbacks’ (https://github.com/dball/skip_activerecord_callbacks)
  2. Blog:Disabling ActiveModel callbacks (http://jeffkreeftmeijer.com/2010/disabling-activemodel-callbacks/)
  3. Activate/deactivate callbacks snippet (https://gist.github.com/Tei/670283)
  4. Providing (:callbacks => false) option (https://github.com/rails/rails/pull/4885, Commit: https://github.com/patrick99e99/rails/commit/d7787d7220a8935d5ee3765824ef0c138b51a4f5)
  5. gem ‘sneaky-save’ (https://github.com/partyearth/sneaky-save)

Since I have been working with Rails for quite some time now, and been observing this situation quite frequently. Based on my observations, I tend to conclude the following on above references and methods,

  1. gem ‘skip_activerecord_callbacks’ : Class Methods are provided which functions similar to *_without_callbacks methods(form Rails 2.x)
  2. Blog:Disabling ActiveModel callbacks : Method given sets skipped callbacks after the necessary work is done but again this is using skip_callback method
  3. Activate/deactivate callbacks snippet : These are also class methods which are removing callbacks definitions
  4. Providing (:callbacks => false) option : This a decent option and also provides same format as (:validate => false) but due to a large number of changes in core rails part it is very difficult to use in application.
  5. gem ‘sneaky-save’ : This is quite a simple solution. It generates raw SQL query to save the record. Hence, no callbacks or validations are triggered. Out of above mentioned references gem ‘sneaky-save’ found to be meeting the requirement of the Ruby on Rails Application. This gem skips all validation as well as all callbacks.

INSTALLATION

$ gem install sneaky-save

Now add it to your Gemfile with:

gem 'sneaky-save'

Run the bundle command to install it.

Once you install sneaky-save and add it to your Gemfile, you are ready to use it in your application.

USAGE

# Create

@user = User.new
@user.first_name = 'UserFirstName'
@user.last_name = 'UserLastName'
@user.sneaky_save

# Update

@user = User.find(123) # say record id = 123 
@user.first_name = 'NewUserFirstName'
@user.last_name = 'NewUserLastName'
@user.sneaky_save

LOGIC

All the logic is in SneakySave module (https://github.com/partyearth/sneaky-save/blob/master/lib/sneaky-save.rb)

def sneaky_save
  begin
    # If it is a new record then create a new record else update changed attributes
    new_record? ? sneaky_create : sneaky_update
  rescue ActiveRecord::StatementInvalid
    false
  end
end

We can use sneaky_save on new or existing object. Depending on the object, if it is a new object then to create object sneaky_create is used to generate SQL insert query. If the object is an existing object then SQL update query is generated.

# File https://github.com/partyearth/sneaky-save/blob/master/lib/sneaky-save.rb, line 26

def sneaky_create

  if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) 
    self.id = connection.next_sequence_value(self.class.sequence_name) 
  end

  attributes_values = send :arel_attributes_values

  # Remove the id field for databases like Postgres which will raise an error on id being NULL 
  if self.id.nil? && !connection.prefetch_primary_key?(self.class.table_name) 
    attributes_values.reject! { |key,_| key.name == 'id' }
  end

  # Uses ActiveRecord's insert() method to generate insert SQL query 
  new_id = if attributes_values.empty?
    self.class.unscoped.insert connection.empty_insert_statement_value 
  else
    self.class.unscoped.insert attributes_values
  end

  @new_record = false
  !!(self.id ||= new_id)

end

Here the main functioning is done by Model.insert(). It is ActiveRecord::ConnectionAdapters::DatabaseStatements#insert method that generates SQL insert query for creating a new record.

# File https://github.com/partyearth/sneaky-save/blob/master/lib/sneaky-save.rb, line 50

def sneaky_update
  # Handle no changes.
  return true if changes.empty?
  # Here we have changes --> save them.
  pk = self.class.primary_key
  original_id = changed_attributes.has_key?(pk) ? changes[pk].first : send(pk) 
  !self.class.update_all(attributes, pk => original_id).zero?
end

Here Model.update_all() is called for generating SQL update query and update the given record. These methods are called from ActiveRecord::Base#update_all

HOW IT WORKS?

‘sneaky_save’ method internally calls ‘sneaky_create’ and ‘sneaky_update’ as per object state. In ‘sneaky_create’ method, INSERT SQL query is generated and executed. Direct INSERT SQL is executed without involving Model, thus no validation or callback is triggered. In ‘sneaky_update’ method, UPDATE SQL query are generated and executed to manipulate database. Here it does not instantiate Model object, hence does not trigger any validation or callback.

ADVANTAGE

Following existing methods which are available for an update but with their own limitations.

update_attributes : Validation is invoked. Callbacks are invoked.

update_attribute : Validation is skipped. Callbacks are invoked.

update_column : Validation is skipped. Callbacks are skipped.

update_columns : Validation is skipped. Callbacks are skipped. (available in Rails4)

update_all : Validation is skipped. Callbacks are skipped.

sneaky_save: sneaky-save is easy to use and have clean syntax. It can be called directly on the object without passing any parameters to it.

DRAWBACKS: Does not reload updated record by default. Does not save associated collections. Saves only belongs_to relations. All the callbacks are skipped, does not have any option to trigger/skip selected callbacks

These are mostly my observations so pros and cons may not be limited to what I have posted above 🙂 . I am willing to learn the feedback of other community guys, ————————-

Leave a Comment

Your email address will not be published. Required fields are marked *

*