#28010: select_for_update() is too eager in presence of joins
-------------------------------------+-------------------------------------
Reporter: Ran | Owner: nobody
Benita |
Type: New | Status: new
feature |
Component: Database | Version: 1.10
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 |
-------------------------------------+-------------------------------------
Using `select_for_update()` on a queryset appends ` FOR UPDATE` to the
query. If the query joins other tables, the effect is that all tables are
locked. I believe this is not what most people using `select_for_update()`
want, instead they only want to lock the row from the main table.
== Example
Consider a scenario like this:
{{{#!python
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.PositiveIntegerField()
class Order(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT)
status = models.CharField(max_length=100, choices=(
('NEW', 'New'),
('APPROVED', 'Approved'),
('CANCELED', 'Canceled'),
)
...
@classmethod
def approve(cls, id):
with transaction.atomic():
order =
Order.objects.select_related('product').select_for_update().get(pk=id)
# ...Check order.product.price & stuff...
order.status = 'APPROVED'
order.save()
@classmethod
def cancel(cls, id):
with transaction.atomic():
# Similar...
}}}
Here, locking is needed in order to serialize `approve()` and `cancel()`
on the same Order, to avoid race conditions.
I have also used `select_related` in `approve()`, in order to avoid an
extra query. Potentially, there are many related objects, but to keep the
example simple I added just one.
The interaction between `select_related` and `select_for_update` has the
unintended consequence (IMO) of also locking the related `Product` row.
Hence, if there is some concurrent task which updates product prices for
example, it can cause conflicts, slowdowns, deadlocks, and other bad
things.
The fix is to remove the `select_related`. But then extra queries are
needed for each related object access without a reasonable workaround (I
think?).
== Possible solutions
PostgreSQL (which I am using) has an option to specify exactly which
tables are to be locked, instead of locking all tables in the query:
https://www.postgresql.org/docs/9.6/static/sql-select.html#SQL-FOR-UPDATE-
SHARE. The syntax is `SELECT ... FOR UPDATE OF orders_order`.
Oracle also supports this with similar syntax, however it allows
specifying which *columns* to lock:
http://docs.oracle.com/javadb/10.8.3.0/ref/rrefsqlj31783.html. I guess
there it is possible to lock specific columns of specific rows, while
postgres only locks full rows.
I am not familiar with MySQL but it doesn't seem like it supports refining
the `FOR UPDATE`.
Here are some possible solutions I can think of:
1. Add support for `select_for_update` to specify which models to lock,
e.g. `select_for_update('self', 'product')`. This will use the `OF`
syntax.
2. Only support the `select_for_update('self')` behavior, and make it
implicit, i.e. `select_for_update` only ever locks the main model of the
QuerySet.
3. Add a way to do `select_related` on an already-existing object,
something like a standalone `select_related_objects` after the existing
`prefetch_related_objects` function:
https://docs.djangoproject.com/en/1.10/ref/models/querysets/#prefetch-
related-objects, and keep `select_for_update` as-is. Then it should at
least be possible to do one extra query instead of one per related object.
Thanks for reading this far :)
--
Ticket URL: <https://code.djangoproject.com/ticket/28010>
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/051.62779c5a3f59a901e217afc382507673%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.