Skip to content
This repository
Browse code

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
  • Loading branch information...
commit eb72e62c3042c0df989d951b1d12291395ebdb8e 1 parent 0d7b0f0
Jon Leighton authored October 19, 2012
34  activerecord/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,35 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   Add `find_or_create_by`, `find_or_create_by!` and
  4
+    `find_or_initialize_by` methods to `Relation`.
  5
+
  6
+    These are similar to the `first_or_create` family of methods, but
  7
+    the behaviour when a record is created is slightly different:
  8
+
  9
+        User.where(first_name: 'Penélope').first_or_create
  10
+
  11
+    will execute:
  12
+
  13
+        User.where(first_name: 'Penélope').create
  14
+
  15
+    Causing all the `create` callbacks to execute within the context of
  16
+    the scope. This could affect queries that occur within callbacks.
  17
+
  18
+        User.find_or_create_by(first_name: 'Penélope')
  19
+
  20
+    will execute:
  21
+
  22
+        User.create(first_name: 'Penélope')
  23
+
  24
+    Which obviously does not affect the scoping of queries within
  25
+    callbacks.
  26
+
  27
+    The `find_or_create_by` version also reads better, frankly. But note
  28
+    that it does not allow attributes to be specified for the `create`
  29
+    that are not included in the `find_by`.
  30
+
  31
+    *Jon Leighton*
  32
+
3 33
 *   Fix bug with presence validation of associations. Would incorrectly add duplicated errors 
4 34
     when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13.
5 35
 
@@ -607,9 +637,9 @@
607 637
       * `find_or_initialize_by_...` can be rewritten using
608 638
         `where(...).first_or_initialize`
609 639
       * `find_or_create_by_...` can be rewritten using
610  
-        `where(...).first_or_create`
  640
+        `find_or_create_by(...)` or where(...).first_or_create`
611 641
       * `find_or_create_by_...!` can be rewritten using
612  
-        `where(...).first_or_create!`
  642
+        `find_or_create_by!(...) or `where(...).first_or_create!`
613 643
 
614 644
     The implementation of the deprecated dynamic finders has been moved
615 645
     to the `activerecord-deprecated_finders` gem. See below for details.
1  activerecord/lib/active_record/querying.rb
@@ -3,6 +3,7 @@ module ActiveRecord
3 3
   module Querying
4 4
     delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
5 5
     delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
  6
+    delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all
6 7
     delegate :find_by, :find_by!, :to => :all
7 8
     delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all
8 9
     delegate :find_each, :find_in_batches, :to => :all
26  activerecord/lib/active_record/relation.rb
@@ -133,6 +133,10 @@ def create!(*args, &block)
133 133
     #
134 134
     # Expects arguments in the same format as +Base.create+.
135 135
     #
  136
+    # Note that the <tt>create</tt> will execute within the context of this scope, and that may for example
  137
+    # affect the result of queries within callbacks. If you don't want this, use the <tt>find_or_create_by</tt>
  138
+    # method.
  139
+    #
136 140
     # ==== Examples
137 141
     #   # Find the first user named Penélope or create a new one.
138 142
     #   User.where(:first_name => 'Penélope').first_or_create
@@ -171,6 +175,28 @@ def first_or_initialize(attributes = nil, &block)
171 175
       first || new(attributes, &block)
172 176
     end
173 177
 
  178
+    # Finds the first record with the given attributes, or creates it if one does not exist.
  179
+    #
  180
+    # See also <tt>first_or_create</tt>.
  181
+    #
  182
+    # ==== Examples
  183
+    #   # Find the first user named Penélope or create a new one.
  184
+    #   User.find_or_create_by(first_name: 'Penélope')
  185
+    #   # => <User id: 1, first_name: 'Penélope', last_name: nil>
  186
+    def find_or_create_by(attributes, &block)
  187
+      find_by(attributes) || create(attributes, &block)
  188
+    end
  189
+
  190
+    # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
  191
+    def find_or_create_by!(attributes, &block)
  192
+      find_by(attributes) || create!(attributes, &block)
  193
+    end
  194
+
  195
+    # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
  196
+    def find_or_initialize_by(attributes, &block)
  197
+      find_by(attributes) || new(attributes, &block)
  198
+    end
  199
+
174 200
     # Runs EXPLAIN on the query or queries triggered by this relation and
175 201
     # returns the result as a string. The string is formatted imitating the
176 202
     # ones printed by the database shell.
23  activerecord/test/cases/relations_test.rb
@@ -1058,6 +1058,29 @@ def test_first_or_initialize_with_block
1058 1058
     assert_equal 'parrot', parrot.name
1059 1059
   end
1060 1060
 
  1061
+  def test_find_or_create_by
  1062
+    assert_nil Bird.find_by(name: 'bob')
  1063
+
  1064
+    bird = Bird.find_or_create_by(name: 'bob')
  1065
+    assert bird.persisted?
  1066
+
  1067
+    assert_equal bird, Bird.find_or_create_by(name: 'bob')
  1068
+  end
  1069
+
  1070
+  def test_find_or_create_by!
  1071
+    assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') }
  1072
+  end
  1073
+
  1074
+  def test_find_or_initialize_by
  1075
+    assert_nil Bird.find_by(name: 'bob')
  1076
+
  1077
+    bird = Bird.find_or_initialize_by(name: 'bob')
  1078
+    assert bird.new_record?
  1079
+    bird.save!
  1080
+
  1081
+    assert_equal bird, Bird.find_or_initialize_by(name: 'bob')
  1082
+  end
  1083
+
1061 1084
   def test_explicit_create_scope
1062 1085
     hens = Bird.where(:name => 'hen')
1063 1086
     assert_equal 'hen', hens.new.name
71  guides/source/active_record_querying.md
Source Rendered
@@ -1225,17 +1225,17 @@ WARNING: Up to and including Rails 3.1, when the number of arguments passed to a
1225 1225
 Find or build a new object
1226 1226
 --------------------------
1227 1227
 
1228  
-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.
  1228
+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.
1229 1229
 
1230  
-### `first_or_create`
  1230
+### `find_or_create_by` and `first_or_create`
1231 1231
 
1232  
-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.
  1232
+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.
1233 1233
 
1234  
-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:
  1234
+Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running:
1235 1235
 
1236 1236
 ```ruby
1237  
-Client.where(:first_name => 'Andy').first_or_create(:locked => false)
1238  
-# => #<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">
  1237
+Client.find_or_create_by(first_name: 'Andy')
  1238
+# => #<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">
1239 1239
 ```
1240 1240
 
1241 1241
 The SQL generated by this method looks like this:
@@ -1243,27 +1243,50 @@ The SQL generated by this method looks like this:
1243 1243
 ```sql
1244 1244
 SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
1245 1245
 BEGIN
1246  
-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')
  1246
+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')
1247 1247
 COMMIT
1248 1248
 ```
1249 1249
 
1250  
-`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.
  1250
+`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.
1251 1251
 
1252 1252
 The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`).
1253 1253
 
1254  
-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.
  1254
+Suppose we want to set the 'locked' attribute to true, if we're
  1255
+creating a new record, but we don't want to include it in the query. So
  1256
+we want to find the client named "Andy", or if that client doesn't
  1257
+exist, create a client named "Andy" which is not locked.
1255 1258
 
1256  
-You can do the same with the `find_or_create_by` method:
  1259
+We can achive this in two ways. The first is passing a block to the
  1260
+`find_or_create_by` method:
1257 1261
 
1258 1262
 ```ruby
1259  
-Client.find_or_create_by_first_name(:first_name => "Andy", :locked => false)
  1263
+Client.find_or_create_by(first_name: 'Andy') do |c|
  1264
+  c.locked = false
  1265
+end
  1266
+```
  1267
+
  1268
+The block will only be executed if the client is being created. The
  1269
+second time we run this code, the block will be ignored.
  1270
+
  1271
+The second way is using the `first_or_create` method:
  1272
+
  1273
+```ruby
  1274
+Client.where(first_name: 'Andy').first_or_create(locked: false)
1260 1275
 ```
1261 1276
 
1262  
-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.
  1277
+In this version, we are building a scope to search for Andy, and getting
  1278
+the first record if it existed, or else creating it with `locked:
  1279
+false`.
1263 1280
 
1264  
-### `first_or_create!`
  1281
+Note that these two are slightly different. In the second version, the
  1282
+scope that we build will affect any other queries that may happens while
  1283
+creating the record. For example, if we had a callback that ran
  1284
+another query, that would execute within the `Client.where(first_name:
  1285
+'Andy')` scope.
1265 1286
 
1266  
-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
  1287
+### `find_or_create_by!` and `first_or_create!`
  1288
+
  1289
+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
1267 1290
 
1268 1291
 ```ruby
1269 1292
 validates :orders_count, :presence => true
@@ -1272,19 +1295,24 @@ validates :orders_count, :presence => true
1272 1295
 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:
1273 1296
 
1274 1297
 ```ruby
1275  
-Client.where(:first_name => 'Andy').first_or_create!(:locked => false)
  1298
+Client.find_or_create_by!(first_name: 'Andy')
1276 1299
 # => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
1277 1300
 ```
1278 1301
 
1279  
-As with `first_or_create` there is a `find_or_create_by!` method but the `first_or_create!` method is preferred for clarity.
  1302
+There is also a `first_or_create!` method which does a similar thing for
  1303
+`first_or_create`.
1280 1304
 
1281  
-### `first_or_initialize`
  1305
+### `find_or_initialize_by` and `first_or_initialize`
1282 1306
 
1283  
-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':
  1307
+The `find_or_initialize_by` method will work just like
  1308
+`find_or_create_by` but it will call `new` instead of `create`. This
  1309
+means that a new model instance will be created in memory but won't be
  1310
+saved to the database. Continuing with the `find_or_create_by` example, we
  1311
+now want the client named 'Nick':
1284 1312
 
1285 1313
 ```ruby
1286  
-nick = Client.where(:first_name => 'Nick').first_or_initialize(:locked => false)
1287  
-# => <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">
  1314
+nick = Client.find_or_initialize_by(first_name: 'Nick')
  1315
+# => <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">
1288 1316
 
1289 1317
 nick.persisted?
1290 1318
 # => false
@@ -1306,6 +1334,9 @@ nick.save
1306 1334
 # => true
1307 1335
 ```
1308 1336
 
  1337
+There is also a `first_or_initialize` method which does a similar thing
  1338
+for `first_or_create`.
  1339
+
1309 1340
 Finding by SQL
1310 1341
 --------------
1311 1342
 

3 notes on commit eb72e62

Yuval Kordov

Having written the original issue, I'm not sure this commit fixes it.

find_or_create_by_attr and first_or_create allow you to pass additional values inline into the object on creation, which is highly desirable. It would probably just be better to remove first_or_create, since its broken, and continue to use find_or_create_by_attr:

User.find_or_create_by_name(:name => "jon", :foo => "bar", :cat => "dog")
Jon Leighton
Owner

@uberllama:

You can do this:

User.create_with(foo: 'bar').find_or_create_by(name: "jon")

Or this:

User.find_or_create_by(name: 'jon') { |u| u.foo = 'bar' }

Based on that, I think there's no reason for first_or_create to stay, and it's a weird API. But I think deprecating it is probably OTT. I think I will nodoc it and we can possibly deprecate some time in the future.

Yuval Kordov

Thanks Jon. I'll probably just continue to use find_or_create_by_attr ;) but I'm glad to see the issue addressed. Cheers.

Please sign in to comment.
Something went wrong with that request. Please try again.