views:

153

answers:

4

MySQL has a very nice option for INSERT statement, which is particularly helpful for join tables without the id column. It inserts a record, but, instead of throwing an error if its key clashed with the existing one, that record is updated. Here's an example:

INSERT INTO table (key1,key2,data) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE data=3;

How to achieve the same with Active Record? The resultant code would then look like this:

class Model < ActiveRecord::Base
  belongs_to :key1
  belongs_to :key2
end

record = Model.new
record.key1 = key1
record.key2 = key2
record.data = 'new data'
record.WHAT?    #Inserts or updates `data` for the existing record
A: 

First of that's a MySQL-specific solution, as far as I know that syntax doesn't work with any other SQL-servers. Usually called Upsert in discussions.

I'd go with making a new method called save_or_update and then if the save fails, with a duplicate key exception, load and update_params.

ba
Your solution is not very performant. When `save` fails, it also may roll back a transaction of notable size, and the data have to be loaded again. As for MySQL, sorry, fixed the question.
Pavel Shved
hm, maybe swap it around then. Load an object like, `@user = User.find_by_login('ba') rescue User.new` `@user.update_params(params[:user])`Not tested code, I don't remember whether `find` returns nil or an exception, but this is how I add new users in my ActiveDirectory integration for work. I don't know of any way around doing at least a `SELECT` to find if there is a duplicate.
ba
`find` by itself will return `AR::RecordNotFound`, whereas `find_by_*` will return `nil`.
theIV
+2  A: 

You can do the following:

record = Model.find_or_create_by_key1_and_key2(:key1 => key1, :key2 => key2)
record.update_attribute(:data, "new data")

EDIT

zaius idea looks better, you should use find_or_initialize_by to avoid multiple saves, as he said :]

j.
Ooh, nice! I found documentation at [DynamicFinderMatch#new](http://railsapi.com/doc/rails-v2.3.5/classes/ActiveRecord/DynamicFinderMatch.html#M001202), just click "Show Source".
ba
Just a comment: this advice appears to be more profound than it might seem due to the way it's presented...
Pavel Shved
I didn't understand your comment...
j.
-1 as it involves two writing SQL statements being emitted on creation.
hurikhan77
+2  A: 

The answer by j is the right idea, but you probably want to go with find_or_initialize_by to avoid multiple saves on the object. E.g.

record = Model.find_or_initialize_by_key1_and_key2 new
record.data = 'new data'
record.save

By the sounds of it, you also want a validation to check that the model doesn't allow duplicate joins too:

class Model < ActiveRecord::Base
  belongs_to :key1
  belongs_to :key2
  validates_uniqueness_of :key1, :scope => :key2
end
zaius
+1  A: 

You may want to take a look at composite primary keys. It's a plugin for ActiveRecord.

hurikhan77