From b55cde054ee7dd22f93c3522a8ddb1d04193bcac Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 20 Feb 2013 11:27:32 -0800 Subject: [PATCH] Added a db_constraint option to ForeignKeys. This controls whether or not a database level cosntraint is created. This is useful in a few specialized circumstances, but in general should not be used! --- django/db/backends/creation.py | 2 +- django/db/models/fields/related.py | 8 +++++--- docs/ref/models/fields.txt | 13 +++++++++++++ docs/releases/1.6.txt | 3 +++ tests/regressiontests/backends/models.py | 15 ++++++++++++++- tests/regressiontests/backends/tests.py | 24 +++++++++++++++++++++--- 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 0afe66ba1c..89ff1170dc 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -77,7 +77,7 @@ class BaseDatabaseCreation(object): tablespace, inline=True) if tablespace_sql: field_output.append(tablespace_sql) - if f.rel: + if f.rel and f.db_constraint: ref_output, pending = self.sql_for_inline_foreign_key_references( model, f, known_models, style) if pending: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index bd2e288410..804dda5817 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -981,9 +981,10 @@ class ForeignKey(RelatedField, Field): } description = _("Foreign Key (type determined by related field)") - def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, + db_constraint=True, **kwargs): try: - to_name = to._meta.model_name + to._meta.model_name except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: @@ -997,13 +998,14 @@ class ForeignKey(RelatedField, Field): if 'db_index' not in kwargs: kwargs['db_index'] = True + self.db_constraint = db_constraint kwargs['rel'] = rel_class(to, to_field, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), parent_link=kwargs.pop('parent_link', False), on_delete=kwargs.pop('on_delete', CASCADE), ) - Field.__init__(self, **kwargs) + super(ForeignKey, self).__init__(**kwargs) def get_path_info(self): """ diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 33ee05dd85..a4ae66d492 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1051,6 +1051,19 @@ define the details of how the relation works. The field on the related object that the relation is to. By default, Django uses the primary key of the related object. +.. attribute:: ForeignKey.db_constraint + + Controls whether or not a constraint should be created in the database for + this foreign key. The default is ``True``, and that's almost certainly what + you want; setting this to ``False`` can be very bad for data integrity. + That said, here are some scenarios where you might want to do this: + + * You have legacy data that is not valid. + * You're sharding your database. + + If you use this, accessing a related object that doesn't exist will raise + its ``DoesNotExist`` exception. + .. attribute:: ForeignKey.on_delete When an object referenced by a :class:`ForeignKey` is deleted, Django by diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 9594481b9f..8f1a4f375e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -85,6 +85,9 @@ Minor features :class:`~django.http.HttpResponsePermanentRedirect` now provide an ``url`` attribute (equivalent to the URL the response will redirect to). +* Added the :attr:`django.db.models.ForeignKey.db_constraint` + option. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index a92aa71e17..5876cbe52d 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -39,7 +39,7 @@ if connection.features.supports_long_model_names: verbose_name = 'model_with_long_table_name' primary_key_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.AutoField(primary_key=True) charfield_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.CharField(max_length=100) - m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person,blank=True) + m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person, blank=True) class Tag(models.Model): @@ -86,3 +86,16 @@ class Item(models.Model): def __str__(self): return self.name + + +@python_2_unicode_compatible +class Object(models.Model): + pass + + +@python_2_unicode_compatible +class ObjectReference(models.Model): + obj = models.ForeignKey(Object, db_constraint=False) + + def __str__(self): + return str(self.obj_id) diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index ed6f07691a..0e1700b36e 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -7,13 +7,12 @@ import threading from django.conf import settings from django.core.management.color import no_style -from django.core.exceptions import ImproperlyConfigured from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version -from django.db.models import fields, Sum, Avg, Variance, StdDev -from django.db.utils import ConnectionHandler, DatabaseError, load_backend +from django.db.models import Sum, Avg, Variance, StdDev +from django.db.utils import ConnectionHandler, DatabaseError from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) from django.test.utils import override_settings, str_prefix @@ -724,3 +723,22 @@ class MySQLPKZeroTests(TestCase): def test_zero_as_autoval(self): with self.assertRaises(ValueError): models.Square.objects.create(id=0, root=0, square=1) + + +class DBConstraintTestCase(TransactionTestCase): + def test_can_reference_existant(self): + obj = models.Object.objects.create() + ref = models.ObjectReference.objects.create(obj=obj) + self.assertEqual(ref.obj, obj) + + ref = models.ObjectReference.objects.get(obj=obj) + self.assertEqual(ref.obj, obj) + + def test_can_reference_non_existant(self): + self.assertFalse(models.Object.objects.filter(id=12345).exists()) + ref = models.ObjectReference.objects.create(obj_id=12345) + ref_new = models.ObjectReference.objects.get(obj_id=12345) + self.assertEqual(ref, ref_new) + + with self.assertRaises(models.Object.DoesNotExist): + ref.obj