Commit 6272d2f1 authored by Karol Sikora's avatar Karol Sikora Committed by Tim Graham
Browse files

Fixed #20429 -- Added QuerySet.update_or_create

Thanks tunixman for the suggestion and Loic Bistuer for the review.
parent 66f3d57b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -530,6 +530,7 @@ answer newbie questions, and generally made Django that much better:
    Leo Shklovskii
    jason.sidabras@gmail.com
    Mikołaj Siedlarek <mikolaj.siedlarek@gmail.com>
    Karol Sikora <elektrrrus@gmail.com>
    Brenton Simpson <http://theillustratedlife.com>
    Jozko Skrablin <jozko.skrablin@gmail.com>
    Ben Slavin <benjamin.slavin@gmail.com>
+3 −0
Original line number Diff line number Diff line
@@ -154,6 +154,9 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
    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)

+68 −21
Original line number Diff line number Diff line
@@ -364,24 +364,51 @@ class QuerySet(object):

        return objs

    def get_or_create(self, **kwargs):
    def get_or_create(self, defaults=None, **kwargs):
        """
        Looks up an object with the given kwargs, creating one if necessary.
        Returns a tuple of (object, created), where created is a boolean
        specifying whether an object was created.
        """
        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)
        lookup, params, _ = self._extract_model_params(defaults, **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, defaults=None, **kwargs):
        """
        Looks up an object with the given kwargs, updating one with defaults
        if it exists, otherwise creates a new one.
        Returns a tuple (object, created), where created is a boolean
        specifying whether an object was created.
        """
        lookup, params, filtered_defaults = self._extract_model_params(defaults, **kwargs)
        try:
            self._for_write = True
            obj = self.get(**lookup)
        except self.model.DoesNotExist:
            obj, created = self._create_object_from_params(lookup, params)
            if created:
                return obj, created
        for k, v in six.iteritems(filtered_defaults):
            setattr(obj, k, v)
        try:
            sid = transaction.savepoint(using=self.db)
            obj.save(update_fields=filtered_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 create an object using passed params.
        Used by get_or_create and update_or_create
        """
        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)
@@ -396,6 +423,26 @@ class QuerySet(object):
                # Re-raise the DatabaseError with its original traceback.
                six.reraise(*exc_info)

    def _extract_model_params(self, defaults, **kwargs):
        """
        Prepares `lookup` (kwargs that are valid model attributes), `params`
        (for creating a model instance) and `filtered_defaults` (defaults
        that are valid model attributes) based on given kwargs; for use by
        get_or_create and update_or_create.
        """
        defaults = defaults or {}
        filtered_defaults = {}
        lookup = kwargs.copy()
        for f in self.model._meta.fields:
            # Filter out fields that don't belongs to the model.
            if f.attname in lookup:
                lookup[f.name] = lookup.pop(f.attname)
            if f.attname in defaults:
                filtered_defaults[f.name] = defaults.pop(f.attname)
        params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k)
        params.update(filtered_defaults)
        return lookup, params, filtered_defaults

    def _earliest_or_latest(self, field_name=None, direction="-"):
        """
        Returns the latest object, according to the model's
+44 −2
Original line number Diff line number Diff line
@@ -1330,7 +1330,7 @@ prepared to handle the exception if you are using manual primary keys.
get_or_create
~~~~~~~~~~~~~

.. method:: get_or_create(**kwargs)
.. method:: get_or_create(defaults=None, **kwargs)

A convenience method for looking up an object with the given ``kwargs`` (may be
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
tuple of the new object and ``True``. The new object will be created roughly
according to this algorithm::

    defaults = kwargs.pop('defaults', {})
    params = dict([(k, v) for k, v in kwargs.items() if '__' not in k])
    params.update(defaults)
    obj = self.model(**params)
@@ -1447,6 +1446,49 @@ in the HTTP spec.
  chapter because it isn't related to that book, but it can't create it either
  because ``title`` field should be unique.

update_or_create
~~~~~~~~~~~~~~~~

.. method:: update_or_create(defaults=None, **kwargs)

.. versionadded:: 1.7

A convenience method for updating an object with the given ``kwargs``, creating
a new one if necessary. The ``defaults`` is a dictionary of (field, value)
pairs used to update the object.

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 ``update_or_create`` method tries to fetch an object from database based on
the given ``kwargs``. If a match is found, it updates the fields passed in the
``defaults`` dictionary.

This is meant as a shortcut to boilerplatish code. For example::

    try:
        obj = Person.objects.get(first_name='John', last_name='Lennon')
        for key, value in updated_values.iteritems():
            setattr(obj, key, value)
        obj.save()
    except Person.DoesNotExist:
        updated_values.update({'first_name': 'John', 'last_name': 'Lennon'})
        obj = Person(**updated_values)
        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::

    obj, created = Person.objects.update_or_create(
        first_name='John', last_name='Lennon', defaults=updated_values)

For detailed description how names passed in ``kwargs`` are resolved see
:meth:`get_or_create`.

As described above in :meth:`get_or_create`, this method is prone to a
race-condition which can result in multiple rows being inserted simultaneously
if uniqueness is not enforced at the database level.

bulk_create
~~~~~~~~~~~
+3 −0
Original line number Diff line number Diff line
@@ -41,6 +41,9 @@ Minor features
* The ``enter`` argument was added to the
  :data:`~django.test.signals.setting_changed` signal.

* The :meth:`QuerySet.update_or_create()
  <django.db.models.query.QuerySet.update_or_create>` method was added.

Backwards incompatible changes in 1.7
=====================================

Loading