210

There is a handy dynamic attribute in active-record called find_or_create_by:

Model.find_or_create_by_<attribute>(:<attribute> => "")

But what if I need to find_or_create by more than one attribute?

Say I have a model to handle a M:M relationship between Group and Member called GroupMember. I could have many instances where member_id = 4, but I don't ever want more than once instance where member_id = 4 and group_id = 7. I'm trying to figure out if it's possible to do something like this:

GroupMember.find_or_create(:member_id => 4, :group_id => 7)

I realize there may be better ways to handle this, but I like the convenience of the idea of find_or_create.

Marlin Pierce
  • 9,931
  • 4
  • 30
  • 52
tybro0103
  • 48,327
  • 33
  • 144
  • 170

5 Answers5

480

Multiple attributes can be connected with an and:

GroupMember.find_or_create_by_member_id_and_group_id(4, 7)

(use find_or_initialize_by if you don't want to save the record right away)

Edit: The above method is deprecated in Rails 4. The new way to do it will be:

GroupMember.where(:member_id => 4, :group_id => 7).first_or_create

and

GroupMember.where(:member_id => 4, :group_id => 7).first_or_initialize

Edit 2: Not all of these were factored out of rails just the attribute specific ones.

https://github.com/rails/rails/blob/4-2-stable/guides/source/active_record_querying.md

Example

GroupMember.find_or_create_by_member_id_and_group_id(4, 7)

became

GroupMember.find_or_create_by(member_id: 4, group_id: 7)
Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
x1a4
  • 19,417
  • 5
  • 40
  • 40
  • You might also like https://github.com/seamusabshere/upsert - the first argument is a hash of attributes that is used to find or create a record – Seamus Abshere May 28 '13 at 19:05
  • 1
    It is helpful to note that initialize calls the create() instead of new() that might be called in case of first_or_create – Sumit Bisht Aug 03 '13 at 10:44
  • Can some one share a gist example of this usage? I'm not familiar with its placement and calling. – 6ft Dan Aug 01 '14 at 00:41
  • I reverted the removal of Rails 3 code, since lots of people are not using Rails 4 yet. – Kyle Heironimus Sep 05 '14 at 21:12
  • Just to add: find_or_create_by_*() functions have been deprecated in Rails 4, however find_or_create_by() is NOT. It is okay to use find_or_create_by(). Check the commit where this was added https://github.com/rails/rails/commit/eb72e62c3042c0df989d951b1d12291395ebdb8e and the official Rails 4.2 documentation for the usage: http://guides.rubyonrails.org/v4.2.0/active_record_querying.html#find-or-create-by – CodeExpress Jan 06 '15 at 23:49
  • Does the `find_or_create_by` is really deprecated in Rails 4? I just found a resource http://apidock.com/rails/v4.2.1/ActiveRecord/Relation/find_or_create_by under version of Rails 4.2.1 – AllenC Sep 22 '15 at 15:08
36

In Rails 4 you could do:

GroupMember.find_or_create_by(member_id: 4, group_id: 7)

And use where is different:

GroupMember.where(member_id: 4, group_id: 7).first_or_create

This will call create on GroupMember.where(member_id: 4, group_id: 7):

GroupMember.where(member_id: 4, group_id: 7).create

On the contrary, the find_or_create_by(member_id: 4, group_id: 7) will call create on GroupMember:

GroupMember.create(member_id: 4, group_id: 7)

Please see this relevant commit on rails/rails.

Juanito Fatas
  • 9,419
  • 9
  • 46
  • 70
35

For anyone else who stumbles across this thread but needs to find or create an object with attributes that might change depending on the circumstances, add the following method to your model:

# Return the first object which matches the attributes hash
# - or -
# Create new object with the given attributes
#
def self.find_or_create(attributes)
  Model.where(attributes).first || Model.create(attributes)
end

Optimization tip: regardless of which solution you choose, consider adding indexes for the attributes you are querying most frequently.

Marco
  • 4,345
  • 6
  • 43
  • 77
  • 8
    One thing to note is that this won't handle attributes that cannot be mass-assigned, while `find_or_create_by` will. – x1a4 Jul 08 '12 at 05:36
  • 1
    Marco, try `Model.where(attributes).instance_eval{|q| q.first || q.create}`. – hiroshi Dec 01 '12 at 14:13
  • 3
    ```find_or_create``` also has the benefit of using a transaction, where ```where….first || create``` introduces a race condition – mrm Jun 01 '13 at 19:04
  • I would say `def self.find_or_create(attributes) self.where(attributes).first || self.create(attributes) end` so that you don't need to repeat `Model` – Augustin Riedinger Jul 03 '13 at 16:01
  • @AugustinRiedinger You don't need `self` inside the method body either (since you're looking to remove words!). – David Tuite Jul 03 '14 at 08:33
  • @mm - Using a transaction here does not prevent a race condition, unless you have configured a higher-than-normal (and less performant) isolation level on your RDBMS. Note that http://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_create_by specifically mentions that find_or_create_by is vulnerable to a race condition. – antinome Aug 31 '14 at 23:40
18

By passing a block to find_or_create, you can pass additional parameters that will be added to the object if it is created new. This is useful if you are validating the presence of a field that you aren't searching by.

Assuming:

class GroupMember < ActiveRecord::Base
    validates_presence_of :name
end

then

GroupMember.where(:member_id => 4, :group_id => 7).first_or_create { |gm| gm.name = "John Doe" }

will create a new GroupMember with the name "John Doe" if it doesn't find one with member_id 4 and group_id 7

Daniel Murphy
  • 291
  • 2
  • 4
6

You can do:

User.find_or_create_by(first_name: 'Penélope', last_name: 'Lopez')
User.where(first_name: 'Penélope', last_name: 'Lopez').first_or_create

Or to just initialize:

User.find_or_initialize_by(first_name: 'Penélope', last_name: 'Lopez')
User.where(first_name: 'Penélope', last_name: 'Lopez').first_or_initialize
Dorian
  • 22,759
  • 8
  • 120
  • 116
  • http://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_create_by – Duke Feb 20 '16 at 00:32