ActsAsInsertOrUpdate

Posted by aaron
on Tuesday, April 22

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 :Person
 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 :Person
     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.

Comments

Leave a response

  1. PWillsApril 23, 2008 @ 10:26 AM
    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. AaronApril 23, 2008 @ 11:17 AM
    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. geshidoApril 24, 2008 @ 02:28 AM
    maybe use github.com for this patch?
  4. Peter VandenberkApril 25, 2008 @ 07:33 AM
    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. dcpattonApril 25, 2008 @ 09:26 AM
    There is also REPLACE. There are differences: http://www.mysqlperformanceblog.com/2007/01/18/insert-on-duplicate-key-update-and-replace-into/
  6. Justin MarneySeptember 26, 2008 @ 11:05 AM
    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
  7. agiledevcoolOctober 24, 2008 @ 12:47 AM
    This plugin would be really helpful. Any sense of when it will become publicly available?