Джанго: 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, первый из которых пройдет, но второй вызовет исключение.