问题描述
我们在Django中遇到了一个已知问题:
We run into a known issue in django:
如果多个进程/请求尝试将同一行添加到ManyToManyRelation中,则存在竞争条件。
There is a race condition if several processes/requests try to add the same row to a ManyToManyRelation.
如何解决此问题?
环境:
- Django 1.9
- Linux服务器
- Postgres 9.3(如有必要,可以进行更新)
如何复制它:
my_user.groups.add(foo_group)
如果两个请求尝试一次执行此代码,则上述失败。这是数据库表和失败的约束:
Above fails if two requests try to execute this code at once. Here is the database table and the failing constraint:
myapp_egs_d=> \d auth_user_groups
id | integer | not null default ...
user_id | integer | not null
group_id | integer | not null
Indexes:
"auth_user_groups_pkey" PRIMARY KEY, btree (id)
fails ==> "auth_user_groups_user_id_group_id_key" UNIQUE CONSTRAINT,
btree (user_id, group_id)
环境
由于这仅发生在生产机器上,并且在我的上下文中所有生产机器都运行postgres,因此仅使用postgres的解决方案将起作用。
Environment
Since this only happens on production machines, and all production machines in my context run postgres, a postgres only solution would work.
推荐答案
可以重现该错误吗?
是的,让我们使用著名的出版物$ c 。然后,让我们创建一些线程。
Can the error be reproduced?
Yes, let us use the famed Publication
and Article
models from Django docs. Then, let's create a few threads.
import threading
import random
def populate():
for i in range(100):
Article.objects.create(headline = 'headline{0}'.format(i))
Publication.objects.create(title = 'title{0}'.format(i))
print 'created objects'
class MyThread(threading.Thread):
def run(self):
for q in range(1,100):
for i in range(1,5):
pub = Publication.objects.all()[random.randint(1,2)]
for j in range(1,5):
article = Article.objects.all()[random.randint(1,15)]
pub.article_set.add(article)
print self.name
Article.objects.all().delete()
Publication.objects.all().delete()
populate()
thrd1 = MyThread()
thrd2 = MyThread()
thrd3 = MyThread()
thrd1.start()
thrd2.start()
thrd3.start()
您一定会看到违反唯一键约束的情况中报告的类型。如果看不到它们,请尝试增加线程数或迭代次数。
You are sure to see unique key constraint violations of the type reported in the bug report. If you don't see them, try increasing the number of threads or iterations.
是的。使用至
模型和 get_or_create
。这是从Django文档中的示例改编而成的models.py。
Yes. Use through
models and get_or_create
. Here is the models.py adapted from the example in the django docs.
class Publication(models.Model):
title = models.CharField(max_length=30)
def __str__(self): # __unicode__ on Python 2
return self.title
class Meta:
ordering = ('title',)
class Article(models.Model):
headline = models.CharField(max_length=100)
publications = models.ManyToManyField(Publication, through='ArticlePublication')
def __str__(self): # __unicode__ on Python 2
return self.headline
class Meta:
ordering = ('headline',)
class ArticlePublication(models.Model):
article = models.ForeignKey('Article', on_delete=models.CASCADE)
publication = models.ForeignKey('Publication', on_delete=models.CASCADE)
class Meta:
unique_together = ('article','publication')
这里是新的线程处理类,是对上面的类的修改。
Here is the new threading class which is a modification of the one above.
class MyThread2(threading.Thread):
def run(self):
for q in range(1,100):
for i in range(1,5):
pub = Publication.objects.all()[random.randint(1,2)]
for j in range(1,5):
article = Article.objects.all()[random.randint(1,15)]
ap , c = ArticlePublication.objects.get_or_create(article=article, publication=pub)
print 'Get or create', self.name
您将发现该异常不再显示。随意增加迭代次数。我只用 get_or_create
达到了1000,它没有抛出异常。但是 add()
通常会在20次迭代中引发异常。
You will find that the exception no longer shows up. Feel free to increase the number of iterations. I only went up to a 1000 with get_or_create
it didn't throw the exception. However add()
usually threw an exception with in 20 iterations.
因为是原子的。
更新:
感谢@louis指出实际上可以消除直通模型。因此,可以将 MyThread2
中的 get_or_create
更改为。
ap , c = article.publications.through.objects.get_or_create(
article=article, publication=pub)
这篇关于Django:多对多add()期间发生IntegrityError的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!