Django:多对多 add() 期间的 IntegrityError

Django: IntegrityError during Many To Many add()

我们 运行 进入 django 中的一个已知问题:

在多对多 add() 期间出现 IntegrityError

如果多个 processes/requests 尝试将同一行添加到 ManyToManyRelation 中,则会出现竞争条件。

如何解决这个问题?

环境:

详情

如何复制:

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 的著名 PublicationArticle 模型。然后,让我们创建几个线程。

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)