Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #20429 -- Added QuerySet.update_or_create

Thanks tunixman for the suggestion and Loic Bistuer for the review.
  • Loading branch information...
commit 6272d2f155adb4f32ef129d57e9eb5493ebde6ed 1 parent 66f3d57
Karol Sikora authored timgraham committed
1  AUTHORS
@@ -530,6 +530,7 @@ answer newbie questions, and generally made Django that much better:
530 530
     Leo Shklovskii
531 531
     jason.sidabras@gmail.com
532 532
     Mikołaj Siedlarek <mikolaj.siedlarek@gmail.com>
  533
+    Karol Sikora <elektrrrus@gmail.com>
533 534
     Brenton Simpson <http://theillustratedlife.com>
534 535
     Jozko Skrablin <jozko.skrablin@gmail.com>
535 536
     Ben Slavin <benjamin.slavin@gmail.com>
3  django/db/models/manager.py
@@ -154,6 +154,9 @@ def get(self, *args, **kwargs):
154 154
     def get_or_create(self, **kwargs):
155 155
         return self.get_queryset().get_or_create(**kwargs)
156 156
 
  157
+    def update_or_create(self, **kwargs):
  158
+        return self.get_queryset().update_or_create(**kwargs)
  159
+
157 160
     def create(self, **kwargs):
158 161
         return self.get_queryset().create(**kwargs)
159 162
 
89  django/db/models/query.py
@@ -364,37 +364,84 @@ def bulk_create(self, objs, batch_size=None):
364 364
 
365 365
         return objs
366 366
 
367  
-    def get_or_create(self, **kwargs):
  367
+    def get_or_create(self, defaults=None, **kwargs):
368 368
         """
369 369
         Looks up an object with the given kwargs, creating one if necessary.
370 370
         Returns a tuple of (object, created), where created is a boolean
371 371
         specifying whether an object was created.
372 372
         """
373  
-        defaults = kwargs.pop('defaults', {})
374  
-        lookup = kwargs.copy()
375  
-        for f in self.model._meta.fields:
376  
-            if f.attname in lookup:
377  
-                lookup[f.name] = lookup.pop(f.attname)
  373
+        lookup, params, _ = self._extract_model_params(defaults, **kwargs)
378 374
         try:
379 375
             self._for_write = True
380 376
             return self.get(**lookup), False
381 377
         except self.model.DoesNotExist:
  378
+            return self._create_object_from_params(lookup, params)
  379
+
  380
+    def update_or_create(self, defaults=None, **kwargs):
  381
+        """
  382
+        Looks up an object with the given kwargs, updating one with defaults
  383
+        if it exists, otherwise creates a new one.
  384
+        Returns a tuple (object, created), where created is a boolean
  385
+        specifying whether an object was created.
  386
+        """
  387
+        lookup, params, filtered_defaults = self._extract_model_params(defaults, **kwargs)
  388
+        try:
  389
+            self._for_write = True
  390
+            obj = self.get(**lookup)
  391
+        except self.model.DoesNotExist:
  392
+            obj, created = self._create_object_from_params(lookup, params)
  393
+            if created:
  394
+                return obj, created
  395
+        for k, v in six.iteritems(filtered_defaults):
  396
+            setattr(obj, k, v)
  397
+        try:
  398
+            sid = transaction.savepoint(using=self.db)
  399
+            obj.save(update_fields=filtered_defaults.keys(), using=self.db)
  400
+            transaction.savepoint_commit(sid, using=self.db)
  401
+            return obj, False
  402
+        except DatabaseError:
  403
+            transaction.savepoint_rollback(sid, using=self.db)
  404
+            six.reraise(sys.exc_info())
  405
+
  406
+    def _create_object_from_params(self, lookup, params):
  407
+        """
  408
+        Tries to create an object using passed params.
  409
+        Used by get_or_create and update_or_create
  410
+        """
  411
+        try:
  412
+            obj = self.model(**params)
  413
+            sid = transaction.savepoint(using=self.db)
  414
+            obj.save(force_insert=True, using=self.db)
  415
+            transaction.savepoint_commit(sid, using=self.db)
  416
+            return obj, True
  417
+        except DatabaseError:
  418
+            transaction.savepoint_rollback(sid, using=self.db)
  419
+            exc_info = sys.exc_info()
382 420
             try:
383  
-                params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k)
384  
-                params.update(defaults)
385  
-                obj = self.model(**params)
386  
-                sid = transaction.savepoint(using=self.db)
387  
-                obj.save(force_insert=True, using=self.db)
388  
-                transaction.savepoint_commit(sid, using=self.db)
389  
-                return obj, True
390  
-            except DatabaseError:
391  
-                transaction.savepoint_rollback(sid, using=self.db)
392  
-                exc_info = sys.exc_info()
393  
-                try:
394  
-                    return self.get(**lookup), False
395  
-                except self.model.DoesNotExist:
396  
-                    # Re-raise the DatabaseError with its original traceback.
397  
-                    six.reraise(*exc_info)
  421
+                return self.get(**lookup), False
  422
+            except self.model.DoesNotExist:
  423
+                # Re-raise the DatabaseError with its original traceback.
  424
+                six.reraise(*exc_info)
  425
+
  426
+    def _extract_model_params(self, defaults, **kwargs):
  427
+        """
  428
+        Prepares `lookup` (kwargs that are valid model attributes), `params`
  429
+        (for creating a model instance) and `filtered_defaults` (defaults
  430
+        that are valid model attributes) based on given kwargs; for use by
  431
+        get_or_create and update_or_create.
  432
+        """
  433
+        defaults = defaults or {}
  434
+        filtered_defaults = {}
  435
+        lookup = kwargs.copy()
  436
+        for f in self.model._meta.fields:
  437
+            # Filter out fields that don't belongs to the model.
  438
+            if f.attname in lookup:
  439
+                lookup[f.name] = lookup.pop(f.attname)
  440
+            if f.attname in defaults:
  441
+                filtered_defaults[f.name] = defaults.pop(f.attname)
  442
+        params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k)
  443
+        params.update(filtered_defaults)
  444
+        return lookup, params, filtered_defaults
398 445
 
399 446
     def _earliest_or_latest(self, field_name=None, direction="-"):
400 447
         """
46  docs/ref/models/querysets.txt
@@ -1330,7 +1330,7 @@ prepared to handle the exception if you are using manual primary keys.
1330 1330
 get_or_create
1331 1331
 ~~~~~~~~~~~~~
1332 1332
 
1333  
-.. method:: get_or_create(**kwargs)
  1333
+.. method:: get_or_create(defaults=None, **kwargs)
1334 1334
 
1335 1335
 A convenience method for looking up an object with the given ``kwargs`` (may be
1336 1336
 empty if your model has defaults for all fields), creating one if necessary.
@@ -1366,7 +1366,6 @@ found, ``get_or_create()`` will instantiate and save a new object, returning a
1366 1366
 tuple of the new object and ``True``. The new object will be created roughly
1367 1367
 according to this algorithm::
1368 1368
 
1369  
-    defaults = kwargs.pop('defaults', {})
1370 1369
     params = dict([(k, v) for k, v in kwargs.items() if '__' not in k])
1371 1370
     params.update(defaults)
1372 1371
     obj = self.model(**params)
@@ -1447,6 +1446,49 @@ in the HTTP spec.
1447 1446
   chapter because it isn't related to that book, but it can't create it either
1448 1447
   because ``title`` field should be unique.
1449 1448
 
  1449
+update_or_create
  1450
+~~~~~~~~~~~~~~~~
  1451
+
  1452
+.. method:: update_or_create(defaults=None, **kwargs)
  1453
+
  1454
+.. versionadded:: 1.7
  1455
+
  1456
+A convenience method for updating an object with the given ``kwargs``, creating
  1457
+a new one if necessary. The ``defaults`` is a dictionary of (field, value)
  1458
+pairs used to update the object.
  1459
+
  1460
+Returns a tuple of ``(object, created)``, where ``object`` is the created or
  1461
+updated object and ``created`` is a boolean specifying whether a new object was
  1462
+created.
  1463
+
  1464
+The ``update_or_create`` method tries to fetch an object from database based on
  1465
+the given ``kwargs``. If a match is found, it updates the fields passed in the
  1466
+``defaults`` dictionary.
  1467
+
  1468
+This is meant as a shortcut to boilerplatish code. For example::
  1469
+
  1470
+    try:
  1471
+        obj = Person.objects.get(first_name='John', last_name='Lennon')
  1472
+        for key, value in updated_values.iteritems():
  1473
+            setattr(obj, key, value)
  1474
+        obj.save()
  1475
+    except Person.DoesNotExist:
  1476
+        updated_values.update({'first_name': 'John', 'last_name': 'Lennon'})
  1477
+        obj = Person(**updated_values)
  1478
+        obj.save()
  1479
+
  1480
+This pattern gets quite unwieldy as the number of fields in a model goes up.
  1481
+The above example can be rewritten using ``update_or_create()`` like so::
  1482
+
  1483
+    obj, created = Person.objects.update_or_create(
  1484
+        first_name='John', last_name='Lennon', defaults=updated_values)
  1485
+
  1486
+For detailed description how names passed in ``kwargs`` are resolved see
  1487
+:meth:`get_or_create`.
  1488
+
  1489
+As described above in :meth:`get_or_create`, this method is prone to a
  1490
+race-condition which can result in multiple rows being inserted simultaneously
  1491
+if uniqueness is not enforced at the database level.
1450 1492
 
1451 1493
 bulk_create
1452 1494
 ~~~~~~~~~~~
3  docs/releases/1.7.txt
@@ -41,6 +41,9 @@ Minor features
41 41
 * The ``enter`` argument was added to the
42 42
   :data:`~django.test.signals.setting_changed` signal.
43 43
 
  44
+* The :meth:`QuerySet.update_or_create()
  45
+  <django.db.models.query.QuerySet.update_or_create>` method was added.
  46
+
44 47
 Backwards incompatible changes in 1.7
45 48
 =====================================
46 49
 
65  tests/get_or_create/tests.py
@@ -131,3 +131,68 @@ def test_something(self):
131 131
         Tag.objects.create(text='foo')
132 132
         a_thing = Thing.objects.create(name='a')
133 133
         self.assertRaises(IntegrityError, a_thing.tags.get_or_create, text='foo')
  134
+
  135
+
  136
+class UpdateOrCreateTests(TestCase):
  137
+
  138
+    def test_update(self):
  139
+        Person.objects.create(
  140
+            first_name='John', last_name='Lennon', birthday=date(1940, 10, 9)
  141
+        )
  142
+        p, created = Person.objects.update_or_create(
  143
+            first_name='John', last_name='Lennon', defaults={
  144
+                'birthday': date(1940, 10, 10)
  145
+            }
  146
+        )
  147
+        self.assertFalse(created)
  148
+        self.assertEqual(p.first_name, 'John')
  149
+        self.assertEqual(p.last_name, 'Lennon')
  150
+        self.assertEqual(p.birthday, date(1940, 10, 10))
  151
+
  152
+    def test_create(self):
  153
+        p, created = Person.objects.update_or_create(
  154
+            first_name='John', last_name='Lennon', defaults={
  155
+                'birthday': date(1940, 10, 10)
  156
+            }
  157
+        )
  158
+        self.assertTrue(created)
  159
+        self.assertEqual(p.first_name, 'John')
  160
+        self.assertEqual(p.last_name, 'Lennon')
  161
+        self.assertEqual(p.birthday, date(1940, 10, 10))
  162
+
  163
+    def test_create_twice(self):
  164
+        params = {
  165
+            'first_name': 'John',
  166
+            'last_name': 'Lennon',
  167
+            'birthday': date(1940, 10, 10),
  168
+        }
  169
+        Person.objects.update_or_create(**params)
  170
+        # If we execute the exact same statement, it won't create a Person.
  171
+        p, created = Person.objects.update_or_create(**params)
  172
+        self.assertFalse(created)
  173
+
  174
+    def test_integrity(self):
  175
+        # If you don't specify a value or default value for all required
  176
+        # fields, you will get an error.
  177
+        self.assertRaises(IntegrityError,
  178
+            Person.objects.update_or_create, first_name="Tom", last_name="Smith")
  179
+
  180
+    def test_mananual_primary_key_test(self):
  181
+        # If you specify an existing primary key, but different other fields,
  182
+        # then you will get an error and data will not be updated.
  183
+        ManualPrimaryKeyTest.objects.create(id=1, data="Original")
  184
+        self.assertRaises(IntegrityError,
  185
+            ManualPrimaryKeyTest.objects.update_or_create, id=1, data="Different"
  186
+        )
  187
+        self.assertEqual(ManualPrimaryKeyTest.objects.get(id=1).data, "Original")
  188
+
  189
+    def test_error_contains_full_traceback(self):
  190
+        # update_or_create should raise IntegrityErrors with the full traceback.
  191
+        # This is tested by checking that a known method call is in the traceback.
  192
+        # We cannot use assertRaises/assertRaises here because we need to inspect
  193
+        # the actual traceback. Refs #16340.
  194
+        try:
  195
+            ManualPrimaryKeyTest.objects.update_or_create(id=1, data="Different")
  196
+        except IntegrityError as e:
  197
+            formatted_traceback = traceback.format_exc()
  198
+            self.assertIn('obj.save', formatted_traceback)

0 notes on commit 6272d2f

Simon Charette

Filtering defaults prevents non-fields kwargs from reaching the model initializer in get_or_create().

e.g. Prior to this change it was possible to pass the value of a GenericForeignKey (which is a virtual field) as a default:
Model.objects.get_or_create(slug=slug, default={'content_object': content_object})

The worst part is that no exception is raised, the specified default value is just dropped.

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