ActsAsInsertOrUpdate

Problem

With high volume Rails applications, entities with unique constraints are expensive and error prone to create/update. ActsAsInsertOrUpdate helps solve that problem (if you’re using MySQL), by leveraging the “INSERT … ON DUPLICATE KEY UPDATE” functionality.

Scenario

Lets say you have a Person, and Entity, and a Rating. Each user can rate each entity only once, and if they re-rate the entity, it should update the value.

class Entity < ActiveRecord::Base
  has_many :ratings
end

class Person < ActiveRecord::Base
 has_many :ratings
end

class Rating < ActiveRecord::Base
 belongs_to  :P erson
 belongs_to :Entity
end  

Here is the table that back’s Rating. Notice the Unique Key constraint on (entity_id, person_id).

CREATE TABLE `ratings` (
  `id` int(11) NOT NULL auto_increment,
  `rating` tinyint(4) default '0',
  `person_id` int(11) default NULL,
  `entity_id` int(11) default NULL,
  `created_at` datetime default NULL,
  `updated_at` datetime default NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `index_ratings_on_entity_id_and_person_id` (`entity_id`,`person_id`),
)

Previously, the logic would be something like:

  • 1) Check if a rating exists for the User + Entity
  • 2) If so, update
  • 3) If not, insert
  • 4) rescue the insert in case there is a unqiue constraint error
  • 5) retrieve the record (and/or update with the new rating)
  • If the table is MyISAM, Steps 1-5 aren’t transactionally safe. If you’re using InnoDB, and experience heavy volumes of traffic, you’re prone to Deadlock’s. This is even more of a concern is the unique entity is shared across multiple users, as seen with a recent client of ours.

    Solution:

    class Rating < ActiveRecord::Base
     belongs_to  :P erson
     belongs_to :Entity
     acts_as_insert_or_update :field_to_update => "rating"
    end  

    Now Steps 1-5 above become, just one. Rating.create(..)

    In the background, ActsAsInsertOrUpdate overwrites the implementation of ActionRecord:Base#create, to leverage an often unsed feature of MySQL called INSERT … ON DUPLICATE KEY UPDATE. As configured above, if a duplicate record is found for the unique constraint, the rating field will be updated with the new value.

    Caution

    This is a brute force hack on ActiveRecord::Base#create. Use at your own risk.

    Code

    Waiting for a rubyforge account. Will post more info soon.

7 Comments

  1. PWills says:

    FWIW, this is called “Upsert” by Oracle and “Merge” by Microsoft. I think both of those verbs are better than the slightly tedious “ActsAsInsertOrUpdate”.

  2. Aaron says:

    Sure… ActsAsUpsert didnt sound as cool though… Its a play on all the crazy ActsAsChicken, ActsAsTaggable, ActsAsCool, etc names used in the Rails community… The name was supposed to be funny..

  3. geshido says:

    maybe use github.com for this patch?

  4. Extremely useful, even with the somewhat dire warning! :-)

    FWIW: we were using MySQL’s "REPLACE INTO…" command to achieve the same net effect:

    http://dev.mysql.com/doc/refman/5.0/en/replace.html

    … but the "INSERT … ON DUPLICATE KEY UPDATE" functionality definitely seems a better way of doing things.

  5. Is this plugin available somewheres? I can’t seem to track down the actual files and I’d lvoe to take a look at how this works.

    Thanks! J

  6. agiledevcool says:

    This plugin would be really helpful. Any sense of when it will become publicly available?