是否存在防止两个或多个用户同时修改同一数据库条目的方法?
向用户显示错误消息以执行第二次提交/保存操作是可以接受的,但是数据不应被静默覆盖。
我认为锁定条目不是一种选择,因为用户可能会使用“后退”按钮或只是关闭浏览器,从而永远保持锁定状态。
Answers:
这就是我在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信号
filter
都收到未经修改的相同列表e
,然后又并发调用时会发生update
什么?我看不到阻止同时过滤和更新的信号灯。编辑:哦,我现在了解惰性过滤器。但是,假设update()是原子的,这有什么用呢?数据库肯定会处理并发访问
这个问题有点老了,我的回答有点晚了,但是在我理解之后,在Django 1.4中使用以下命令解决了这个问题:
select_for_update(nowait=True)
看文档
返回一个查询集,该查询集将锁定行直到事务结束,从而在支持的数据库上生成SELECT ... FOR UPDATE SQL语句。
通常,如果另一个事务已经获得了对所选行之一的锁定,则查询将阻塞,直到释放该锁定为止。如果这不是您想要的行为,请调用select_for_update(nowait = True)。这将使呼叫成为非阻塞。如果另一个事务已经获取了冲突的锁,则在评估查询集时将引发DatabaseError。
当然,这仅在后端支持“选择更新”功能(例如sqlite不支持)时才有效。不幸的是:nowait=True
MySql不支持:您必须在其中使用:nowait=False
,它只会在释放锁之前阻塞。
实际上,事务在这里无济于事...除非您希望使事务运行在多个HTTP请求上(您可能不希望这样做)。
在这些情况下,我们通常使用的是“乐观锁定”。据我所知,Django ORM不支持该功能。但是,已经有一些关于添加此功能的讨论。
所以你自己一个人。基本上,您应该做的是在模型中添加一个“版本”字段,并将其作为隐藏字段传递给用户。更新的正常周期为:
为了实现乐观锁定,在保存数据时,您需要检查从用户那里获得的版本是否与数据库中的版本相同,然后更新数据库并递增版本。如果不是,则表示自加载数据以来发生了更改。
您可以通过单个SQL调用来实现,例如:
UPDATE ... WHERE version = 'version_from_user';
仅当版本相同时,此调用才会更新数据库。
Django 1.11提供了三个方便的选项来处理这种情况,具体取决于您的业务逻辑要求:
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
让我静静地跳过重复的触发器。
为了将来参考,请查看https://github.com/RobCombs/django-locking。它以不会留下永久锁定的方式进行锁定,这是由用户离开页面时JavaScript解锁和锁定超时(例如,如果用户的浏览器崩溃)混合而成的。该文档非常完整。
即使不考虑此问题,您也可能至少应使用django事务中间件。
至于让多个用户编辑相同数据的实际问题,是的,请使用锁定。要么:
检查用户要针对哪个版本进行更新(安全地执行此操作,因此用户不能简单地破解系统以说他们正在更新最新副本!),并且仅在该版本为当前版本时才进行更新。否则,向用户发送一个新页面,其中包含他们正在编辑的原始版本,提交的版本以及其他人编写的新版本。要求他们将更改合并到一个完全最新的版本中。您可能会尝试使用diff + patch之类的工具集自动合并这些文件,但是无论如何,您都需要使手动合并方法适用于失败案例,因此从此开始。此外,您将需要保留版本历史记录,并允许管理员还原更改,以防有人无意或无意地破坏了合并。但是无论如何,您可能都应该拥有它。
django应用程序/库很可能为您完成了大部分工作。
要寻找的另一件事是“原子”一词。原子操作意味着您的数据库更改将成功发生或明显失败。快速搜索显示此问题,询问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()
看起来不错,即使没有可序列化的事务也可以正常工作。
问题在于如何增强默认的.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这样的现代工具缺少有关光学并发控制的指导。
解决谜题时,我将更新此帖子。希望解决方案将采用一种不错的pythonic方式,而不涉及大量的编码,怪异的视图,跳过Django的重要部分等。
为了安全起见,数据库需要支持事务。
如果字段是“自由格式”(例如文本等),并且您需要允许多个用户能够编辑相同的字段(您不能拥有数据的单个用户所有权),则可以将原始数据存储在变量。当用户提交时,检查输入数据是否已从原始数据更改(如果没有更改,则无需通过重写旧数据来打扰数据库),如果原始数据与数据库中的当前数据相比是相同的您可以保存,如果已更改,则可以向用户显示差异并询问用户该怎么做。
如果这些字段是数字,例如帐户余额,商店中的项目数等,则可以计算原始值(用户开始填写表格时存储的值)与新值之间的差,从而可以更自动地处理开始交易时,读取当前值并加上差额,然后结束交易。如果不能使用负值,则如果结果为负,则应中止事务,并告知用户。
我不知道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()