Loading django/db/models/fields/related.py +11 −6 Original line number Diff line number Diff line Loading @@ -955,7 +955,9 @@ class OneToOneRel(ManyToOneRel): class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): symmetrical=True, through=None, db_constraint=True): if through and not db_constraint: raise ValueError("Can't supply a through model and db_constraint=False") self.to = to self.related_name = related_name if limit_choices_to is None: Loading @@ -964,6 +966,7 @@ class ManyToManyRel(object): self.symmetrical = symmetrical self.multiple = True self.through = through self.db_constraint = db_constraint def is_hidden(self): "Should the related object be hidden?" Loading Loading @@ -1196,15 +1199,15 @@ def create_many_to_many_intermediary_model(field, klass): return type(name, (models.Model,), { 'Meta': meta, '__module__': klass.__module__, from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace), to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace, db_constraint=field.rel.db_constraint), to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace, db_constraint=field.rel.db_constraint) }) class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") def __init__(self, to, **kwargs): def __init__(self, to, db_constraint=True, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT Loading @@ -1219,13 +1222,15 @@ class ManyToManyField(RelatedField, Field): related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) through=kwargs.pop('through', None), db_constraint=db_constraint, ) self.db_table = kwargs.pop('db_table', None) if kwargs['rel'].through is not None: assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." Field.__init__(self, **kwargs) super(ManyToManyField, self).__init__(**kwargs) msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) Loading docs/ref/models/fields.txt +14 −0 Original line number Diff line number Diff line Loading @@ -1227,6 +1227,20 @@ that control how the relationship functions. the table for the model defining the relationship and the name of the field itself. .. attribute:: ManyToManyField.db_constraint Controls whether or not constraints should be created in the database for the foreign keys in the intermediary table. 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. It is an error to pass both ``db_constraint`` and ``through``. .. _ref-onetoone: ``OneToOneField`` Loading docs/releases/1.6.txt +2 −2 Original line number Diff line number Diff line Loading @@ -113,8 +113,8 @@ Minor features * The ``MemcachedCache`` cache backend now uses the latest :mod:`pickle` protocol available. * Added the :attr:`django.db.models.ForeignKey.db_constraint` option. * Added the :attr:`django.db.models.ForeignKey.db_constraint` and :attr:`django.db.models.ManyToManyField.db_constraint` options. * The jQuery library embedded in the admin has been upgraded to version 1.9.1. Loading tests/backends/models.py +4 −1 Original line number Diff line number Diff line Loading @@ -90,7 +90,10 @@ class Item(models.Model): @python_2_unicode_compatible class Object(models.Model): pass related_objects = models.ManyToManyField("self", db_constraint=False, symmetrical=False) def __str__(self): return str(self.id) @python_2_unicode_compatible Loading tests/backends/tests.py +34 −13 Original line number Diff line number Diff line Loading @@ -12,13 +12,12 @@ from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version from django.db.models import Sum, Avg, Variance, StdDev from django.db.utils import ConnectionHandler, DatabaseError from django.db.utils import ConnectionHandler from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) from django.test.utils import override_settings, str_prefix from django.utils import six from django.utils import six, unittest from django.utils.six.moves import xrange from django.utils import unittest from . import models Loading Loading @@ -52,7 +51,7 @@ class OracleChecks(unittest.TestCase): convert_unicode = backend.convert_unicode cursor = connection.cursor() cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), [convert_unicode('_django_testing!'),]) [convert_unicode('_django_testing!')]) @unittest.skipUnless(connection.vendor == 'oracle', "No need to check Oracle cursor semantics") Loading Loading @@ -99,6 +98,7 @@ class OracleChecks(unittest.TestCase): c.execute(query) self.assertEqual(c.fetchone()[0], 1) class MySQLTests(TestCase): @unittest.skipUnless(connection.vendor == 'mysql', "Test valid only for MySQL") Loading Loading @@ -182,6 +182,7 @@ class LastExecutedQueryTest(TestCase): self.assertEqual(connection.queries[-1]['sql'], str_prefix("QUERY = %(_)s\"SELECT strftime('%%Y', 'now');\" - PARAMS = ()")) class ParameterHandlingTest(TestCase): def test_bad_parameter_count(self): "An executemany call with too many/not enough parameters will raise an exception (Refs #12612)" Loading @@ -191,8 +192,9 @@ class ParameterHandlingTest(TestCase): connection.ops.quote_name('root'), connection.ops.quote_name('square') )) self.assertRaises(Exception, cursor.executemany, query, [(1,2,3),]) self.assertRaises(Exception, cursor.executemany, query, [(1,),]) self.assertRaises(Exception, cursor.executemany, query, [(1, 2, 3)]) self.assertRaises(Exception, cursor.executemany, query, [(1,)]) # Unfortunately, the following tests would be a good test to run on all # backends, but it breaks MySQL hard. Until #13711 is fixed, it can't be run Loading Loading @@ -240,6 +242,7 @@ class LongNameTest(TestCase): for statement in connection.ops.sql_flush(no_style(), tables, sequences): cursor.execute(statement) class SequenceResetTest(TestCase): def test_generic_relation(self): "Sequence names are correct when resetting generic relations (Ref #13941)" Loading @@ -257,6 +260,7 @@ class SequenceResetTest(TestCase): obj = models.Post.objects.create(name='New post', text='goodbye world') self.assertTrue(obj.pk > 10) class PostgresVersionTest(TestCase): def assert_parses(self, version_string, version): self.assertEqual(pg_version._parse_version(version_string), version) Loading Loading @@ -291,6 +295,7 @@ class PostgresVersionTest(TestCase): conn = OlderConnectionMock() self.assertEqual(pg_version.get_version(conn), 80300) class PostgresNewConnectionTest(TestCase): """ #17062: PostgreSQL shouldn't roll back SET TIME ZONE, even if the first Loading Loading @@ -338,17 +343,18 @@ class ConnectionCreatedSignalTest(TestCase): @skipUnlessDBFeature('test_db_allows_multiple_connections') def test_signal(self): data = {} def receiver(sender, connection, **kwargs): data["connection"] = connection connection_created.connect(receiver) connection.close() cursor = connection.cursor() connection.cursor() self.assertTrue(data["connection"].connection is connection.connection) connection_created.disconnect(receiver) data.clear() cursor = connection.cursor() connection.cursor() self.assertTrue(data == {}) Loading Loading @@ -443,7 +449,7 @@ class BackendTestCase(TestCase): old_password = connection.settings_dict['PASSWORD'] connection.settings_dict['PASSWORD'] = "françois" try: cursor = connection.cursor() connection.cursor() except DatabaseError: # As password is probably wrong, a database exception is expected pass Loading @@ -470,6 +476,7 @@ class BackendTestCase(TestCase): with self.assertRaises(DatabaseError): cursor.execute(query) # We don't make these tests conditional because that means we would need to # check and differentiate between: # * MySQL+InnoDB, MySQL+MYISAM (something we currently can't do). Loading @@ -477,7 +484,6 @@ class BackendTestCase(TestCase): # on or not, something that would be controlled by runtime support and user # preference. # verify if its type is django.database.db.IntegrityError. class FkConstraintsTests(TransactionTestCase): def setUp(self): Loading Loading @@ -581,6 +587,7 @@ class ThreadTests(TestCase): connections_dict = {} connection.cursor() connections_dict[id(connection)] = connection def runner(): # Passing django.db.connection between threads doesn't work while # connections[DEFAULT_DB_ALIAS] does. Loading Loading @@ -616,6 +623,7 @@ class ThreadTests(TestCase): connections_dict = {} for conn in connections.all(): connections_dict[id(conn)] = conn def runner(): from django.db import connections for conn in connections.all(): Loading Loading @@ -682,6 +690,7 @@ class ThreadTests(TestCase): """ # First, without explicitly enabling the connection for sharing. exceptions = set() def runner1(): def runner2(other_thread_connection): try: Loading @@ -699,6 +708,7 @@ class ThreadTests(TestCase): # Then, with explicitly enabling the connection for sharing. exceptions = set() def runner1(): def runner2(other_thread_connection): try: Loading Loading @@ -746,3 +756,14 @@ class DBConstraintTestCase(TransactionTestCase): with self.assertRaises(models.Object.DoesNotExist): ref.obj def test_many_to_many(self): obj = models.Object.objects.create() obj.related_objects.create() self.assertEqual(models.Object.objects.count(), 2) self.assertEqual(obj.related_objects.count(), 1) intermediary_model = models.Object._meta.get_field_by_name("related_objects")[0].rel.through intermediary_model.objects.create(from_object_id=obj.id, to_object_id=12345) self.assertEqual(obj.related_objects.count(), 1) self.assertEqual(intermediary_model.objects.count(), 2) Loading
django/db/models/fields/related.py +11 −6 Original line number Diff line number Diff line Loading @@ -955,7 +955,9 @@ class OneToOneRel(ManyToOneRel): class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): symmetrical=True, through=None, db_constraint=True): if through and not db_constraint: raise ValueError("Can't supply a through model and db_constraint=False") self.to = to self.related_name = related_name if limit_choices_to is None: Loading @@ -964,6 +966,7 @@ class ManyToManyRel(object): self.symmetrical = symmetrical self.multiple = True self.through = through self.db_constraint = db_constraint def is_hidden(self): "Should the related object be hidden?" Loading Loading @@ -1196,15 +1199,15 @@ def create_many_to_many_intermediary_model(field, klass): return type(name, (models.Model,), { 'Meta': meta, '__module__': klass.__module__, from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace), to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace, db_constraint=field.rel.db_constraint), to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace, db_constraint=field.rel.db_constraint) }) class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") def __init__(self, to, **kwargs): def __init__(self, to, db_constraint=True, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT Loading @@ -1219,13 +1222,15 @@ class ManyToManyField(RelatedField, Field): related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) through=kwargs.pop('through', None), db_constraint=db_constraint, ) self.db_table = kwargs.pop('db_table', None) if kwargs['rel'].through is not None: assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." Field.__init__(self, **kwargs) super(ManyToManyField, self).__init__(**kwargs) msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) Loading
docs/ref/models/fields.txt +14 −0 Original line number Diff line number Diff line Loading @@ -1227,6 +1227,20 @@ that control how the relationship functions. the table for the model defining the relationship and the name of the field itself. .. attribute:: ManyToManyField.db_constraint Controls whether or not constraints should be created in the database for the foreign keys in the intermediary table. 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. It is an error to pass both ``db_constraint`` and ``through``. .. _ref-onetoone: ``OneToOneField`` Loading
docs/releases/1.6.txt +2 −2 Original line number Diff line number Diff line Loading @@ -113,8 +113,8 @@ Minor features * The ``MemcachedCache`` cache backend now uses the latest :mod:`pickle` protocol available. * Added the :attr:`django.db.models.ForeignKey.db_constraint` option. * Added the :attr:`django.db.models.ForeignKey.db_constraint` and :attr:`django.db.models.ManyToManyField.db_constraint` options. * The jQuery library embedded in the admin has been upgraded to version 1.9.1. Loading
tests/backends/models.py +4 −1 Original line number Diff line number Diff line Loading @@ -90,7 +90,10 @@ class Item(models.Model): @python_2_unicode_compatible class Object(models.Model): pass related_objects = models.ManyToManyField("self", db_constraint=False, symmetrical=False) def __str__(self): return str(self.id) @python_2_unicode_compatible Loading
tests/backends/tests.py +34 −13 Original line number Diff line number Diff line Loading @@ -12,13 +12,12 @@ from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version from django.db.models import Sum, Avg, Variance, StdDev from django.db.utils import ConnectionHandler, DatabaseError from django.db.utils import ConnectionHandler from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) from django.test.utils import override_settings, str_prefix from django.utils import six from django.utils import six, unittest from django.utils.six.moves import xrange from django.utils import unittest from . import models Loading Loading @@ -52,7 +51,7 @@ class OracleChecks(unittest.TestCase): convert_unicode = backend.convert_unicode cursor = connection.cursor() cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), [convert_unicode('_django_testing!'),]) [convert_unicode('_django_testing!')]) @unittest.skipUnless(connection.vendor == 'oracle', "No need to check Oracle cursor semantics") Loading Loading @@ -99,6 +98,7 @@ class OracleChecks(unittest.TestCase): c.execute(query) self.assertEqual(c.fetchone()[0], 1) class MySQLTests(TestCase): @unittest.skipUnless(connection.vendor == 'mysql', "Test valid only for MySQL") Loading Loading @@ -182,6 +182,7 @@ class LastExecutedQueryTest(TestCase): self.assertEqual(connection.queries[-1]['sql'], str_prefix("QUERY = %(_)s\"SELECT strftime('%%Y', 'now');\" - PARAMS = ()")) class ParameterHandlingTest(TestCase): def test_bad_parameter_count(self): "An executemany call with too many/not enough parameters will raise an exception (Refs #12612)" Loading @@ -191,8 +192,9 @@ class ParameterHandlingTest(TestCase): connection.ops.quote_name('root'), connection.ops.quote_name('square') )) self.assertRaises(Exception, cursor.executemany, query, [(1,2,3),]) self.assertRaises(Exception, cursor.executemany, query, [(1,),]) self.assertRaises(Exception, cursor.executemany, query, [(1, 2, 3)]) self.assertRaises(Exception, cursor.executemany, query, [(1,)]) # Unfortunately, the following tests would be a good test to run on all # backends, but it breaks MySQL hard. Until #13711 is fixed, it can't be run Loading Loading @@ -240,6 +242,7 @@ class LongNameTest(TestCase): for statement in connection.ops.sql_flush(no_style(), tables, sequences): cursor.execute(statement) class SequenceResetTest(TestCase): def test_generic_relation(self): "Sequence names are correct when resetting generic relations (Ref #13941)" Loading @@ -257,6 +260,7 @@ class SequenceResetTest(TestCase): obj = models.Post.objects.create(name='New post', text='goodbye world') self.assertTrue(obj.pk > 10) class PostgresVersionTest(TestCase): def assert_parses(self, version_string, version): self.assertEqual(pg_version._parse_version(version_string), version) Loading Loading @@ -291,6 +295,7 @@ class PostgresVersionTest(TestCase): conn = OlderConnectionMock() self.assertEqual(pg_version.get_version(conn), 80300) class PostgresNewConnectionTest(TestCase): """ #17062: PostgreSQL shouldn't roll back SET TIME ZONE, even if the first Loading Loading @@ -338,17 +343,18 @@ class ConnectionCreatedSignalTest(TestCase): @skipUnlessDBFeature('test_db_allows_multiple_connections') def test_signal(self): data = {} def receiver(sender, connection, **kwargs): data["connection"] = connection connection_created.connect(receiver) connection.close() cursor = connection.cursor() connection.cursor() self.assertTrue(data["connection"].connection is connection.connection) connection_created.disconnect(receiver) data.clear() cursor = connection.cursor() connection.cursor() self.assertTrue(data == {}) Loading Loading @@ -443,7 +449,7 @@ class BackendTestCase(TestCase): old_password = connection.settings_dict['PASSWORD'] connection.settings_dict['PASSWORD'] = "françois" try: cursor = connection.cursor() connection.cursor() except DatabaseError: # As password is probably wrong, a database exception is expected pass Loading @@ -470,6 +476,7 @@ class BackendTestCase(TestCase): with self.assertRaises(DatabaseError): cursor.execute(query) # We don't make these tests conditional because that means we would need to # check and differentiate between: # * MySQL+InnoDB, MySQL+MYISAM (something we currently can't do). Loading @@ -477,7 +484,6 @@ class BackendTestCase(TestCase): # on or not, something that would be controlled by runtime support and user # preference. # verify if its type is django.database.db.IntegrityError. class FkConstraintsTests(TransactionTestCase): def setUp(self): Loading Loading @@ -581,6 +587,7 @@ class ThreadTests(TestCase): connections_dict = {} connection.cursor() connections_dict[id(connection)] = connection def runner(): # Passing django.db.connection between threads doesn't work while # connections[DEFAULT_DB_ALIAS] does. Loading Loading @@ -616,6 +623,7 @@ class ThreadTests(TestCase): connections_dict = {} for conn in connections.all(): connections_dict[id(conn)] = conn def runner(): from django.db import connections for conn in connections.all(): Loading Loading @@ -682,6 +690,7 @@ class ThreadTests(TestCase): """ # First, without explicitly enabling the connection for sharing. exceptions = set() def runner1(): def runner2(other_thread_connection): try: Loading @@ -699,6 +708,7 @@ class ThreadTests(TestCase): # Then, with explicitly enabling the connection for sharing. exceptions = set() def runner1(): def runner2(other_thread_connection): try: Loading Loading @@ -746,3 +756,14 @@ class DBConstraintTestCase(TransactionTestCase): with self.assertRaises(models.Object.DoesNotExist): ref.obj def test_many_to_many(self): obj = models.Object.objects.create() obj.related_objects.create() self.assertEqual(models.Object.objects.count(), 2) self.assertEqual(obj.related_objects.count(), 1) intermediary_model = models.Object._meta.get_field_by_name("related_objects")[0].rel.through intermediary_model.objects.create(from_object_id=obj.id, to_object_id=12345) self.assertEqual(obj.related_objects.count(), 1) self.assertEqual(intermediary_model.objects.count(), 2)