Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Ticket 20429 #1132

Closed
wants to merge 3 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+223 −39
Split
View
@@ -630,6 +630,7 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
+ Karol Sikora <elektrrrus@gmail.com>
@timgraham

timgraham May 29, 2013

Owner

AUTHORS should be alphabetized

A big THANK YOU goes to:
@@ -153,6 +153,9 @@ def get(self, *args, **kwargs):
def get_or_create(self, **kwargs):
return self.get_queryset().get_or_create(**kwargs)
+ def update_or_create(self, **kwargs):
+ return self.get_queryset().update_or_create(**kwargs)
+
def create(self, **kwargs):
return self.get_queryset().create(**kwargs)
View
@@ -364,19 +364,57 @@ def get_or_create(self, **kwargs):
specifying whether an object was created.
"""
assert kwargs, \
- 'get_or_create() must be passed at least one keyword argument'
- defaults = kwargs.pop('defaults', {})
- lookup = kwargs.copy()
- for f in self.model._meta.fields:
- if f.attname in lookup:
- lookup[f.name] = lookup.pop(f.attname)
+ 'get_or_create() must be passed at least one keyword argument'
@timgraham

timgraham May 29, 2013

Owner

please rebase your changes, this assertion has been removed

+ lookup, params, defaults = self._extract_model_params(**kwargs)
try:
self._for_write = True
return self.get(**lookup), False
except self.model.DoesNotExist:
+ return self._create_object_from_params(lookup, params)
+
+ def update_or_create(self, **kwargs):
+ """
+ Looks up an object with the given kwargs, update them with defaults
+ if exists, otherwise create new one.
@timgraham

timgraham May 29, 2013

Owner

create -> creates

+ Returns a tuple (object, created), where created is a boolean
+ specyfing whether an object was created.
+ """
+ assert kwargs, \
@timgraham

timgraham May 29, 2013

Owner

remove this assertion? (see 90af278)

+ 'update_or_create() must be passed at least one keyword argument'
+ lookup, defaults, params = self._extract_model_params(**kwargs)
+ created = False
+ try:
+ self._for_write = True
+ obj = self.get(**lookup)
+ except self.model.DoesNotExist:
+ obj, created = self._create_object_from_params(lookup, defaults)
+ if created:
+ return obj, created
+ for k, v in defaults.iteritems():
+ setattr(obj, k, v)
+ try:
+ sid = transaction.savepoint(using=self.db)
+ obj.save(update_fields=defaults.keys(), using=self.db)
+ transaction.savepoint_commit(sid, using=self.db)
+ return obj, False
+ except DatabaseError:
+ transaction.savepoint_rollback(sid, using=self.db)
+ six.reraise(sys.exc_info())
+
+ def _create_object_from_params(self, lookup, params):
+ """
+ Tries to creating object based on passed parameters.
+ Used in get_or_create and create_or_update
+ """
+ try:
+ obj = self.model(**params)
+ sid = transaction.savepoint(using=self.db)
+ obj.save(force_insert=True, using=self.db)
+ transaction.savepoint_commit(sid, using=self.db)
+ return obj, True
+ except DatabaseError:
+ transaction.savepoint_rollback(sid, using=self.db)
try:
- params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k)
- params.update(defaults)
obj = self.model(**params)
sid = transaction.savepoint(using=self.db)
obj.save(force_insert=True, using=self.db)
@@ -388,9 +426,27 @@ def get_or_create(self, **kwargs):
try:
return self.get(**lookup), False
except self.model.DoesNotExist:
- # Re-raise the DatabaseError with its original traceback.
+ # Re-raise the IntegrityError with its original traceback.
six.reraise(*exc_info)
+ def _extract_model_params(self, **kwargs):
+ """
+ Prepares lookup, params and defaults fields based on given kwargs.
+ Used in get_or_create and create_or_update.
+ """
+ unfiltered_defaults = kwargs.pop('defaults', {})
+ defaults = {}
+ lookup = kwargs.copy()
+ for f in self.model._meta.fields:
+ if f.attname in lookup:
+ lookup[f.name] = lookup.pop(f.attname)
+ # Strictly filters out fields that don't belongs to model.
+ if f.attname in unfiltered_defaults:
+ defaults[f.name] = unfiltered_defaults.pop(f.attname)
+ params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k)
+ params.update(defaults)
+ return lookup, params, defaults
+
def _earliest_or_latest(self, field_name=None, direction="-"):
"""
Returns the latest object, according to the model's
@@ -1365,6 +1365,10 @@ The above example can be rewritten using ``get_or_create()`` like so::
obj, created = Person.objects.get_or_create(first_name='John', last_name='Lennon',
defaults={'birthday': date(1940, 10, 9)})
+There is also similar method ``update_or_create`` which will be helpful
@timgraham

timgraham May 29, 2013

Owner

make it a link :meth:update_or_create

+in similar cases that reqires to ensure that model after performing update exists
@timgraham

timgraham May 29, 2013

Owner

English is awkward here. something like "in similar cases where an object with the given kwargs may already exist."

+even where they didn't exists in database before.
+
Any keyword arguments passed to ``get_or_create()`` — *except* an optional one
called ``defaults`` — will be used in a :meth:`get()` call. If an object is
found, ``get_or_create()`` returns a tuple of that object and ``False``. If
@@ -1444,6 +1448,45 @@ has a side effect on your data. For more, see `Safe methods`_ in the HTTP spec.
because ``title`` field should be unique.
+update_or_create
+~~~~~~~~~~~~~~~~
+
+.. method:: update_or_create(**kwargs)
+
@timgraham

timgraham May 29, 2013

Owner

.. versionadded:: 1.6

+A convenience method for updating an object with the given kwargs, creating
@timgraham

timgraham May 29, 2013

Owner

add "``" around kwargs

+new one if necessary.
+
+Returns a tuple of ``(object, created)``, where ``object`` is the created or
+updated object and ``created`` is a boolean specifying whether a new object was
+created.
+
+The ``create_or_update`` method tries to fetch object from database based on passed
@timgraham

timgraham May 29, 2013

Owner

create_or_update -> update_or_create

@timgraham

timgraham May 29, 2013

Owner

"fetch object" -> "fetch an object"

+keywords arguments, and then update fields passed in ``defaults`` dict on kwargs.
@timgraham

timgraham May 29, 2013

Owner

keywords arguments -> kwargs or "keyword arguments"

+
+There is also similar method ``get_or_create`` which will be useful when we need
@timgraham

timgraham May 29, 2013

Owner

:meth:get_or_create

@timgraham

timgraham May 29, 2013

Owner

will be useful -> is useful

+to get object with given parameters even when they didnt's exists before.
@timgraham

timgraham May 29, 2013

Owner

to get an object with the given parameters even if it doesn't exist.

+
+This is meant as a shortcut to boilerplatish code and is mostly useful for
+data-handling scripts. For example::
+
+ try:
@timgraham

timgraham May 29, 2013

Owner

only need 4 spaces on indentation for this code block

+ obj = SalesRank.objects.get(product_name='Pony')
+ for key, value in updated_values.iteritems():
+ setattr(obj, key, value)
+ obj.save()
+ except SalesRank.DoesNotExist:
+ obj = SalesRank(**updated_values.update({product_name='Pony')
+ obj.save()
+
+This pattern gets quite unwieldy as the number of fields in a model goes up.
+The above example can be rewritten using ``update__or_create()`` like so::
@timgraham

timgraham May 29, 2013

Owner

double underscore typo

+
+ obj, created = SalesRank.objects.update_or_create(produc_name='Pony,
+ defaults=updated_values)
+
+For detailed description how names passed in kwargs are resolved see ``get_or_create`` documentation.
@timgraham

timgraham May 29, 2013

Owner

:meth:get_or_create:
also wrap line

+
+
bulk_create
~~~~~~~~~~~
@@ -1585,36 +1628,6 @@ earliest
Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except
the direction is changed.
-first
@timgraham

timgraham May 29, 2013

Owner

shouldn't be removed

-~~~~~
-.. method:: first()
-
-.. versionadded:: 1.6
-
-Returns the first object matched by the queryset, or ``None`` if there
-is no matching object. If the ``QuerySet`` has no ordering defined, then the
-queryset is automatically ordered by the primary key.
-
-Example::
-
- p = Article.objects.order_by('title', 'pub_date').first()
-
-Note that ``first()`` is a convenience method, the following code sample is
-equivalent to the above example::
-
- try:
- p = Article.objects.order_by('title', 'pub_date')[0]
- except IndexError:
- p = None
-
-last
-~~~~
-.. method:: last()
-
-.. versionadded:: 1.6
-
-Works like :meth:`first()`, but returns the last object in the queryset.
-
aggregate
~~~~~~~~~
@@ -0,0 +1 @@
+
@@ -0,0 +1,31 @@
+"""
+update_or_create()
+
+``update_or_create()`` does what it says: it tries to look up an object with the
+given parameters and update If an object isn't found, it creates one with the given
+parameters.
+"""
+
+from __future__ import unicode_literals
+
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class SalesRank(models.Model):
+ product_name = models.CharField(max_length=100)
+ total_rank = models.IntegerField()
+ num_of_likes = models.IntegerField(default=0)
+
+ def __str__(self):
+ return self.product_name
+
+
+class ManualPrimaryKeyTest(models.Model):
+ id = models.IntegerField(primary_key=True)
+ data = models.CharField(max_length=100)
+
+
+class Product(models.Model):
+ sales_rank = models.ForeignKey(SalesRank, primary_key=True)
@@ -0,0 +1,79 @@
+from __future__ import absolute_import
+
+import traceback
+
+from django.db.utils import IntegrityError
+from django.test import TestCase, TransactionTestCase
+
+from .models import SalesRank, ManualPrimaryKeyTest, Product
+
+
+class UpdateOrCreateTests(TestCase):
+
+ def test_update(self):
+ SalesRank.objects.create(
+ product_name='Pony', total_rank=10,
+ )
+ p, created = SalesRank.objects.update_or_create(
+ product_name='Pony', defaults={
+ 'total_rank': 20
+ }
+ )
+ self.assertFalse(created)
+ self.assertEqual(p.product_name, 'Pony')
+ self.assertEqual(p.total_rank, 20)
+
+ def test_create(self):
+ p, created = SalesRank.objects.update_or_create(
+ product_name='Guitar', defaults={
+ 'total_rank': 1000
+ }
+ )
+ self.assertTrue(created)
+ self.assertEqual(p.product_name, 'Guitar')
+ self.assertEqual(p.total_rank, 1000)
+
+ def test_create_twice(self):
+ SalesRank.objects.update_or_create(product_name='Bicycle', total_rank=20)
+ # If we execute the exact same statement, it won't create a SalesRank.
+ p, created = SalesRank.objects.update_or_create(product_name='Bicycle', total_rank=20)
+ self.assertFalse(created)
+
+ def test_integrity(self):
+ # If you don't specify a value or default value for all required
+ # fields, you will get an error.
+ self.assertRaises(IntegrityError, SalesRank.objects.update_or_create, product_name='Car')
+
+ def test_mananual_primary_key_test(self):
+ # If you specify an existing primary key, but different other fields,
+ # then you will get an error and data will not be updated.
+ ManualPrimaryKeyTest.objects.create(id=1, data="Original")
+ self.assertRaises(IntegrityError,
+ ManualPrimaryKeyTest.objects.get_or_create, id=1, data="Different"
+ )
+ self.assertEqual(ManualPrimaryKeyTest.objects.get(id=1).data, "Original")
+
+ def test_error_contains_full_traceback(self):
+ # update_or_create should raise IntegrityErrors with the full traceback.
+ # This is tested by checking that a known method call is in the traceback.
+ # We cannot use assertRaises/assertRaises here because we need to inspect
+ # the actual traceback. Refs #16340.
+ try:
+ ManualPrimaryKeyTest.objects.get_or_create(id=1, data="Different")
+ except IntegrityError as e:
+ formatted_traceback = traceback.format_exc()
+ self.assertIn('obj.save', formatted_traceback)
+
+
+class GetOrCreateTransactionTests(TransactionTestCase):
+
+ def test_get_or_create_integrityerror(self):
+ # Regression test for #15117. Requires a TransactionTestCase on
+ # databases that delay integrity checks until the end of transactions,
+ # otherwise the exception is never raised.
+ try:
+ Product.objects.get_or_create(sales_rank=SalesRank(id=1))
+ except IntegrityError:
+ pass
+ else:
+ self.skipTest("This backend does not support integrity checks.")