49š
This is how I do optimistic locking in Django:
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
.update(updated_field=new_value, version=e.version+1)
if not updated:
raise ConcurrentModificationException()
The code listed above can be implemented as a method in Custom Manager.
I am making the following assumptions:
- filter().update() will result in a single database query because filter is lazy
- a database query is atomic
These assumptions are enough to ensure that no one else has updated the entry before. If multiple rows are updated this way you should use transactions.
WARNING Django Doc:
Be aware that the update() method is
converted directly to an SQL
statement. It is a bulk operation for
direct updates. It doesnāt run any
save() methods on your models, or emit
the pre_save or post_save signals
44š
This question is a bit old and my answer a bit late, but after what I understand this has been fixed in Django 1.4 using:
select_for_update(nowait=True)
see the docs
Returns a queryset that will lock rows until the end of the transaction, generating a SELECT ā¦ FOR UPDATE SQL statement on supported databases.
Usually, if another transaction has already acquired a lock on one of the selected rows, the query will block until the lock is released. If this is not the behavior you want, call select_for_update(nowait=True). This will make the call non-blocking. If a conflicting lock is already acquired by another transaction, DatabaseError will be raised when the queryset is evaluated.
Of course this will only work if the back-end support the āselect for updateā feature, which for example sqlite doesnāt. Unfortunately: nowait=True
is not supported by MySql, there you have to use: nowait=False
, which will only block until the lock is released.
- [Django]-Django 2.0 ā Not a valid view function or pattern name (Customizing Auth views)
- [Django]-What's the best solution for OpenID with Django?
- [Django]-Django: Calculate the Sum of the column values through query
31š
Actually, transactions donāt help you much here ā¦ unless you want to have transactions running over multiple HTTP requests (which you most probably donāt want).
What we usually use in those cases is āOptimistic Lockingā. The Django ORM doesnāt support that as far as I know. But there has been some discussion about adding this feature.
So you are on your own. Basically, what you should do is add a āversionā field to your model and pass it to the user as a hidden field. The normal cycle for an update is :
- read the data and show it to the user
- user modify data
- user post the data
- the app saves it back in the database.
To implement optimistic locking, when you save the data, you check if the version that you got back from the user is the same as the one in the database, and then update the database and increment the version. If they are not, it means that there has been a change since the data was loaded.
You can do that with a single SQL call with something like :
UPDATE ... WHERE version = 'version_from_user';
This call will update the database only if the version is still the same.
- [Django]-Django The 'image' attribute has no file associated with it
- [Django]-How to update fields in a model without creating a new record in django?
- [Django]-How to configure where to redirect after a log out in Django?
15š
Django 1.11 has three convenient options to handle this situation depending on your business logic requirements:
Something.objects.select_for_update()
will block until the model become freeSomething.objects.select_for_update(nowait=True)
and catchDatabaseError
if the model is currently locked for updateSomething.objects.select_for_update(skip_locked=True)
will not return the objects that are currently locked
In my application, which has both interactive and batch workflows on various models, I found these three options to solve most of my concurrent processing scenarios.
The āwaitingā select_for_update
is very convenient in sequential batch processes ā I want them all to execute, but let them take their time. The nowait
is used when an user wants to modify an object that is currently locked for update ā I will just tell them itās being modified at this moment.
The skip_locked
is useful for another type of update, when users can trigger a rescan of an object ā and I donāt care who triggers it, as long as itās triggered, so skip_locked
allows me to silently skip the duplicated triggers.
- [Django]-Django Forms: if not valid, show form with error message
- [Django]-Django error message "Add a related_name argument to the definition"
- [Django]-Convert seconds to hh:mm:ss in Python
3š
For future reference, check out https://github.com/RobCombs/django-locking. It does locking in a way that doesnāt leave everlasting locks, by a mixture of javascript unlocking when the user leaves the page, and lock timeouts (e.g. in case the userās browser crashes). The documentation is pretty complete.
- [Django]-Django: Error: You don't have permission to access that port
- [Django]-Django rest framework change primary key to use a unqiue field
- [Django]-Django ā Reverse for '' not found. '' is not a valid view function or pattern name
1š
You should probably use the django transaction middleware at least, even regardless of this problem.
As to your actual problem of having multiple users editing the same dataā¦ yes, use locking. OR:
Check what version a user is updating against (do this securely, so users canāt simply hack the system to say they were updating the latest copy!), and only update if that version is current. Otherwise, send the user back a new page with the original version they were editing, their submitted version, and the new version(s) written by others. Ask them to merge the changes into one, completely up-to-date version. You might try to auto-merge these using a toolset like diff+patch, but youāll need to have the manual merge method working for failure cases anyway, so start with that. Also, youāll need to preserve version history, and allow admins to revert changes, in case someone unintentionally or intentionally messes up the merge. But you should probably have that anyway.
Thereās very likely a django app/library that does most of this for you.
- [Django]-Django: How do I add arbitrary html attributes to input fields on a form?
- [Django]-Automatic creation date for Django model form objects
- [Django]-Get class name of django model
0š
Another thing to look for is the word āatomicā. An atomic operation means that your database change will either happen successfully, or fail obviously. A quick search shows this question asking about atomic operations in Django.
- [Django]-Django Rest Framework with ChoiceField
- [Django]-Django.contrib.gis.db.backends.postgis vs django.db.backends.postgresql_psycopg2
- [Django]-How to run own daemon processes with Django?
0š
The idea above
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
.update(updated_field=new_value, version=e.version+1)
if not updated:
raise ConcurrentModificationException()
looks great and should work fine even without serializable transactions.
The problem is how to augment the deafult .save() behavior as to not have to do manual plumbing to call the .update() method.
I looked at the Custom Manager idea.
My plan is to override the Manager _update method that is called by Model.save_base() to perform the update.
This is the current code in Django 1.3
def _update(self, values, **kwargs):
return self.get_query_set()._update(values, **kwargs)
What needs to be done IMHO is something like:
def _update(self, values, **kwargs):
#TODO Get version field value
v = self.get_version_field_value(values[0])
return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)
Similar thing needs to happen on delete. However delete is a bit more difficult as Django is implementing quite some voodoo in this area through django.db.models.deletion.Collector.
It is weird that modren tool like Django lacks guidance for Optimictic Concurency Control.
I will update this post when I solve the riddle. Hopefully solution will be in a nice pythonic way that does not involve tons of coding, weird views, skipping essential pieces of Django etc.
- [Django]-Django gives Bad Request (400) when DEBUG = False
- [Django]-How to assign items inside a Model object with Django?
- [Django]-Django order_by query set, ascending and descending
-3š
To be safe the database needs to support transactions.
If the fields is āfree-formā e.g. text etc. and you need to allow several users to be able to edit the same fields (you canāt have single user ownership to the data), you could store the original data in a variable.
When the user committs, check if the input data has changed from the original data (if not, you donāt need to bother the DB by rewriting old data),
if the original data compared to the current data in the db is the same you can save, if it has changed you can show the user the difference and ask the user what to do.
If the fields is numbers e.g. account balance, number of items in a store etc., you can handle it more automatically if you calculate the difference between the original value (stored when the user started filling out the form) and the new value you can start a transaction read the current value and add the difference, then end transaction. If you canāt have negative values, you should abort the transaction if the result is negative, and tell the user.
I donāt know django, so I canāt give you teh cod3s.. š
- [Django]-How to monkey patch Django?
- [Django]-Django limit_choices_to for multiple fields with "or" condition
- [Django]-Django: Catching Integrity Error and showing a customized message using template
-6š
From here:
How to prevent overwriting an object someone else has modified
Iām assuming that the timestamp will be held as a hidden field in the form youāre trying to save the details of.
def save(self):
if(self.id):
foo = Foo.objects.get(pk=self.id)
if(foo.timestamp > self.timestamp):
raise Exception, "trying to save outdated Foo"
super(Foo, self).save()
- [Django]-Django stops working with RuntimeError: populate() isn't reentrant
- [Django]-Strange PostgreSQL "value too long for type character varying(500)"
- [Django]-Django 1.5 custom User model error. "Manager isn't available; User has been swapped"