diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 9677b321c6..2dec00afc1 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -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 +.. 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") + (, True) + >>> book.chapters.get_or_create(title="Telemachus") + (, False) + >>> Chapter.objects.create(title="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 ~~~~~~~~~~~ diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 82905de4f8..2f21344f59 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -28,3 +28,12 @@ class ManualPrimaryKeyTest(models.Model): class Profile(models.Model): 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) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index e9cce9bbde..5117a2f915 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -6,7 +6,7 @@ import traceback from django.db import IntegrityError from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest, Profile +from .models import Person, ManualPrimaryKeyTest, Profile, Tag, Thing class GetOrCreateTests(TestCase): @@ -77,3 +77,28 @@ class GetOrCreateTransactionTests(TransactionTestCase): pass else: 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')