Django:如何防止数据库条目的同时修改

如果有防止两个或多个用户同时修改相同数据库条目的方法?

向执行第二次提交/保存操作的用户显示错误消息是可以接受的,但数据不应该被默默覆盖。

我认为locking条目不是一个选项,因为用户可能会使用“返回”button或简单地closures他的浏览器,永远留下锁。

这是我如何在Django中进行乐观locking:

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() 

上面列出的代码可以作为自定义pipe理器中的方法实现。

我正在做出以下假设:

  • filter()。update()将导致单个数据库查询,因为filter是惰性的
  • 数据库查询是primefaces的

这些假设足以确保以前没有其他人更新过条目。 如果以这种方式更新多行,则应使用事务。

警告 Django文档 :

请注意,update()方法直接转换为SQL语句。 这是直接更新的批量操作。 它不会在模型上运行任何save()方法,或者发出pre_save或post_save信号

这个问题有点老了,我的答案有点晚,但是我知道这个问题已经在Django 1.4中使用了:

 select_for_update(nowait=True) 

看文档

返回将locking行直到事务结束的查询集,在受支持的数据库上生成SELECT … FOR UPDATE SQL语句。

通常,如果另一个事务已经获得了所选行之一的锁,则查询将被阻塞,直到锁被释放。 如果这不是您想要的行为,请调用select_for_update(nowait = True)。 这将使呼叫非阻塞。 如果另一个事务已经获得冲突的锁,那么当queryset被评估时,将引发DatabaseError。

当然,这只会在后端支持“select for update”function时才起作用,例如sqlite不支持。 不幸的是: nowait=True不被MySql支持,你必须使用: nowait=False ,它只会在锁被释放之前被阻塞。

事实上,事务在这里帮不了你的忙,除非你想让事务运行在多个HTTP请求上(你很可能不需要)。

我们通常在这些情况下使用的是“乐观locking”。 就我所知,Django ORM并不支持这一点。 但是有一些关于添加这个function的讨论。

所以你是自己的。 基本上,你应该做的是添加一个“版本”字段到你的模型,并将其作为隐藏字段传递给用户。 正常的更新周期是:

  1. 读取数据并将其显示给用户
  2. 用户修改数据
  3. 用户发布数据
  4. 该应用程序将其保存回数据库中。

为了实现乐观locking,在保存数据时,检查从用户处获得的版本是否与数据库中的版本相同,然后更新数据库并增加版本。 如果不是这样,则意味着数据加载后发生了变化。

您可以使用类似于以下方法的单个SQL调用来完成此操作:

 UPDATE ... WHERE version = 'version_from_user'; 

只有当版本仍然相同时,这个调用才会更新数据库。

Django 1.11有三个方便的选项来处理这种情况,这取决于您的业务逻辑要求:

  • Something.objects.select_for_update()会阻塞,直到模型变为空闲
  • Something.objects.select_for_update(nowait=True)并捕获模型当前被locking更新时的DatabaseError
  • Something.objects.select_for_update(skip_locked=True)不会返回当前被locking的对象

在我的应用程序中,在各种模型上都有交互式和批处理工作stream程,我发现这三个选项可以解决大部分并发处理场景。

“等待” select_for_update在顺序批处理过程中非常方便 – 我希望它们全部执行,但让他们花时间。 nowait用于当用户想要修改当前被locking更新的对象 – 我只是告诉他们现在正在修改它。

skip_locked对于另一种types的更新很有用,当用户可以触发一个对象的重新扫描 – 我不关心是谁触发它,只要它被触发,所以skip_locked允许我静静地跳过重复的触发器。

为了将来的参考,请查看https://github.com/RobCombs/django-locking 。 它以一种不会留下永久locking的方式进行locking,通过在用户离开页面时的javascript解锁混合以及locking超时(例如,在用户的浏览器崩溃的情况下)。 文档相当完整。

至less应该使用django事务中间件,即使不pipe这个问题。

至于你有多个用户编辑相同的数据的实际问题…是的,使用locking。 要么:

检查用户正在更新的版本(安全地执行此操作,因此用户不能简单地破解系统,说他们正在更新最新的副本!),并且只有当该版本是最新版本时才会更新。 否则,请向用户返回一个新的页面,其中包含他们正在编辑的原始版本,他们提交的版本以及他人编写的新版本。 要求他们将这些更改合并到一个完全更新的版本中。 你可能会尝试使用diff +补丁等工具集来自动合并这些文件,但是无论如何你都需要手动合并方法来处理失败的情况,所以从这里开始。 另外,您需要保留版本历史logging,并允许pipe理员恢复更改,以防有人无意或故意混淆合并。 但是,无论如何你应该可以拥有。

这很可能是一个django应用程序/库,它为你做了大部分的工作。

另一个要寻找的是“primefaces”这个词。 primefaces操作意味着您的数据库更改将成功发生,或者明显失败。 快速search显示这个问题在Django中询问primefaces操作。

上面的想法

 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() 

看起来不错,即使没有可序列化的交易也应该工作。

问题是如何增强deafult .save()行为,而不必手动pipe道调用.update()方法。

我看着定制经理的想法。

我的计划是覆盖由Model.save_base()调用的Manager _update方法来执行更新。

这是Django 1.3中的当前代码

 def _update(self, values, **kwargs): return self.get_query_set()._update(values, **kwargs) 

需要做什么恕我直言,就像这样:

 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) 

类似的事情需要发生删除。 但是,由于Django通过django.db.models.deletion.Collector在这个区域实现了相当多的巫术,删除有点困难。

奇怪的是,像Django这样的modren工具缺乏对Optimictic Concurency Control的指导。

当我解决谜题时,我会更新这个post。 希望解决scheme将是一个很好的pythonic方式,不涉及大量的编码,奇怪的观点,跳过Django的基本片断等。

为了安全起见,数据库需要支持事务 。

如果这些字段是“自由格式”(例如文本等),并且您需要允许多个用户能够编辑相同的字段(您不能对数据拥有单一用户所有权),则可以将原始数据存储在variables。 当用户提交时,检查input数据是否已经从原始数据改变(如果不是,则不需要通过重写旧数据来打扰DB),如果原始数据与db中的当前数据相比是相同的你可以保存,如果它已经改变了,你可以向用户展示不同之处,并询问用户该做什么。

如果这些字段是数字,例如账户余额,商店中的物品数量等,如果您计算原始值(用户开始填写表格时存储的)和新值(您可以开始一个交易读取当前值并添加差额,然后结束交易。 如果你不能有负值,你应该中止交易,如果结果是否定的,并告诉用户。

我不知道django,所以我不能给你的cod3s ..;)

从这里:
如何防止覆盖别人修改过的对象

我假设时间戳将被保存为您要保存细节的窗体中的隐藏字段。

 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()