Merge pull request #1164 from pyriku/get_or_create_through_m2m

Integrity problems when using get_or_create through M2M
This commit is contained in:
Aymeric Augustin 2013-05-19 05:21:39 -07:00
commit c28b795804
3 changed files with 70 additions and 1 deletions

View File

@ -1409,6 +1409,41 @@ has a side effect on your data. For more, see `Safe methods`_ in the HTTP spec.
.. _Safe methods: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 .. _Safe methods: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
.. warning::
You can use ``get_or_create()`` through :class:`~django.db.models.ManyToManyField`
attributes and reverse relations. In that case you will restrict the queries
inside the context of that relation. That could lead you to some integrity
problems if you don't use it consistently.
Being the following models::
class Chapter(models.Model):
title = models.CharField(max_length=255, unique=True)
class Book(models.Model):
title = models.CharField(max_length=256)
chapters = models.ManyToManyField(Chapter)
You can use ``get_or_create()`` through Book's chapters field, but it only
fetches inside the context of that book::
>>> book = Book.objects.create(title="Ulysses")
>>> book.chapters.get_or_create(title="Telemachus")
(<Chapter: Telemachus>, True)
>>> book.chapters.get_or_create(title="Telemachus")
(<Chapter: Telemachus>, False)
>>> Chapter.objects.create(title="Chapter 1")
<Chapter: Chapter 1>
>>> book.chapters.get_or_create(title="Chapter 1")
# Raises IntegrityError
This is happening because it's trying to get or create "Chapter 1" through the
book "Ulysses", but it can't do any of them: the relation can't fetch that
chapter because it isn't related to that book, but it can't create it either
because ``title`` field should be unique.
bulk_create bulk_create
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -28,3 +28,12 @@ class ManualPrimaryKeyTest(models.Model):
class Profile(models.Model): class Profile(models.Model):
person = models.ForeignKey(Person, primary_key=True) person = models.ForeignKey(Person, primary_key=True)
class Tag(models.Model):
text = models.CharField(max_length=256, unique=True)
class Thing(models.Model):
name = models.CharField(max_length=256)
tags = models.ManyToManyField(Tag)

View File

@ -6,7 +6,7 @@ import traceback
from django.db import IntegrityError from django.db import IntegrityError
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from .models import Person, ManualPrimaryKeyTest, Profile from .models import Person, ManualPrimaryKeyTest, Profile, Tag, Thing
class GetOrCreateTests(TestCase): class GetOrCreateTests(TestCase):
@ -77,3 +77,28 @@ class GetOrCreateTransactionTests(TransactionTestCase):
pass pass
else: else:
self.skipTest("This backend does not support integrity checks.") self.skipTest("This backend does not support integrity checks.")
class GetOrCreateThroughManyToMany(TestCase):
def test_get_get_or_create(self):
tag = Tag.objects.create(text='foo')
a_thing = Thing.objects.create(name='a')
a_thing.tags.add(tag)
obj, created = a_thing.tags.get_or_create(text='foo')
self.assertFalse(created)
self.assertEqual(obj.pk, tag.pk)
def test_create_get_or_create(self):
a_thing = Thing.objects.create(name='a')
obj, created = a_thing.tags.get_or_create(text='foo')
self.assertTrue(created)
self.assertEqual(obj.text, 'foo')
self.assertIn(obj, a_thing.tags.all())
def test_something(self):
Tag.objects.create(text='foo')
a_thing = Thing.objects.create(name='a')
self.assertRaises(IntegrityError, a_thing.tags.get_or_create, text='foo')