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:
parent
0d7b0f0183
commit
eb72e62c30
@ -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
|
||||
--------------
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user