Permalink
Browse files

Merge pull request #2757 from andmej/first_or_create_pull_request

Add first_or_create family of methods to Active Record
  • Loading branch information...
2 parents 04baa4b + 7231788 commit cbf1dc78429bcceb9e90a3ff3159eafd8af5c711 @jonleighton jonleighton committed Sep 8, 2011
View
@@ -4,6 +4,12 @@ Wed Sep 7 15:25:02 2011 Aaron Patterson <aaron@tenderlovemaking.com>
keys are per process id.
* lib/active_record/connection_adapters/sqlite_adapter.rb: ditto
+* Add first_or_create, first_or_create!, first_or_build and first_or_new methods to Active Record. This is a better approach over the old find_or_create_by dynamic methods because it's clearer which arguments are used to find the record and which are used to create it:
+
+ User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson", :hot => true)
+
+ [Andrés Mejía]
+
* Support bulk change_table in mysql2 adapter, as well as the mysql one. [Jon Leighton]
* If multiple parameters are sent representing a date, and some are blank, the
@@ -442,6 +442,7 @@ class Base
class << self # Class methods
delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
+ delegate :first_or_create, :first_or_create!, :first_or_new, :first_or_build, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
@@ -94,6 +94,49 @@ def create!(*args, &block)
scoping { @klass.create!(*args, &block) }
end
+ # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
+ #
+ # Expects arguments in the same format as <tt>Base.create</tt>.
+ #
+ # ==== Examples
+ # # Find the first user named Penélope or create a new one.
+ # User.where(:first_name => 'Penélope').first_or_create
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ #
+ # # Find the first user named Penélope or create a new one.
+ # # We already have one so the existing record will be returned.
+ # User.where(:first_name => 'Penélope').first_or_create
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ #
+ # # Find the first user named Scarlett or create a new one with a particular last name.
+ # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ #
+ # # Find the first user named Scarlett or create a new one with a different last name.
+ # # We already have one so the existing record will be returned.
+ # User.where(:first_name => 'Scarlett').first_or_create do |user|
+ # user.last_name = "O'Hara"
+ # end
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ def first_or_create(attributes = nil, options = {}, &block)
+ first || create(attributes, options, &block)
+ end
+
+ # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ #
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
+ def first_or_create!(attributes = nil, options = {}, &block)
+ first || create!(attributes, options, &block)
+ end
+
+ # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
+ #
+ # Expects arguments in the same format as <tt>Base.new</tt>.
+ def first_or_new(attributes = nil, options = {}, &block)
@josevalim

josevalim Sep 8, 2011

Contributor

Shouldn't this be called first_or_initialize in order to be consistent with find_or_initialize_by_name ? I would prefer that than first_or_new or first_or_build (which makes you think they are different but they are not).

@jonleighton

jonleighton Sep 8, 2011

Member

Personally I think that find_or_intiailize_by_... is badly named, not the other way around. The class-method for creating a new object is new, not initialize. build was included as an alias to be analogous to associations (which again, do not have an initialize method).

@jpemberthy

jpemberthy Sep 8, 2011

+1, in my opinion first_or_initialize is syntactically better than first_or_new.

@tenderlove

tenderlove Sep 8, 2011

Owner

👎 on first_or_initialize. new calls initialize. We are calling new.

The method name should describe what the method actually does. This method does not call first, then initialize. The method calls first then new.

Rather than _by, call it first_or_new_with if you must.

@josevalim

josevalim Sep 8, 2011

Contributor

My main argument is consistency. At least, I would then remove first_or_new from the API, the alias does not add anything.

@fxn

fxn Sep 8, 2011

Owner

I am with José.

On one hand you have consistency with existing API, and find_or_initialize has been around since the invention of fire.

On the other hand I don't think we need to assume "initialize" means a method name there. I've always interpreted "find_or_initialize" as "find or initialize an object for me with these attributes", in an informal sense of the word "initialize".

Also, in my opinion it reads better.

@jonleighton

jonleighton Sep 13, 2011

Member

I changed it because me and @tenderlove don't care very much, and I even sort of agree with you (omg!): 1187011

+ first || new(attributes, options, &block)
+ end
+ alias :first_or_build :first_or_new
+
def respond_to?(method, include_private = false)
arel.respond_to?(method, include_private) ||
Array.method_defined?(method) ||
@@ -278,6 +278,37 @@ def test_create_after_initialize_with_block
assert_equal(true, cb.frickinawesome)
end
+ def test_first_or_create
+ parrot = Bird.first_or_create(:color => 'green', :name => 'parrot')
+ assert parrot.persisted?
+ the_same_parrot = Bird.first_or_create(:color => 'yellow', :name => 'macaw')
+ assert_equal parrot, the_same_parrot
+ end
+
+ def test_first_or_create_bang
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.first_or_create! }
+ parrot = Bird.first_or_create!(:color => 'green', :name => 'parrot')
+ assert parrot.persisted?
+ the_same_parrot = Bird.first_or_create!(:color => 'yellow', :name => 'macaw')
+ assert_equal parrot, the_same_parrot
+ end
+
+ def test_first_or_new
+ parrot = Bird.first_or_new(:color => 'green', :name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.new_record?
+ assert parrot.valid?
+ end
+
+ def test_first_or_build
+ parrot = Bird.first_or_build(:color => 'green', :name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.new_record?
+ assert parrot.valid?
+ end
+
def test_load
topics = Topic.find(:all, :order => 'id')
assert_equal(4, topics.size)
@@ -863,6 +863,134 @@ def test_create_bang
assert_equal 'hen', hen.name
end
+ def test_first_or_create
+ parrot = Bird.where(:color => 'green').first_or_create(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+
+ same_parrot = Bird.where(:color => 'green').first_or_create(:name => 'parakeet')
+ assert_kind_of Bird, same_parrot
+ assert same_parrot.persisted?
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_with_no_parameters
+ parrot = Bird.where(:color => 'green').first_or_create
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_create_with_block
+ parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+
+ same_parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parakeet' }
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_with_array
+ several_green_birds = Bird.where(:color => 'green').first_or_create([{:name => 'parrot'}, {:name => 'parakeet'}])
+ assert_kind_of Array, several_green_birds
+ several_green_birds.each { |bird| assert bird.persisted? }
+
+ same_parrot = Bird.where(:color => 'green').first_or_create([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ assert_kind_of Bird, same_parrot
+ assert_equal several_green_birds.first, same_parrot
+ end
+
+ def test_first_or_create_bang_with_valid_options
+ parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+
+ same_parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parakeet')
+ assert_kind_of Bird, same_parrot
+ assert same_parrot.persisted?
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_bang_with_invalid_options
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!(:pirate_id => 1) }
+ end
+
+ def test_first_or_create_bang_with_no_parameters
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create! }
+ end
+
+ def test_first_or_create_bang_with_valid_block
+ parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert parrot.persisted?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+
+ same_parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parakeet' }
+ assert_equal parrot, same_parrot
+ end
+
+ def test_first_or_create_bang_with_invalid_block
+ assert_raise(ActiveRecord::RecordInvalid) do
+ parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.pirate_id = 1 }
+ end
+ end
+
+ def test_first_or_create_with_valid_array
+ several_green_birds = Bird.where(:color => 'green').first_or_create!([{:name => 'parrot'}, {:name => 'parakeet'}])
+ assert_kind_of Array, several_green_birds
+ several_green_birds.each { |bird| assert bird.persisted? }
+
+ same_parrot = Bird.where(:color => 'green').first_or_create!([{:name => 'hummingbird'}, {:name => 'macaw'}])
+ assert_kind_of Bird, same_parrot
+ assert_equal several_green_birds.first, same_parrot
+ end
+
+ def test_first_or_create_with_invalid_array
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!([ {:name => 'parrot'}, {:pirate_id => 1} ]) }
+ end
+
+ def test_first_or_new
+ parrot = Bird.where(:color => 'green').first_or_new(:name => 'parrot')
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'parrot', parrot.name
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_new_with_no_parameters
+ parrot = Bird.where(:color => 'green').first_or_new
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert !parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'green', parrot.color
+ end
+
+ def test_first_or_new_with_block
+ parrot = Bird.where(:color => 'green').first_or_new { |bird| bird.name = 'parrot' }
+ assert_kind_of Bird, parrot
+ assert !parrot.persisted?
+ assert parrot.valid?
+ assert parrot.new_record?
+ assert_equal 'green', parrot.color
+ assert_equal 'parrot', parrot.name
+ end
+
+ def test_first_or_build_is_alias_for_first_or_new
+ birds = Bird.scoped
+ assert birds.respond_to?(:first_or_build)
+ assert_equal birds.method(:first_or_new), birds.method(:first_or_build)
+ end
+
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name
@@ -1018,23 +1018,84 @@ If you want to find both by name and locked, you can chain these finders togethe
WARNING: Up to and including Rails 3.1, when the number of arguments passed to a dynamic finder method is lesser than the number of fields, say <tt>Client.find_by_name_and_locked("Ryan")</tt>, the behavior is to pass +nil+ as the missing argument. This is *unintentional* and this behavior will be changed in Rails 3.2 to throw an +ArgumentError+.
-There's another set of dynamic finders that let you find or create/initialize objects if they aren't found. These work in a similar fashion to the other finders and can be used like +find_or_create_by_first_name(params[:first_name])+. Using this will first perform a find and then create if the find returns +nil+. The SQL looks like this for +Client.find_or_create_by_first_name("Ryan")+:
+h3. Find or build a new object
+
+It's common that you need to find a record or create it if it doesn't exist. You can do that with the +first_or_create+ and +first_or_create!+ methods.
+
+h4. +first_or_create+
+
+The +first_or_create+ method checks whether +first+ returns +nil+ or not. If it does return +nil+, then +create+ is called. This is very powerful when coupled with the +where+ method. Let's see an example.
+
+Suppose you want to find a client named 'Andy', and if there's none, create one and additionally set his +locked+ attribute to false. You can do so by running:
+
+<ruby>
+Client.where(:first_name => 'Andy').first_or_create(:locked => false)
+# => <Client id: 1, first_name: "Andy", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+</ruby>
+
+The SQL generated by this method looks like this:
<sql>
-SELECT * FROM clients WHERE (clients.first_name = 'Ryan') LIMIT 1
+SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
-INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked)
- VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0')
+INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 0, NULL, '2011-08-30 05:22:57')
COMMIT
</sql>
-+find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similarly to calling +new+ with the arguments you passed in. For example:
++first_or_create+ returns either the record that already existed or the new record. In our case, we didn't already have a client named Andy so the record was created an returned.
+
+The new record might not be saved to the database; that depends on whether validations passed or not (just like +create+).
+
+It's also worth noting that +first_or_create+ takes into account the arguments of the +where+ method. In the example above we didn't explicitly pass a +:first_name => 'Andy'+ argument to +first_or_create+. However, that was used when creating the new record because it was already passed before to the +where+ method.
+
+NOTE: On previous versions of Rails you could do a similar thing with the +find_or_create_by+ method. Following our example, you could also run something like +Client.find_or_create_by_first_name(:first_name => "Andy", :locked => false)+. This method still works, but it's encouraged to use +first_or_create+ because it's more explicit on what arguments are used to _find_ the record and what arguments are used to _create_ it, resulting in less confusion overall.
+
+h4. +first_or_create!+
+
+You can also use +first_or_create!+ to raise an exception if the new record is invalid. Validations are not covered on this guide, but let's assume for a moment that you temporarily add
+
+<ruby>
+ validates :orders_count, :presence => true
+</ruby>
+
+to your +Client+ model. If you try to create a new +Client+ without passing an +orders_count+, the record will be invalid and an exception will be raised:
+
+<ruby>
+Client.where(:first_name => 'Andy').first_or_create!(:locked => false)
+# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
+</ruby>
+
+NOTE: Be sure to check the extensive *Active Record Validations and Callbacks Guide* for more information about validations.
+
+h4. +first_or_new+
+
+The +first_or_new+ method will work just like +first_or_create+ but it will not call +create+ but +new+. This means that a new model instance will be created in memory but won't be saved to the database. Continuing with the +first_or_create+ example, we now want the client named 'Nick':
+
+<ruby>
+nick = Client.where(:first_name => 'Nick').first_or_new(:locked => false)
+# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+nick.persisted?
+# => false
+
+nick.new_record?
+# => true
+</ruby>
+
+Because the object is not yet stored in the database, the SQL generated looks like this:
+
+<sql>
+SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
+</sql>
+
+When you want to save it to the database, just call +save+:
<ruby>
-client = Client.find_or_initialize_by_first_name('Ryan')
+nick.save
+# => true
</ruby>
-will either assign an existing client object with the name "Ryan" to the client local variable, or initialize a new object similar to calling +Client.new(:first_name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it.
+Just like you can use *+build+* instead of *+new+*, you can use *+first_or_build+* instead of *+first_or_new+*.
h3. Finding by SQL

0 comments on commit cbf1dc7

Please sign in to comment.