Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 63b3f27

Browse files
committed
models.DB_CASCADE on_delete
1 parent 1103c37 commit 63b3f27

File tree

11 files changed

+107
-18
lines changed

11 files changed

+107
-18
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ answer newbie questions, and generally made Django that much better:
590590
Nick Pope <[email protected]>
591591
Nick Presta <[email protected]>
592592
Nick Sandford <[email protected]>
593+
Nick Stefan <[email protected]>
593594
Niclas Olofsson <[email protected]>
594595
Nicola Larosa <[email protected]>
595596
Nicolas Lara <[email protected]>

django/db/backends/base/operations.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ def deferrable_sql(self):
143143
"""
144144
return ''
145145

146+
def fk_on_delete_sql(self, operation):
147+
"""
148+
Return the SQL to make an ON DELETE statement during a CREATE TABLE
149+
statement.
150+
"""
151+
on_delete = ' ON DELETE %s '
152+
if operation == 'CASCADE':
153+
return on_delete % (operation)
154+
elif operation == 'NO ACTION':
155+
return on_delete % (operation)
156+
else:
157+
raise NotImplementedError('ON DELETE %s is not supported.' % (operation))
158+
146159
def distinct_sql(self, fields):
147160
"""
148161
Return an SQL DISTINCT clause which removes duplicate rows from the

django/db/backends/base/schema.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class BaseDatabaseSchemaEditor:
5555

5656
sql_create_fk = (
5757
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
58-
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
58+
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete)s%(deferrable)s"
5959
)
6060
sql_create_inline_fk = None
6161
sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"
@@ -951,6 +951,13 @@ def _rename_field_sql(self, table, old_field, new_field, new_type):
951951
"type": new_type,
952952
}
953953

954+
def _create_on_delete_sql(self, model, field, suffix):
955+
on_delete = getattr(field.remote_field, 'on_delete', False)
956+
if on_delete and on_delete.with_db:
957+
return on_delete.as_sql(self.connection)
958+
else:
959+
return ""
960+
954961
def _create_fk_sql(self, model, field, suffix):
955962
from_table = model._meta.db_table
956963
from_column = field.column
@@ -968,6 +975,7 @@ def create_fk_name(*args, **kwargs):
968975
to_table=Table(to_table, self.quote_name),
969976
to_column=Columns(to_table, [to_column], self.quote_name),
970977
deferrable=self.connection.ops.deferrable_sql(),
978+
on_delete=self._create_on_delete_sql(model, field, suffix)
971979
)
972980

973981
def _create_unique_sql(self, model, columns):

django/db/migrations/serializer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ def serialize(self):
221221
return string.rstrip(','), imports
222222

223223

224+
class OnDeleteSerializer(BaseSerializer):
225+
def serialize(self):
226+
if self.value.value is None:
227+
return "models.%s" % (self.value.name), {}
228+
elif self.value:
229+
return "models.%s(%s)" % (self.value.name, self.value.value), {}
230+
231+
224232
class RegexSerializer(BaseSerializer):
225233
def serialize(self):
226234
imports = {"import re"}
@@ -364,6 +372,8 @@ def serializer_factory(value):
364372
return RegexSerializer(value)
365373
if isinstance(value, uuid.UUID):
366374
return UUIDSerializer(value)
375+
if isinstance(value, models.OnDelete):
376+
return OnDeleteSerializer(value)
367377
raise ValueError(
368378
"Cannot serialize: %r\nThere are some values Django cannot serialize into "
369379
"migration files.\nFor more, see https://docs.djangoproject.com/en/%s/"

django/db/models/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from django.db.models.aggregates import * # NOQA
44
from django.db.models.aggregates import __all__ as aggregates_all
55
from django.db.models.deletion import (
6-
CASCADE, DO_NOTHING, PROTECT, SET, SET_DEFAULT, SET_NULL, ProtectedError,
6+
CASCADE, DB_CASCADE, DO_NOTHING, PROTECT, SET, SET_DEFAULT, SET_NULL,
7+
OnDelete, ProtectedError,
78
)
89
from django.db.models.expressions import (
910
Case, Exists, Expression, ExpressionWrapper, F, Func, OuterRef, Subquery,
@@ -63,7 +64,8 @@ def inner(*args, **kwargs):
6364
__all__ += [
6465
'ObjectDoesNotExist', 'signals',
6566
'CASCADE', 'DO_NOTHING', 'PROTECT', 'SET', 'SET_DEFAULT', 'SET_NULL',
66-
'ProtectedError',
67+
'DB_CASCADE',
68+
'OnDelete', 'ProtectedError',
6769
'Case', 'Exists', 'Expression', 'ExpressionWrapper', 'F', 'Func',
6870
'OuterRef', 'Subquery', 'Value', 'When',
6971
'FileField', 'ImageField', 'OrderWrt', 'Lookup', 'Transform', 'Manager',

django/db/models/deletion.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,26 @@ def __init__(self, msg, protected_objects):
1111
super().__init__(msg, protected_objects)
1212

1313

14-
def CASCADE(collector, field, sub_objs, using):
14+
class OnDelete(object):
15+
with_db = False
16+
17+
def __init__(self, name, operation, value=None):
18+
self.name = name
19+
self.operation = operation
20+
self.value = value
21+
22+
def __call__(self, *args, **kwargs):
23+
return self.operation(*args, **kwargs)
24+
25+
26+
def application_cascade(collector, field, sub_objs, using):
1527
collector.collect(sub_objs, source=field.remote_field.model,
1628
source_attr=field.name, nullable=field.null)
1729
if field.null and not connections[using].features.can_defer_constraint_checks:
1830
collector.add_field_update(field, None, sub_objs)
1931

2032

21-
def PROTECT(collector, field, sub_objs, using):
33+
def application_protect(collector, field, sub_objs, using):
2234
raise ProtectedError(
2335
"Cannot delete some instances of model '%s' because they are "
2436
"referenced through a protected foreign key: '%s.%s'" % (
@@ -28,27 +40,44 @@ def PROTECT(collector, field, sub_objs, using):
2840
)
2941

3042

31-
def SET(value):
43+
def application_set(value):
3244
if callable(value):
3345
def set_on_delete(collector, field, sub_objs, using):
3446
collector.add_field_update(field, value(), sub_objs)
3547
else:
3648
def set_on_delete(collector, field, sub_objs, using):
3749
collector.add_field_update(field, value, sub_objs)
3850
set_on_delete.deconstruct = lambda: ('django.db.models.SET', (value,), {})
39-
return set_on_delete
51+
return OnDelete('SET', set_on_delete, value)
4052

4153

42-
def SET_NULL(collector, field, sub_objs, using):
54+
def application_set_null(collector, field, sub_objs, using):
4355
collector.add_field_update(field, None, sub_objs)
4456

4557

46-
def SET_DEFAULT(collector, field, sub_objs, using):
58+
def application_set_default(collector, field, sub_objs, using):
4759
collector.add_field_update(field, field.get_default(), sub_objs)
4860

4961

50-
def DO_NOTHING(collector, field, sub_objs, using):
51-
pass
62+
CASCADE = OnDelete('CASCADE', application_cascade)
63+
PROTECT = OnDelete('PROTECT', application_protect)
64+
SET = OnDelete('SET', application_set)
65+
SET_NULL = OnDelete('SET_NULL', application_set_null)
66+
SET_DEFAULT = OnDelete('SET_DEFAULT', application_set_default)
67+
68+
69+
class DatabaseOnDelete(OnDelete):
70+
with_db = True
71+
72+
def __call__(self, collector, field, sub_objs, using):
73+
pass
74+
75+
def as_sql(self, connection):
76+
return connection.ops.fk_on_delete_sql(self.operation)
77+
78+
79+
DO_NOTHING = DatabaseOnDelete('DO_NOTHING', 'NO ACTION')
80+
DB_CASCADE = DatabaseOnDelete('DB_CASCADE', 'CASCADE')
5281

5382

5483
def get_candidate_relations_to_delete(opts):
@@ -144,7 +173,7 @@ def can_fast_delete(self, objs, from_field=None):
144173
# Foreign keys pointing to this model, both from m2m and other
145174
# models.
146175
for related in get_candidate_relations_to_delete(opts):
147-
if related.field.remote_field.on_delete is not DO_NOTHING:
176+
if related.field.remote_field.on_delete.with_db is False:
148177
return False
149178
for field in model._meta.private_fields:
150179
if hasattr(field, 'bulk_related_objects'):
@@ -211,7 +240,7 @@ def collect(self, objs, source=None, nullable=False, collect_related=True,
211240
if keep_parents and related.model in parents:
212241
continue
213242
field = related.field
214-
if field.remote_field.on_delete == DO_NOTHING:
243+
if field.remote_field.on_delete.with_db:
215244
continue
216245
batches = self.get_del_batches(new_objs, field)
217246
for batch in batches:

docs/ref/models/fields.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,17 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
12991299
integrity, this will cause an :exc:`~django.db.IntegrityError` unless
13001300
you manually add an SQL ``ON DELETE`` constraint to the database field.
13011301

1302+
* .. attribute:: DB_CASCADE
1303+
1304+
.. versionadded:: 2.0
1305+
1306+
Cascade deletes. Django takes no direct action, and behaves exactly like
1307+
DO_NOTHING. The cascade deletion happens at the database level, by adding
1308+
ON DELETE CASCADE as an SQL constraint.
1309+
1310+
.. note::
1311+
This is only supported by the PostgreSQL and MySQL database backends.
1312+
13021313
.. attribute:: ForeignKey.limit_choices_to
13031314

13041315
Sets a limit to the available choices for this field when this field is

docs/releases/2.0.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ Models
261261

262262
* The new ``field_name`` parameter of :meth:`.QuerySet.in_bulk` allows fetching
263263
results based on any unique model field.
264+
* The ``on_delete`` argument for ``ForeignKey`` and ``OneToOneField`` now accepts
265+
DB_CASCADE. This will behave like DO_NOTHING in Django, but leverage
266+
ON DELETE CASCADE, via an SQL constraint.
264267

265268
Requests and Responses
266269
~~~~~~~~~~~~~~~~~~~~~~

tests/delete/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,11 @@ class Base(models.Model):
127127

128128
class RelToBase(models.Model):
129129
base = models.ForeignKey(Base, models.DO_NOTHING)
130+
131+
132+
class BaseDbCascade(models.Model):
133+
pass
134+
135+
136+
class RelToBaseDbCascade(models.Model):
137+
base = models.ForeignKey(BaseDbCascade, models.DB_CASCADE)

tests/delete/tests.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
66

77
from .models import (
8-
MR, A, Avatar, Base, Child, HiddenUser, HiddenUserProfile, M, M2MFrom,
9-
M2MTo, MRNull, Parent, R, RChild, S, T, User, create_a, get_default_r,
8+
MR, A, Avatar, Base, BaseDbCascade, Child, HiddenUser, HiddenUserProfile,
9+
M, M2MFrom, M2MTo, MRNull, Parent, R, RChild, S, T, User, create_a,
10+
get_default_r,
1011
)
1112

1213

@@ -88,20 +89,22 @@ def test_do_nothing_qscount(self):
8889
b.delete()
8990
self.assertEqual(Base.objects.count(), 0)
9091

92+
@skipUnlessDBFeature("supports_foreign_keys")
9193
def test_db_cascade(self):
9294
a = create_a('db_cascade')
9395
a.db_cascade.delete()
9496
self.assertFalse(A.objects.filter(name='db_cascade').exists())
9597

98+
@skipUnlessDBFeature("supports_foreign_keys")
9699
def test_db_cascade_qscount(self):
97100
"""
98101
A models.DB_CASCADE relation doesn't trigger a query
99102
"""
100-
b = Base.objects.create()
103+
b = BaseDbCascade.objects.create()
101104
with self.assertNumQueries(1):
102-
# RelToBase should not be queried.
105+
# RelToBaseDbCascade should not be queried.
103106
b.delete()
104-
self.assertEqual(Base.objects.count(), 0)
107+
self.assertEqual(BaseDbCascade.objects.count(), 0)
105108

106109
def test_inheritance_cascade_up(self):
107110
child = RChild.objects.create()

0 commit comments

Comments
 (0)