Add Relation#find_or_create_by and friends

This is similar to #first_or_create, but slightly different and a nicer
API. See the CHANGELOG/docs in the commit.

Fixes #7853
This commit is contained in:
Jon Leighton 2012-10-19 13:18:47 +01:00
parent 0d7b0f0183
commit eb72e62c30
5 changed files with 133 additions and 22 deletions

@ -1,5 +1,35 @@
## Rails 4.0.0 (unreleased) ##
* Add `find_or_create_by`, `find_or_create_by!` and
`find_or_initialize_by` methods to `Relation`.
These are similar to the `first_or_create` family of methods, but
the behaviour when a record is created is slightly different:
User.where(first_name: 'Penélope').first_or_create
will execute:
User.where(first_name: 'Penélope').create
Causing all the `create` callbacks to execute within the context of
the scope. This could affect queries that occur within callbacks.
User.find_or_create_by(first_name: 'Penélope')
will execute:
User.create(first_name: 'Penélope')
Which obviously does not affect the scoping of queries within
callbacks.
The `find_or_create_by` version also reads better, frankly. But note
that it does not allow attributes to be specified for the `create`
that are not included in the `find_by`.
*Jon Leighton*
* Fix bug with presence validation of associations. Would incorrectly add duplicated errors
when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13.
@ -607,9 +637,9 @@
* `find_or_initialize_by_...` can be rewritten using
`where(...).first_or_initialize`
* `find_or_create_by_...` can be rewritten using
`where(...).first_or_create`
`find_or_create_by(...)` or where(...).first_or_create`
* `find_or_create_by_...!` can be rewritten using
`where(...).first_or_create!`
`find_or_create_by!(...) or `where(...).first_or_create!`
The implementation of the deprecated dynamic finders has been moved
to the `activerecord-deprecated_finders` gem. See below for details.

@ -3,6 +3,7 @@ module ActiveRecord
module Querying
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all
delegate :find_by, :find_by!, :to => :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all
delegate :find_each, :find_in_batches, :to => :all

@ -133,6 +133,10 @@ def create!(*args, &block)
#
# Expects arguments in the same format as +Base.create+.
#
# Note that the <tt>create</tt> will execute within the context of this scope, and that may for example
# affect the result of queries within callbacks. If you don't want this, use the <tt>find_or_create_by</tt>
# method.
#
# ==== Examples
# # Find the first user named Penélope or create a new one.
# User.where(:first_name => 'Penélope').first_or_create
@ -171,6 +175,28 @@ def first_or_initialize(attributes = nil, &block)
first || new(attributes, &block)
end
# Finds the first record with the given attributes, or creates it if one does not exist.
#
# See also <tt>first_or_create</tt>.
#
# ==== Examples
# # Find the first user named Penélope or create a new one.
# User.find_or_create_by(first_name: 'Penélope')
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
# Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
# Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
# Runs EXPLAIN on the query or queries triggered by this relation and
# returns the result as a string. The string is formatted imitating the
# ones printed by the database shell.

@ -1058,6 +1058,29 @@ def test_first_or_initialize_with_block
assert_equal 'parrot', parrot.name
end
def test_find_or_create_by
assert_nil Bird.find_by(name: 'bob')
bird = Bird.find_or_create_by(name: 'bob')
assert bird.persisted?
assert_equal bird, Bird.find_or_create_by(name: 'bob')
end
def test_find_or_create_by!
assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') }
end
def test_find_or_initialize_by
assert_nil Bird.find_by(name: 'bob')
bird = Bird.find_or_initialize_by(name: 'bob')
assert bird.new_record?
bird.save!
assert_equal bird, Bird.find_or_initialize_by(name: 'bob')
end
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name

@ -1225,17 +1225,17 @@ WARNING: Up to and including Rails 3.1, when the number of arguments passed to a
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.
It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods.
### `first_or_create`
### `find_or_create_by` and `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.
The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. 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:
Suppose you want to find a client named 'Andy', and if there's none, create one. 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">
Client.find_or_create_by(first_name: 'Andy')
# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
```
The SQL generated by this method looks like this:
@ -1243,27 +1243,50 @@ The SQL generated by this method looks like this:
```sql
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
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')
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
```
`first_or_create` returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and returned.
`find_or_create_by` returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and 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.
Suppose we want to set the 'locked' attribute to true, if we're
creating a new record, but we don't want to include it in the query. So
we want to find the client named "Andy", or if that client doesn't
exist, create a client named "Andy" which is not locked.
You can do the same with the `find_or_create_by` method:
We can achive this in two ways. The first is passing a block to the
`find_or_create_by` method:
```ruby
Client.find_or_create_by_first_name(:first_name => "Andy", :locked => false)
Client.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
```
This method still works, but it's encouraged to use `first_or_create` because it's more explicit on which arguments are used to _find_ the record and which are used to _create_, resulting in less confusion overall.
The block will only be executed if the client is being created. The
second time we run this code, the block will be ignored.
### `first_or_create!`
The second way is using the `first_or_create` method:
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
Client.where(first_name: 'Andy').first_or_create(locked: false)
```
In this version, we are building a scope to search for Andy, and getting
the first record if it existed, or else creating it with `locked:
false`.
Note that these two are slightly different. In the second version, the
scope that we build will affect any other queries that may happens while
creating the record. For example, if we had a callback that ran
another query, that would execute within the `Client.where(first_name:
'Andy')` scope.
### `find_or_create_by!` and `first_or_create!`
You can also use `find_or_create_by!` 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
@ -1272,19 +1295,24 @@ validates :orders_count, :presence => true
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)
Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
```
As with `first_or_create` there is a `find_or_create_by!` method but the `first_or_create!` method is preferred for clarity.
There is also a `first_or_create!` method which does a similar thing for
`first_or_create`.
### `first_or_initialize`
### `find_or_initialize_by` and `first_or_initialize`
The `first_or_initialize` 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':
The `find_or_initialize_by` method will work just like
`find_or_create_by` but it will call `new` instead of `create`. This
means that a new model instance will be created in memory but won't be
saved to the database. Continuing with the `find_or_create_by` example, we
now want the client named 'Nick':
```ruby
nick = Client.where(:first_name => 'Nick').first_or_initialize(: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 = Client.find_or_initialize_by(first_name: 'Nick')
# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
@ -1306,6 +1334,9 @@ nick.save
# => true
```
There is also a `first_or_initialize` method which does a similar thing
for `first_or_create`.
Finding by SQL
--------------