Skip to content

Commit

Permalink
Merge pull request #2757 from andmej/first_or_create_pull_request
Browse files Browse the repository at this point in the history
Add first_or_create family of methods to Active Record
  • Loading branch information
jonleighton committed Sep 8, 2011
2 parents 04baa4b + 7231788 commit cbf1dc7
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 7 deletions.
6 changes: 6 additions & 0 deletions activerecord/CHANGELOG
Expand Up @@ -4,6 +4,12 @@ Wed Sep 7 15:25:02 2011 Aaron Patterson <aaron@tenderlovemaking.com>
keys are per process id. keys are per process id.
* lib/active_record/connection_adapters/sqlite_adapter.rb: ditto * 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] * 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 * If multiple parameters are sent representing a date, and some are blank, the
Expand Down
1 change: 1 addition & 0 deletions activerecord/lib/active_record/base.rb
Expand Up @@ -442,6 +442,7 @@ class Base


class << self # Class methods class << self # Class methods
delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped 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 :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :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 delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
Expand Down
43 changes: 43 additions & 0 deletions activerecord/lib/active_record/relation.rb
Expand Up @@ -94,6 +94,49 @@ def create!(*args, &block)
scoping { @klass.create!(*args, &block) } scoping { @klass.create!(*args, &block) }
end 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)

This comment has been minimized.

Copy link
@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).

This comment has been minimized.

Copy link
@jonleighton

jonleighton Sep 8, 2011

Author 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).

This comment has been minimized.

Copy link
@jpemberthy

jpemberthy Sep 8, 2011

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

This comment has been minimized.

Copy link
@tenderlove

tenderlove Sep 8, 2011

Member

👎 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.

This comment has been minimized.

Copy link
@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.

This comment has been minimized.

Copy link
@fxn

fxn Sep 8, 2011

Member

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.

This comment has been minimized.

Copy link
@jonleighton

jonleighton Sep 13, 2011

Author 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) def respond_to?(method, include_private = false)
arel.respond_to?(method, include_private) || arel.respond_to?(method, include_private) ||
Array.method_defined?(method) || Array.method_defined?(method) ||
Expand Down
31 changes: 31 additions & 0 deletions activerecord/test/cases/base_test.rb
Expand Up @@ -278,6 +278,37 @@ def test_create_after_initialize_with_block
assert_equal(true, cb.frickinawesome) assert_equal(true, cb.frickinawesome)
end 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 def test_load
topics = Topic.find(:all, :order => 'id') topics = Topic.find(:all, :order => 'id')
assert_equal(4, topics.size) assert_equal(4, topics.size)
Expand Down
128 changes: 128 additions & 0 deletions activerecord/test/cases/relations_test.rb
Expand Up @@ -863,6 +863,134 @@ def test_create_bang
assert_equal 'hen', hen.name assert_equal 'hen', hen.name
end 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 def test_explicit_create_scope
hens = Bird.where(:name => 'hen') hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name assert_equal 'hen', hens.new.name
Expand Down
75 changes: 68 additions & 7 deletions railties/guides/source/active_record_querying.textile
Expand Up @@ -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+. 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> <sql>
SELECT * FROM clients WHERE (clients.first_name = 'Ryan') LIMIT 1 SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN BEGIN
INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked) 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')
VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0')
COMMIT COMMIT
</sql> </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> <ruby>
client = Client.find_or_initialize_by_first_name('Ryan') nick.save
# => true
</ruby> </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 h3. Finding by SQL


Expand Down

0 comments on commit cbf1dc7

Please sign in to comment.