Django:多对多 add() 期间的 IntegrityError
Django: IntegrityError during Many To Many add()
我们 运行 进入 django 中的一个已知问题:
在多对多 add() 期间出现 IntegrityError
如果多个 processes/requests 尝试将同一行添加到 ManyToManyRelation 中,则会出现竞争条件。
如何解决这个问题?
环境:
- Django 1.9
- Linux 服务器
- Postgres 9.3(如有必要,可以进行更新)
详情
如何复制:
my_user.groups.add(foo_group)
如果有两个请求同时尝试执行这段代码,上面的代码就会失败。这是数据库 table 和失败的约束:
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 的解决方案会起作用。
根据我在提供的代码中看到的内容。我相信您对组中成对 (user_id、group_id) 的唯一性有约束。所以这就是为什么 运行 两次相同的查询将失败,因为您试图添加具有相同 user_id 和 group_id 的 2 行,第一个执行的将通过,但第二个将引发异常。
如果您准备好在 PostgreSQL 中解决它,您可以在 psql
中执行以下操作:
-- Create a RULE and function to intercept all INSERT attempts to the table and perform a check whether row exists:
CREATE RULE auth_user_group_ins AS
ON INSERT TO auth_user_groups
WHERE (EXISTS (SELECT 1
FROM auth_user_groups
WHERE user_id=NEW.user_id AND group_id=NEW.group_id))
DO INSTEAD NOTHING;
然后它将忽略重复项,仅在 table:
中插入新内容
db=# TRUNCATE auth_user_groups;
TRUNCATE TABLE
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 1 -- added
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 0 -- no insert no error
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,2);
INSERT 0 1 -- added
db=# SELECT * FROM auth_user_groups; -- check
id | user_id | group_id
----+---------+----------
14 | 1 | 1
16 | 1 | 2
(2 rows)
db=#
能否重现错误?
是的,让我们使用来自 Django docs 的著名 Publication
和 Article
模型。然后,让我们创建几个线程。
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()
您肯定会看到 bug report 中报告的类型的唯一键约束违规。如果您没有看到它们,请尝试增加线程数或迭代数。
有解决办法吗?
是的。使用 through
模型和 get_or_create
。这是根据 django 文档中的示例改编的 models.py。
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')
这是新线程 class,它是对上述线程的修改。
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 次迭代中抛出异常。
为什么这样做?
因为get_or_create是原子的。
This method is atomic assuming correct usage, correct database
configuration, and correct behavior of the underlying database.
However, if uniqueness is not enforced at the database level for the
kwargs used in a get_or_create call (see unique or unique_together),
this method is prone to a race-condition which can result in multiple
rows with the same parameters being inserted simultaneously.
更新:
感谢@louis 指出直通模型实际上可以被淘汰。因此 MyThread2
中的 get_or_create
可以更改为.
ap , c = article.publications.through.objects.get_or_create(
article=article, publication=pub)
我们 运行 进入 django 中的一个已知问题:
在多对多 add() 期间出现 IntegrityError
如果多个 processes/requests 尝试将同一行添加到 ManyToManyRelation 中,则会出现竞争条件。
如何解决这个问题?
环境:
- Django 1.9
- Linux 服务器
- Postgres 9.3(如有必要,可以进行更新)
详情
如何复制:
my_user.groups.add(foo_group)
如果有两个请求同时尝试执行这段代码,上面的代码就会失败。这是数据库 table 和失败的约束:
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 的解决方案会起作用。
根据我在提供的代码中看到的内容。我相信您对组中成对 (user_id、group_id) 的唯一性有约束。所以这就是为什么 运行 两次相同的查询将失败,因为您试图添加具有相同 user_id 和 group_id 的 2 行,第一个执行的将通过,但第二个将引发异常。
如果您准备好在 PostgreSQL 中解决它,您可以在 psql
中执行以下操作:
-- Create a RULE and function to intercept all INSERT attempts to the table and perform a check whether row exists:
CREATE RULE auth_user_group_ins AS
ON INSERT TO auth_user_groups
WHERE (EXISTS (SELECT 1
FROM auth_user_groups
WHERE user_id=NEW.user_id AND group_id=NEW.group_id))
DO INSTEAD NOTHING;
然后它将忽略重复项,仅在 table:
中插入新内容db=# TRUNCATE auth_user_groups;
TRUNCATE TABLE
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 1 -- added
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 0 -- no insert no error
db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,2);
INSERT 0 1 -- added
db=# SELECT * FROM auth_user_groups; -- check
id | user_id | group_id
----+---------+----------
14 | 1 | 1
16 | 1 | 2
(2 rows)
db=#
能否重现错误?
是的,让我们使用来自 Django docs 的著名 Publication
和 Article
模型。然后,让我们创建几个线程。
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()
您肯定会看到 bug report 中报告的类型的唯一键约束违规。如果您没有看到它们,请尝试增加线程数或迭代数。
有解决办法吗?
是的。使用 through
模型和 get_or_create
。这是根据 django 文档中的示例改编的 models.py。
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')
这是新线程 class,它是对上述线程的修改。
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 次迭代中抛出异常。
为什么这样做?
因为get_or_create是原子的。
This method is atomic assuming correct usage, correct database configuration, and correct behavior of the underlying database. However, if uniqueness is not enforced at the database level for the kwargs used in a get_or_create call (see unique or unique_together), this method is prone to a race-condition which can result in multiple rows with the same parameters being inserted simultaneously.
更新:
感谢@louis 指出直通模型实际上可以被淘汰。因此 MyThread2
中的 get_or_create
可以更改为.
ap , c = article.publications.through.objects.get_or_create(
article=article, publication=pub)