Джанго: IntegrityError во многих, многих добавить()

мы сталкиваемся с известной проблемой в Django:

IntegrityError во время многих ко многим добавить()

существует условие гонки, если несколько процессов / запросов пытаются добавить одну и ту же строку в ManyToManyRelation.

Как обойти это?

Envionment:

  • Джанго 1.9
  • Linux-Сервер
  • базы данных Postgres 9.3 (обновление может быть сделано, если необходимо)

подробности

Как воспроизвести это:

my_user.groups.add(foo_group)

выше не удается, если два запроса пытаются выполнить этот код сразу. Вот таблица базы данных и отсутствии ограничений:

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.

3 ответов


можно ли воспроизвести ошибку?

Да, давайте использовать знаменитый Publication и Article модели Django docs. Затем давайте создадим несколько потоков.

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()

вы обязательно увидите уникальные нарушения ключевых ограничений типа, указанного в сообщить об ошибке. Если вы их не видите, попробуйте увеличить количество потоков, или итераций.

есть ли обойти?

да. Использовать through модели и get_or_create. Вот models.py адаптировано из примера в документах django.

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')

вот новый класс threading, который является модификацией приведенного выше.

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

вы обнаружите, что исключение больше не отображается. Не стесняйтесь увеличивать количество итераций. Я только дошел до 1000 с get_or_create это не бросать исключение. Однако add() обычно выбрасывал исключение с 20 итерациями.

почему это работа?

, потому что get_or_create состоит из атомов.

этот метод является атомарным, предполагая правильное использование, правильную базу данных конфигурация и правильное поведение базовой базы данных. Однако если уникальность не применяется на уровне базы данных для кварги, используемые в вызове get_or_create (см. unique или unique_together), этот метод склонен к состоянию гонки, которое может привести к нескольким вставляемые строки с одинаковыми параметрами одновременно.

обновление: Спасибо @louis за указание на то, что сквозная модель может быть фактически устранена. Thuse в get_or_create на MyThread2 можно изменить.

ap , c = article.publications.through.objects.get_or_create(
            article=article, publication=pub)

Если вы готовы решить его в 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;

тогда он будет игнорировать дубликаты только новых вставок в таблице:

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=#

из того, что я вижу в предоставленном коде. Я считаю, что у вас есть ограничение на уникальность в парах (user_id, group_id) в группах. Так вот почему запуск 2 раза одного и того же запроса завершится неудачей, поскольку вы пытаетесь добавить 2 строки с одинаковыми user_id и group_id, первый из которых пройдет, но второй вызовет исключение.