4

I'm looking for a clean way to create a record with a set of attributes if the record does not exist and - if the record do exist - to update its attributes. I love the syntax of the block in the find_or_create_by_id call. Here's my code:

@categories = Highrise::DealCategory.find(:all)

@categories.each do |category|
  puts "Category: #{category.name}"

  Category.find_or_create_by_id(category.id) do |c|
    c.name = category.name
  end
end

The problem here is that if the record exists but the name has changed, it is not being updated.

Looking for a clean solution to this problem...

fl00r
  • 82,987
  • 33
  • 217
  • 237
Jonas Grau
  • 43
  • 1
  • 3
  • http://stackoverflow.com/questions/5578625/find-or-create-by-in-rails-3-and-updating-for-creating-records/5580108#5580108 – fl00r Apr 07 '11 at 14:17

7 Answers7

4

You can write your own method:

class ActiveRecord::Base
  def self.find_by_id_or_create(id, &block)
    obj = self.find_by_id( id ) || self.new
    yield obj
    obj.save
  end
end

usage

 Category.find_by_id_or_create(10) do |c|
   c.name = "My new name"
 end

Of course, in this way you should extend method missing method and implement this method in the same way as others find_by_something methods. But for being short this will be enough.

fl00r
  • 82,987
  • 33
  • 217
  • 237
1

I have coded this finders that can be used for different scenarios.

The most important thing is, that it removes the parameter :id on create and update.

Creating a model with :id can cause problems with MySql or PostgreSQL because Rails uses the auto sequence number of the database. If you create new model instances with an :id you can get a UniqueViolation: ERROR: duplicate key value violates unique constraint.

# config/initializers/model_finders.rb
class ActiveRecord::Base

  def self.find_by_or_create(attributes, &block)
    self.find_by(attributes) || self.create(attributes.except(:id), &block)
  end


  def self.find_by_or_create!(attributes, &block)
    self.find_by(attributes) || self.create!(attributes.except(:id), &block)
  end


  def self.find_or_create_update_by(attributes, &block)
    self.find_by(attributes).try(:update, attributes.except(:id), &block) || self.create(attributes.except(:id), &block)
  end


  def self.find_or_create_update_by!(attributes, &block)
    self.find_by(attributes).try(:update!, attributes.except(:id), &block) || self.create!(attributes.except(:id), &block)
  end


  def self.find_by_or_initialize(attributes, &block)
    self.find_by(attributes) || new(attributes.except(:id), &block)
  end

end
phlegx
  • 2,618
  • 3
  • 35
  • 39
0

I liked fl00r's answer. But why do we have to save the object every time? We can check if its already there in records or else save it

def self.find_or_create_by_id(id, &block)
    obj = self.find_by_id(id) 
    unless obj
      obj = self.create(id: id)
    end
    obj
end
Aditya
  • 144
  • 7
  • `self.create(id: id)` is not a good idea. This can cause a duplicate key unique constraint error `ERROR: duplicate key value violates unique constraint`. Please use `self.new` instead. – phlegx Feb 26 '15 at 11:06
0

I have been using this patten for seeds:

Category.find_or_initialize_by(id: category.id).update |c|
  c.name = category.name
end

It works the same as Dale Wijnand's and Teoulas answers (only saves the instance once) but uses a block like in your question.

complistic
  • 2,610
  • 1
  • 29
  • 37
  • I don't know if this used to work, but in recent versions of Rails the update method doesn't take a block as an argument, so this won't work – wnm Sep 03 '20 at 07:12
0

I did this yesterday, wishing there was a way to do it in a one-liner.

Ended up going with (using your code):

c = Category.find_or_initialize_by_id(category.id)
c.name = category.name
c.save

Perhaps there is a nicer way, but this is what I used.

[Edit: use initialize instead of create to avoid hitting the DB twice)

Dale Wijnand
  • 6,054
  • 5
  • 28
  • 55
0

Try this:

c = Category.find_or_initialize_by_id(category.id)
c.name = category.name
c.save!

This way you only save the instance once, instead of twice, if you called find_or_create_by_id (assuming it's a new record).

Teoulas
  • 2,943
  • 22
  • 27
0

I think the simplest way is using Ruby's tap method, like this:

Category.find_or_initialize_by(id: category.id).tap do |c|
  c.name = category.name
  c.save
end

Also, I changed find_or_create_by_id to find_or_initialize_by, otherwise it will hit the database twice.

find_or_initialize_by finds or intitializes a record, then returns it. We then tap into it, make our updates and save it to the database.

wnm
  • 1,349
  • 13
  • 12