是否有办法防止两个或更多用户同时修改同一数据库条目?
向执行第二次提交/保存操作的用户显示错误消息是可以接受的,但不应以静默方式覆盖数据。
我认为锁定条目不是一个选项,因为用户可能会使用“后退”按钮或只是关闭他的浏览器,永远保持锁定。
答案 0 :(得分:46)
这就是我在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()
上面列出的代码可以作为Custom Manager中的方法实现。
我做出以下假设:
这些假设足以确保之前没有其他人更新过该条目。如果以这种方式更新多行,则应使用事务。
警告 Django Doc:
请注意update()方法是 直接转换为SQL 声明。这是一个批量操作 直接更新。它没有运行任何 模型上的save()方法或发射 pre_save或post_save信号
答案 1 :(得分:35)
这个问题有点陈旧,我的回答有点晚了,但据我所知,这已经在Django 1.4 中修复了使用:
select_for_update(nowait=True)
请参阅docs
返回一个查询集,该查询集将锁定行直到事务结束,在支持的数据库上生成SELECT ... FOR UPDATE SQL语句。
通常,如果另一个事务已经对其中一个选定行获取了锁定,则查询将一直阻塞,直到锁定被释放。如果这不是您想要的行为,请调用select_for_update(nowait = True)。这将使呼叫无阻塞。如果另一个事务已经获取了冲突的锁,则在评估查询集时将引发DatabaseError。
当然,这仅在后端支持“select for update”功能时才有效,例如sqlite不支持。不幸的是:MySql不支持nowait=True
,您必须使用nowait=False
,它只会在锁被释放之前阻止。
答案 2 :(得分:28)
实际上,事务在这里对你没什么帮助...除非你想让事务在多个HTTP请求上运行(你很可能不想要)。
我们在这些情况下通常使用的是“乐观锁定”。据我所知,Django ORM不支持这一点。但是有一些关于添加此功能的讨论。
所以你独自一人。基本上,您应该做的是在模型中添加“版本”字段,并将其作为隐藏字段传递给用户。更新的正常周期是:
要实现乐观锁定,在保存数据时,检查从用户返回的版本是否与数据库中的版本相同,然后更新数据库并增加版本。如果不是,则表示自加载数据后发生了变化。
您可以通过单个SQL调用执行此操作,例如:
UPDATE ... WHERE version = 'version_from_user';
仅当版本仍然相同时,此调用才会更新数据库。
答案 3 :(得分:12)
Django 1.11有three convenient options来处理这种情况,具体取决于您的业务逻辑要求:
Something.objects.select_for_update()
将阻止该模型免费Something.objects.select_for_update(nowait=True)
并捕获DatabaseError
Something.objects.select_for_update(skip_locked=True)
不会返回当前锁定的对象在我的应用程序中,它在各种模型上同时具有交互式和批处理工作流程,我发现这三个选项可以解决大多数并发处理方案。
"等待" select_for_update
在顺序批处理过程中非常方便 - 我希望它们全部执行,但让他们花时间。当用户想要修改当前被锁定以进行更新的对象时,使用nowait
- 我将告诉他们此时正在修改它。
skip_locked
对于其他类型的更新很有用,当用户可以触发对象的重新扫描时 - 我不在乎是谁触发它,只要它被触发,所以skip_locked
允许我默默地跳过重复的触发器。
答案 4 :(得分:3)
为了将来参考,请查看https://github.com/RobCombs/django-locking。它以一种不会留下永久锁定的方式锁定,通过用户离开页面时javascript解锁的混合,以及锁定超时(例如,在用户的浏览器崩溃的情况下)。文档非常完整。
答案 5 :(得分:1)
你应该至少使用django事务中间件,即使不管这个问题。
关于让多个用户编辑相同数据的实际问题...是的,请使用锁定。 OR:
检查用户正在更新的版本(安全地执行此操作,因此用户不能简单地破解系统以说他们正在更新最新版本!),并且只有在该版本是最新版本时才会更新。否则,向用户返回一个新页面,其中包含他们正在编辑的原始版本,他们提交的版本以及其他人编写的新版本。要求他们将更改合并为一个完全最新的版本。您可能尝试使用像diff + patch这样的工具集自动合并它们,但无论如何您都需要使用手动合并方法来处理故障情况,所以从此开始。此外,您需要保留版本历史记录,并允许管理员还原更改,以防有人无意或故意搞砸合并。但无论如何你应该有这个。
很可能有一个django app / library为你做了大部分工作。
答案 6 :(得分:0)
要寻找的另一件事是“原子”这个词。原子操作意味着您的数据库更改将成功发生,或者显然会失败。快速搜索显示this question询问Django中的原子操作。
答案 7 :(得分:0)
上面的想法
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()行为,而不必手动管道来调用.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的指导,这很奇怪。
当我解开谜语时,我会更新这篇文章。希望解决方案将以一种不错的pythonic方式,不涉及大量的编码,奇怪的视图,跳过Django等基本部分。
答案 8 :(得分:-2)
为了安全起见,数据库需要支持transactions。
如果字段是“自由格式”,例如文本等,您需要允许多个用户能够编辑相同的字段(您不能拥有单个用户对数据的所有权),您可以将原始数据存储在变量中。 当用户提交时,检查输入数据是否已从原始数据更改(如果没有,则不需要通过重写旧数据来打扰数据库), 如果原始数据与db中的当前数据相比是可以保存的,如果已经更改,则可以向用户显示差异并询问用户该做什么。
如果字段是数字,例如帐户余额,商店中的商品数量等,如果您计算原始值(用户开始填写表格时存储的)与您可以开始交易的新值之间的差异,您可以更自动地处理它值并添加差异,然后结束交易。如果您不能有负值,则应该在结果为否定时中止事务,并告诉用户。
我不知道django,所以我不能给你teh cod3s ..;)
答案 9 :(得分:-6)
从这里:
How to prevent overwriting an object someone else has modified
我假设时间戳将作为隐藏字段保存在您尝试保存详细信息的表单中。
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()