#28704: update_or_create() calls select_for_update(), which locks database row
-------------------------------------+-------------------------------------
               Reporter:  Rafal      |          Owner:  nobody
  Radulski                           |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  1.11
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 An issue arises when update_or_create() is executed during a long-running
 transaction. It's caused by update_or_create() calling
 select_for_update(), which locks a row until the end of the entire
 transaction.

 {{{
 def update_or_create(self, defaults=None, **kwargs):
     defaults = defaults or {}
     lookup, params = self._extract_model_params(defaults, **kwargs)
     self._for_write = True
     with transaction.atomic(using=self.db):
         try:
             obj = self.select_for_update().get(**lookup)
         except self.model.DoesNotExist:
             obj, created = self._create_object_from_params(lookup, params)
             if created:
                 return obj, created
         for k, v in defaults.items():
             setattr(obj, k, v() if callable(v) else v)
         obj.save(using=self.db)
     return obj, False
 }}}


 Let's say that we are using a PostgreSQL database with "read committed"
 isolation level. There are two processes. First process starts a
 transaction. It updates and retrieves an existing model using
 update_or_create(). select_for_update() is called, which locks the row
 until the end of the transaction. Then, second process starts. It
 retrieves the same row using get() method. At this point, database query
 in second process is blocked until the end of transaction in first
 process.

 I believe this is a bug. update_or_create() should not call
 select_for_update(). Doing so can create a long-lived database lock, even
 when database transaction isolation level is relaxed.

 I don't have a possible solution. I believe that
 QuerySet.update_or_create() should call QuerySet.update(). Unfortunately,
 QuerySet.update() does not support generic relationships. If
 QuerySet.update() did support generic relationships, a solution could work
 as follows:

 {{{
 def update_or_create(self, defaults=None, **kwargs):
     defaults = defaults or {}
     lookup, params = self._extract_model_params(defaults, **kwargs)
     self._for_write = True
     with transaction.atomic(using=self.db):
         try:
             obj = self.only('pk').get(**lookup)
         except self.model.DoesNotExist:
             obj, created = self._create_object_from_params(lookup, params)
             if created:
                 return obj, created

         update_params = {k: v() if callable(v) else v for k, v in
 defaults.items()}
         self.filter(pk=obj.pk).update(**update_params)
         obj = self.get(pk=obj.pk)
     return obj, False
 }}}

 Related ticket:
 https://code.djangoproject.com/ticket/26804

-- 
Ticket URL: <https://code.djangoproject.com/ticket/28704>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/049.cf1acaf173ebae3b7f3fa5c7528cb12d%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to