mirror of
https://github.com/django/django.git
synced 2025-10-26 15:16:09 +00:00
Fixed #21961 -- Added support for database-level delete options for ForeignKey.
Thanks Simon Charette for pair programming. Co-authored-by: Nick Stefan <NickStefan12@gmail.com> Co-authored-by: Akash Kumar Sen <71623442+Akash-Kumar-Sen@users.noreply.github.com> Co-authored-by: Simon Charette <charette.s@gmail.com>
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -791,6 +791,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Nick Presta <nick@nickpresta.ca>
|
Nick Presta <nick@nickpresta.ca>
|
||||||
Nick Sandford <nick.sandford@gmail.com>
|
Nick Sandford <nick.sandford@gmail.com>
|
||||||
Nick Sarbicki <nick.a.sarbicki@gmail.com>
|
Nick Sarbicki <nick.a.sarbicki@gmail.com>
|
||||||
|
Nick Stefan <https://github.com/nickstefan>
|
||||||
Niclas Olofsson <n@niclasolofsson.se>
|
Niclas Olofsson <n@niclasolofsson.se>
|
||||||
Nicola Larosa <nico@teknico.net>
|
Nicola Larosa <nico@teknico.net>
|
||||||
Nicolas Lara <nicolaslara@gmail.com>
|
Nicolas Lara <nicolaslara@gmail.com>
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ def get_deleted_objects(objs, request, admin_site):
|
|||||||
|
|
||||||
|
|
||||||
class NestedObjects(Collector):
|
class NestedObjects(Collector):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, force_collection=True, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, force_collection=force_collection, **kwargs)
|
||||||
self.edges = {} # {from_instance: [to_instances]}
|
self.edges = {} # {from_instance: [to_instances]}
|
||||||
self.protected = set()
|
self.protected = set()
|
||||||
self.model_objs = defaultdict(set)
|
self.model_objs = defaultdict(set)
|
||||||
@@ -242,13 +242,6 @@ class NestedObjects(Collector):
|
|||||||
roots.extend(self._nested(root, seen, format_callback))
|
roots.extend(self._nested(root, seen, format_callback))
|
||||||
return roots
|
return roots
|
||||||
|
|
||||||
def can_fast_delete(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
We always want to load the objects into memory so that we can display
|
|
||||||
them to the user in confirm page.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def model_format_dict(obj):
|
def model_format_dict(obj):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
|
|||||||
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
|
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
|
||||||
from django.db.models import DO_NOTHING, ForeignObject, ForeignObjectRel
|
from django.db.models import DO_NOTHING, ForeignObject, ForeignObjectRel
|
||||||
from django.db.models.base import ModelBase, make_foreign_order_accessors
|
from django.db.models.base import ModelBase, make_foreign_order_accessors
|
||||||
|
from django.db.models.deletion import DatabaseOnDelete
|
||||||
from django.db.models.fields import Field
|
from django.db.models.fields import Field
|
||||||
from django.db.models.fields.mixins import FieldCacheMixin
|
from django.db.models.fields.mixins import FieldCacheMixin
|
||||||
from django.db.models.fields.related import (
|
from django.db.models.fields.related import (
|
||||||
@@ -139,6 +140,16 @@ class GenericForeignKey(FieldCacheMixin, Field):
|
|||||||
id="contenttypes.E004",
|
id="contenttypes.E004",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
elif isinstance(field.remote_field.on_delete, DatabaseOnDelete):
|
||||||
|
return [
|
||||||
|
checks.Error(
|
||||||
|
f"'{self.model._meta.object_name}.{self.ct_field}' cannot use "
|
||||||
|
"the database-level on_delete variant.",
|
||||||
|
hint="Change the on_delete rule to the non-database variant.",
|
||||||
|
obj=self,
|
||||||
|
id="contenttypes.E006",
|
||||||
|
)
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class Command(BaseCommand):
|
|||||||
ct_info.append(
|
ct_info.append(
|
||||||
" - Content type for %s.%s" % (ct.app_label, ct.model)
|
" - Content type for %s.%s" % (ct.app_label, ct.model)
|
||||||
)
|
)
|
||||||
collector = NoFastDeleteCollector(using=using, origin=ct)
|
collector = Collector(
|
||||||
|
using=using, origin=ct, force_collection=True
|
||||||
|
)
|
||||||
collector.collect([ct])
|
collector.collect([ct])
|
||||||
|
|
||||||
for obj_type, objs in collector.data.items():
|
for obj_type, objs in collector.data.items():
|
||||||
@@ -103,11 +105,3 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
self.stdout.write("Stale content types remain.")
|
self.stdout.write("Stale content types remain.")
|
||||||
|
|
||||||
|
|
||||||
class NoFastDeleteCollector(Collector):
|
|
||||||
def can_fast_delete(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Always load related objects to display them when showing confirmation.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -390,6 +390,9 @@ class BaseDatabaseFeatures:
|
|||||||
# subqueries?
|
# subqueries?
|
||||||
supports_tuple_comparison_against_subquery = True
|
supports_tuple_comparison_against_subquery = True
|
||||||
|
|
||||||
|
# Does the backend support DEFAULT as delete option?
|
||||||
|
supports_on_delete_db_default = True
|
||||||
|
|
||||||
# Collation names for use by the Django test suite.
|
# Collation names for use by the Django test suite.
|
||||||
test_collations = {
|
test_collations = {
|
||||||
"ci": None, # Case-insensitive.
|
"ci": None, # Case-insensitive.
|
||||||
|
|||||||
@@ -254,6 +254,16 @@ class BaseDatabaseOperations:
|
|||||||
if sql
|
if sql
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def fk_on_delete_sql(self, operation):
|
||||||
|
"""
|
||||||
|
Return the SQL to make an ON DELETE statement.
|
||||||
|
"""
|
||||||
|
if operation in ["CASCADE", "SET NULL", "SET DEFAULT"]:
|
||||||
|
return f" ON DELETE {operation}"
|
||||||
|
if operation == "":
|
||||||
|
return ""
|
||||||
|
raise NotImplementedError(f"ON DELETE {operation} is not supported.")
|
||||||
|
|
||||||
def bulk_insert_sql(self, fields, placeholder_rows):
|
def bulk_insert_sql(self, fields, placeholder_rows):
|
||||||
placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
|
placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
|
||||||
values_sql = ", ".join([f"({sql})" for sql in placeholder_rows_sql])
|
values_sql = ", ".join([f"({sql})" for sql in placeholder_rows_sql])
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class BaseDatabaseSchemaEditor:
|
|||||||
|
|
||||||
sql_create_fk = (
|
sql_create_fk = (
|
||||||
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
||||||
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
|
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete_db)s%(deferrable)s"
|
||||||
)
|
)
|
||||||
sql_create_inline_fk = None
|
sql_create_inline_fk = None
|
||||||
sql_create_column_inline_fk = None
|
sql_create_column_inline_fk = None
|
||||||
@@ -241,6 +241,7 @@ class BaseDatabaseSchemaEditor:
|
|||||||
definition += " " + self.sql_create_inline_fk % {
|
definition += " " + self.sql_create_inline_fk % {
|
||||||
"to_table": self.quote_name(to_table),
|
"to_table": self.quote_name(to_table),
|
||||||
"to_column": self.quote_name(to_column),
|
"to_column": self.quote_name(to_column),
|
||||||
|
"on_delete_db": self._create_on_delete_sql(model, field),
|
||||||
}
|
}
|
||||||
elif self.connection.features.supports_foreign_keys:
|
elif self.connection.features.supports_foreign_keys:
|
||||||
self.deferred_sql.append(
|
self.deferred_sql.append(
|
||||||
@@ -759,6 +760,7 @@ class BaseDatabaseSchemaEditor:
|
|||||||
"to_table": self.quote_name(to_table),
|
"to_table": self.quote_name(to_table),
|
||||||
"to_column": self.quote_name(to_column),
|
"to_column": self.quote_name(to_column),
|
||||||
"deferrable": self.connection.ops.deferrable_sql(),
|
"deferrable": self.connection.ops.deferrable_sql(),
|
||||||
|
"on_delete_db": self._create_on_delete_sql(model, field),
|
||||||
}
|
}
|
||||||
# Otherwise, add FK constraints later.
|
# Otherwise, add FK constraints later.
|
||||||
else:
|
else:
|
||||||
@@ -1628,6 +1630,13 @@ class BaseDatabaseSchemaEditor:
|
|||||||
new_name=self.quote_name(new_name),
|
new_name=self.quote_name(new_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_on_delete_sql(self, model, field):
|
||||||
|
remote_field = field.remote_field
|
||||||
|
try:
|
||||||
|
return remote_field.on_delete.on_delete_sql(self)
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
def _index_columns(self, table, columns, col_suffixes, opclasses):
|
def _index_columns(self, table, columns, col_suffixes, opclasses):
|
||||||
return Columns(table, columns, self.quote_name, col_suffixes=col_suffixes)
|
return Columns(table, columns, self.quote_name, col_suffixes=col_suffixes)
|
||||||
|
|
||||||
@@ -1740,6 +1749,7 @@ class BaseDatabaseSchemaEditor:
|
|||||||
to_table=to_table,
|
to_table=to_table,
|
||||||
to_column=to_column,
|
to_column=to_column,
|
||||||
deferrable=deferrable,
|
deferrable=deferrable,
|
||||||
|
on_delete_db=self._create_on_delete_sql(model, field),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _fk_constraint_name(self, model, field, suffix):
|
def _fk_constraint_name(self, model, field, suffix):
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
SET V_I = P_I;
|
SET V_I = P_I;
|
||||||
END;
|
END;
|
||||||
"""
|
"""
|
||||||
|
supports_on_delete_db_default = False
|
||||||
# Neither MySQL nor MariaDB support partial indexes.
|
# Neither MySQL nor MariaDB support partial indexes.
|
||||||
supports_partial_indexes = False
|
supports_partial_indexes = False
|
||||||
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
|
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s"
|
sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s"
|
||||||
sql_create_column_inline_fk = (
|
sql_create_column_inline_fk = (
|
||||||
", ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
", ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
|
||||||
"REFERENCES %(to_table)s(%(to_column)s)"
|
"REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)s"
|
||||||
)
|
)
|
||||||
sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
|
sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
supports_json_field_contains = False
|
supports_json_field_contains = False
|
||||||
supports_json_negative_indexing = False
|
supports_json_negative_indexing = False
|
||||||
supports_collation_on_textfield = False
|
supports_collation_on_textfield = False
|
||||||
|
supports_on_delete_db_default = False
|
||||||
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
|
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
|
||||||
django_test_expected_failures = {
|
django_test_expected_failures = {
|
||||||
# A bug in Django/oracledb with respect to string handling (#23843).
|
# A bug in Django/oracledb with respect to string handling (#23843).
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
sql_alter_column_no_default_null = sql_alter_column_no_default
|
sql_alter_column_no_default_null = sql_alter_column_no_default
|
||||||
|
|
||||||
sql_create_column_inline_fk = (
|
sql_create_column_inline_fk = (
|
||||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s"
|
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)"
|
||||||
|
"s%(deferrable)s"
|
||||||
)
|
)
|
||||||
sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
|
sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
|
||||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
# Setting the constraint to IMMEDIATE to allow changing data in the same
|
# Setting the constraint to IMMEDIATE to allow changing data in the same
|
||||||
# transaction.
|
# transaction.
|
||||||
sql_create_column_inline_fk = (
|
sql_create_column_inline_fk = (
|
||||||
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s"
|
"CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(on_delete_db)s"
|
||||||
"; SET CONSTRAINTS %(namespace)s%(name)s IMMEDIATE"
|
"%(deferrable)s; SET CONSTRAINTS %(namespace)s%(name)s IMMEDIATE"
|
||||||
)
|
)
|
||||||
# Setting the constraint to IMMEDIATE runs any deferred checks to allow
|
# Setting the constraint to IMMEDIATE runs any deferred checks to allow
|
||||||
# dropping it in the same transaction.
|
# dropping it in the same transaction.
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
sql_delete_table = "DROP TABLE %(table)s"
|
sql_delete_table = "DROP TABLE %(table)s"
|
||||||
sql_create_fk = None
|
sql_create_fk = None
|
||||||
sql_create_inline_fk = (
|
sql_create_inline_fk = (
|
||||||
"REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED"
|
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete_db)s DEFERRABLE INITIALLY "
|
||||||
|
"DEFERRED"
|
||||||
)
|
)
|
||||||
sql_create_column_inline_fk = sql_create_inline_fk
|
sql_create_column_inline_fk = sql_create_inline_fk
|
||||||
sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
|
sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from django.conf import SettingsReference
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.migrations.operations.base import Operation
|
from django.db.migrations.operations.base import Operation
|
||||||
from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
|
from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
|
||||||
|
from django.db.models.deletion import DatabaseOnDelete
|
||||||
from django.utils.functional import LazyObject, Promise
|
from django.utils.functional import LazyObject, Promise
|
||||||
from django.utils.version import get_docs_version
|
from django.utils.version import get_docs_version
|
||||||
|
|
||||||
@@ -71,6 +72,12 @@ class ChoicesSerializer(BaseSerializer):
|
|||||||
return serializer_factory(self.value.value).serialize()
|
return serializer_factory(self.value.value).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseOnDeleteSerializer(BaseSerializer):
|
||||||
|
def serialize(self):
|
||||||
|
path = self.value.__class__.__module__
|
||||||
|
return f"{path}.{self.value.__name__}", {f"import {path}"}
|
||||||
|
|
||||||
|
|
||||||
class DateTimeSerializer(BaseSerializer):
|
class DateTimeSerializer(BaseSerializer):
|
||||||
"""For datetime.*, except datetime.datetime."""
|
"""For datetime.*, except datetime.datetime."""
|
||||||
|
|
||||||
@@ -363,6 +370,7 @@ class Serializer:
|
|||||||
pathlib.PurePath: PathSerializer,
|
pathlib.PurePath: PathSerializer,
|
||||||
os.PathLike: PathLikeSerializer,
|
os.PathLike: PathLikeSerializer,
|
||||||
zoneinfo.ZoneInfo: ZoneInfoSerializer,
|
zoneinfo.ZoneInfo: ZoneInfoSerializer,
|
||||||
|
DatabaseOnDelete: DatabaseOnDeleteSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ from django.db.models.constraints import * # NOQA
|
|||||||
from django.db.models.constraints import __all__ as constraints_all
|
from django.db.models.constraints import __all__ as constraints_all
|
||||||
from django.db.models.deletion import (
|
from django.db.models.deletion import (
|
||||||
CASCADE,
|
CASCADE,
|
||||||
|
DB_CASCADE,
|
||||||
|
DB_SET_DEFAULT,
|
||||||
|
DB_SET_NULL,
|
||||||
DO_NOTHING,
|
DO_NOTHING,
|
||||||
PROTECT,
|
PROTECT,
|
||||||
RESTRICT,
|
RESTRICT,
|
||||||
@@ -75,6 +78,9 @@ __all__ += [
|
|||||||
"ObjectDoesNotExist",
|
"ObjectDoesNotExist",
|
||||||
"signals",
|
"signals",
|
||||||
"CASCADE",
|
"CASCADE",
|
||||||
|
"DB_CASCADE",
|
||||||
|
"DB_SET_DEFAULT",
|
||||||
|
"DB_SET_NULL",
|
||||||
"DO_NOTHING",
|
"DO_NOTHING",
|
||||||
"PROTECT",
|
"PROTECT",
|
||||||
"RESTRICT",
|
"RESTRICT",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from django.db import (
|
|||||||
)
|
)
|
||||||
from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value
|
from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.deletion import CASCADE, Collector
|
from django.db.models.deletion import CASCADE, DO_NOTHING, Collector, DatabaseOnDelete
|
||||||
from django.db.models.expressions import DatabaseDefault
|
from django.db.models.expressions import DatabaseDefault
|
||||||
from django.db.models.fetch_modes import FETCH_ONE
|
from django.db.models.fetch_modes import FETCH_ONE
|
||||||
from django.db.models.fields.composite import CompositePrimaryKey
|
from django.db.models.fields.composite import CompositePrimaryKey
|
||||||
@@ -1770,6 +1770,7 @@ class Model(AltersData, metaclass=ModelBase):
|
|||||||
*cls._check_fields(**kwargs),
|
*cls._check_fields(**kwargs),
|
||||||
*cls._check_m2m_through_same_relationship(),
|
*cls._check_m2m_through_same_relationship(),
|
||||||
*cls._check_long_column_names(databases),
|
*cls._check_long_column_names(databases),
|
||||||
|
*cls._check_related_fields(),
|
||||||
]
|
]
|
||||||
clash_errors = (
|
clash_errors = (
|
||||||
*cls._check_id_field(),
|
*cls._check_id_field(),
|
||||||
@@ -2455,6 +2456,29 @@ class Model(AltersData, metaclass=ModelBase):
|
|||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_related_fields(cls):
|
||||||
|
has_db_variant = False
|
||||||
|
has_python_variant = False
|
||||||
|
for rel in cls._meta.get_fields():
|
||||||
|
if rel.related_model:
|
||||||
|
if not (on_delete := getattr(rel.remote_field, "on_delete", None)):
|
||||||
|
continue
|
||||||
|
if isinstance(on_delete, DatabaseOnDelete):
|
||||||
|
has_db_variant = True
|
||||||
|
elif on_delete != DO_NOTHING:
|
||||||
|
has_python_variant = True
|
||||||
|
if has_db_variant and has_python_variant:
|
||||||
|
return [
|
||||||
|
checks.Error(
|
||||||
|
"The model cannot have related fields with both "
|
||||||
|
"database-level and Python-level on_delete variants.",
|
||||||
|
obj=cls,
|
||||||
|
id="models.E050",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_expr_references(cls, expr):
|
def _get_expr_references(cls, expr):
|
||||||
if isinstance(expr, Q):
|
if isinstance(expr, Q):
|
||||||
|
|||||||
@@ -81,6 +81,28 @@ def DO_NOTHING(collector, field, sub_objs, using):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseOnDelete:
|
||||||
|
def __init__(self, operation, name, forced_collector=None):
|
||||||
|
self.operation = operation
|
||||||
|
self.forced_collector = forced_collector
|
||||||
|
self.__name__ = name
|
||||||
|
|
||||||
|
__call__ = DO_NOTHING
|
||||||
|
|
||||||
|
def on_delete_sql(self, schema_editor):
|
||||||
|
return schema_editor.connection.ops.fk_on_delete_sql(self.operation)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__name__
|
||||||
|
|
||||||
|
|
||||||
|
DB_CASCADE = DatabaseOnDelete("CASCADE", "DB_CASCADE", CASCADE)
|
||||||
|
DB_SET_DEFAULT = DatabaseOnDelete("SET DEFAULT", "DB_SET_DEFAULT")
|
||||||
|
DB_SET_NULL = DatabaseOnDelete("SET NULL", "DB_SET_NULL")
|
||||||
|
|
||||||
|
SKIP_COLLECTION = frozenset([DO_NOTHING, DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL])
|
||||||
|
|
||||||
|
|
||||||
def get_candidate_relations_to_delete(opts):
|
def get_candidate_relations_to_delete(opts):
|
||||||
# The candidate relations are the ones that come from N-1 and 1-1
|
# The candidate relations are the ones that come from N-1 and 1-1
|
||||||
# relations. N-N (i.e., many-to-many) relations aren't candidates for
|
# relations. N-N (i.e., many-to-many) relations aren't candidates for
|
||||||
@@ -93,10 +115,12 @@ def get_candidate_relations_to_delete(opts):
|
|||||||
|
|
||||||
|
|
||||||
class Collector:
|
class Collector:
|
||||||
def __init__(self, using, origin=None):
|
def __init__(self, using, origin=None, force_collection=False):
|
||||||
self.using = using
|
self.using = using
|
||||||
# A Model or QuerySet object.
|
# A Model or QuerySet object.
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
|
# Force collecting objects for deletion on the Python-level.
|
||||||
|
self.force_collection = force_collection
|
||||||
# Initially, {model: {instances}}, later values become lists.
|
# Initially, {model: {instances}}, later values become lists.
|
||||||
self.data = defaultdict(set)
|
self.data = defaultdict(set)
|
||||||
# {(field, value): [instances, …]}
|
# {(field, value): [instances, …]}
|
||||||
@@ -194,6 +218,8 @@ class Collector:
|
|||||||
skipping parent -> child -> parent chain preventing fast delete of
|
skipping parent -> child -> parent chain preventing fast delete of
|
||||||
the child.
|
the child.
|
||||||
"""
|
"""
|
||||||
|
if self.force_collection:
|
||||||
|
return False
|
||||||
if from_field and from_field.remote_field.on_delete is not CASCADE:
|
if from_field and from_field.remote_field.on_delete is not CASCADE:
|
||||||
return False
|
return False
|
||||||
if hasattr(objs, "_meta"):
|
if hasattr(objs, "_meta"):
|
||||||
@@ -215,7 +241,7 @@ class Collector:
|
|||||||
and
|
and
|
||||||
# Foreign keys pointing to this model.
|
# Foreign keys pointing to this model.
|
||||||
all(
|
all(
|
||||||
related.field.remote_field.on_delete is DO_NOTHING
|
related.field.remote_field.on_delete in SKIP_COLLECTION
|
||||||
for related in get_candidate_relations_to_delete(opts)
|
for related in get_candidate_relations_to_delete(opts)
|
||||||
)
|
)
|
||||||
and (
|
and (
|
||||||
@@ -316,8 +342,13 @@ class Collector:
|
|||||||
continue
|
continue
|
||||||
field = related.field
|
field = related.field
|
||||||
on_delete = field.remote_field.on_delete
|
on_delete = field.remote_field.on_delete
|
||||||
if on_delete == DO_NOTHING:
|
if on_delete in SKIP_COLLECTION:
|
||||||
continue
|
if self.force_collection and (
|
||||||
|
forced_on_delete := getattr(on_delete, "forced_collector", None)
|
||||||
|
):
|
||||||
|
on_delete = forced_on_delete
|
||||||
|
else:
|
||||||
|
continue
|
||||||
related_model = related.related_model
|
related_model = related.related_model
|
||||||
if self.can_fast_delete(related_model, from_field=field):
|
if self.can_fast_delete(related_model, from_field=field):
|
||||||
model_fast_deletes[related_model].append(field)
|
model_fast_deletes[related_model].append(field)
|
||||||
|
|||||||
@@ -155,8 +155,6 @@ class Field(RegisterLookupMixin):
|
|||||||
"error_messages",
|
"error_messages",
|
||||||
"help_text",
|
"help_text",
|
||||||
"limit_choices_to",
|
"limit_choices_to",
|
||||||
# Database-level options are not supported, see #21961.
|
|
||||||
"on_delete",
|
|
||||||
"related_name",
|
"related_name",
|
||||||
"related_query_name",
|
"related_query_name",
|
||||||
"validators",
|
"validators",
|
||||||
|
|||||||
@@ -6,11 +6,19 @@ from django import forms
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import SettingsReference, settings
|
from django.conf import SettingsReference, settings
|
||||||
from django.core import checks, exceptions
|
from django.core import checks, exceptions
|
||||||
from django.db import connection, router
|
from django.db import connection, connections, router
|
||||||
from django.db.backends import utils
|
from django.db.backends import utils
|
||||||
from django.db.models import Q
|
from django.db.models import NOT_PROVIDED, Q
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.deletion import CASCADE, SET_DEFAULT, SET_NULL
|
from django.db.models.deletion import (
|
||||||
|
CASCADE,
|
||||||
|
DB_SET_DEFAULT,
|
||||||
|
DB_SET_NULL,
|
||||||
|
DO_NOTHING,
|
||||||
|
SET_DEFAULT,
|
||||||
|
SET_NULL,
|
||||||
|
DatabaseOnDelete,
|
||||||
|
)
|
||||||
from django.db.models.query_utils import PathInfo
|
from django.db.models.query_utils import PathInfo
|
||||||
from django.db.models.utils import make_model_tuple
|
from django.db.models.utils import make_model_tuple
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -1041,18 +1049,21 @@ class ForeignKey(ForeignObject):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
def check(self, **kwargs):
|
def check(self, **kwargs):
|
||||||
|
databases = kwargs.get("databases") or []
|
||||||
return [
|
return [
|
||||||
*super().check(**kwargs),
|
*super().check(**kwargs),
|
||||||
*self._check_on_delete(),
|
*self._check_on_delete(databases),
|
||||||
*self._check_unique(),
|
*self._check_unique(),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _check_on_delete(self):
|
def _check_on_delete(self, databases):
|
||||||
on_delete = getattr(self.remote_field, "on_delete", None)
|
on_delete = getattr(self.remote_field, "on_delete", None)
|
||||||
if on_delete == SET_NULL and not self.null:
|
errors = []
|
||||||
return [
|
if on_delete in [DB_SET_NULL, SET_NULL] and not self.null:
|
||||||
|
errors.append(
|
||||||
checks.Error(
|
checks.Error(
|
||||||
"Field specifies on_delete=SET_NULL, but cannot be null.",
|
f"Field specifies on_delete={on_delete.__name__}, but cannot be "
|
||||||
|
"null.",
|
||||||
hint=(
|
hint=(
|
||||||
"Set null=True argument on the field, or change the on_delete "
|
"Set null=True argument on the field, or change the on_delete "
|
||||||
"rule."
|
"rule."
|
||||||
@@ -1060,18 +1071,80 @@ class ForeignKey(ForeignObject):
|
|||||||
obj=self,
|
obj=self,
|
||||||
id="fields.E320",
|
id="fields.E320",
|
||||||
)
|
)
|
||||||
]
|
)
|
||||||
elif on_delete == SET_DEFAULT and not self.has_default():
|
elif on_delete == SET_DEFAULT and not self.has_default():
|
||||||
return [
|
errors.append(
|
||||||
checks.Error(
|
checks.Error(
|
||||||
"Field specifies on_delete=SET_DEFAULT, but has no default value.",
|
"Field specifies on_delete=SET_DEFAULT, but has no default value.",
|
||||||
hint="Set a default value, or change the on_delete rule.",
|
hint="Set a default value, or change the on_delete rule.",
|
||||||
obj=self,
|
obj=self,
|
||||||
id="fields.E321",
|
id="fields.E321",
|
||||||
)
|
)
|
||||||
]
|
)
|
||||||
else:
|
elif on_delete == DB_SET_DEFAULT:
|
||||||
return []
|
if self.db_default is NOT_PROVIDED:
|
||||||
|
errors.append(
|
||||||
|
checks.Error(
|
||||||
|
"Field specifies on_delete=DB_SET_DEFAULT, but has "
|
||||||
|
"no db_default value.",
|
||||||
|
hint="Set a db_default value, or change the on_delete rule.",
|
||||||
|
obj=self,
|
||||||
|
id="fields.E322",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for db in databases:
|
||||||
|
if not router.allow_migrate_model(db, self.model):
|
||||||
|
continue
|
||||||
|
connection = connections[db]
|
||||||
|
if not (
|
||||||
|
"supports_on_delete_db_default"
|
||||||
|
in self.model._meta.required_db_features
|
||||||
|
or connection.features.supports_on_delete_db_default
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
checks.Error(
|
||||||
|
f"{connection.display_name} does not support a "
|
||||||
|
"DB_SET_DEFAULT.",
|
||||||
|
hint="Change the on_delete rule to SET_DEFAULT.",
|
||||||
|
obj=self,
|
||||||
|
id="fields.E324",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif not isinstance(self.remote_field.model, str) and on_delete != DO_NOTHING:
|
||||||
|
# Database and Python variants cannot be mixed in a chain of
|
||||||
|
# model references.
|
||||||
|
is_db_on_delete = isinstance(on_delete, DatabaseOnDelete)
|
||||||
|
ref_model_related_fields = (
|
||||||
|
ref_model_field.remote_field
|
||||||
|
for ref_model_field in self.remote_field.model._meta.get_fields()
|
||||||
|
if ref_model_field.related_model
|
||||||
|
and hasattr(ref_model_field.remote_field, "on_delete")
|
||||||
|
)
|
||||||
|
|
||||||
|
for ref_remote_field in ref_model_related_fields:
|
||||||
|
if (
|
||||||
|
ref_remote_field.on_delete is not None
|
||||||
|
and ref_remote_field.on_delete != DO_NOTHING
|
||||||
|
and isinstance(ref_remote_field.on_delete, DatabaseOnDelete)
|
||||||
|
is not is_db_on_delete
|
||||||
|
):
|
||||||
|
on_delete_type = "database" if is_db_on_delete else "Python"
|
||||||
|
ref_on_delete_type = "Python" if is_db_on_delete else "database"
|
||||||
|
errors.append(
|
||||||
|
checks.Error(
|
||||||
|
f"Field specifies {on_delete_type}-level on_delete "
|
||||||
|
"variant, but referenced model uses "
|
||||||
|
f"{ref_on_delete_type}-level variant.",
|
||||||
|
hint=(
|
||||||
|
"Use either database or Python on_delete variants "
|
||||||
|
"uniformly in the references chain."
|
||||||
|
),
|
||||||
|
obj=self,
|
||||||
|
id="fields.E323",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
return errors
|
||||||
|
|
||||||
def _check_unique(self, **kwargs):
|
def _check_unique(self, **kwargs):
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -299,9 +299,15 @@ Related fields
|
|||||||
referenced by a ``ForeignKey``.
|
referenced by a ``ForeignKey``.
|
||||||
* **fields.E312**: The ``to_field`` ``<field name>`` doesn't exist on the
|
* **fields.E312**: The ``to_field`` ``<field name>`` doesn't exist on the
|
||||||
related model ``<app label>.<model>``.
|
related model ``<app label>.<model>``.
|
||||||
* **fields.E320**: Field specifies ``on_delete=SET_NULL``, but cannot be null.
|
* **fields.E320**: Field specifies ``on_delete=<set null option>``, but cannot
|
||||||
* **fields.E321**: The field specifies ``on_delete=SET_DEFAULT``, but has no
|
be null.
|
||||||
default value.
|
* **fields.E321**: Field specifies ``on_delete=SET_DEFAULT``, but has no
|
||||||
|
``default`` value.
|
||||||
|
* **fields.E322**: Field specifies ``on_delete=DB_SET_DEFAULT``, but has no
|
||||||
|
``db_default`` value.
|
||||||
|
* **fields.E323**: Field specifies database/Python-level on_delete variant, but
|
||||||
|
referenced model uses python/database-level variant.
|
||||||
|
* **fields.E324**: ``<database>`` does not support ``DB_SET_DEFAULT``.
|
||||||
* **fields.E330**: ``ManyToManyField``\s cannot be unique.
|
* **fields.E330**: ``ManyToManyField``\s cannot be unique.
|
||||||
* **fields.E331**: Field specifies a many-to-many relation through model
|
* **fields.E331**: Field specifies a many-to-many relation through model
|
||||||
``<model>``, which has not been installed.
|
``<model>``, which has not been installed.
|
||||||
@@ -446,6 +452,8 @@ Models
|
|||||||
* **models.E049**: ``constraints/indexes/unique_together`` refers to a
|
* **models.E049**: ``constraints/indexes/unique_together`` refers to a
|
||||||
``ForeignObject`` ``<field name>`` with multiple ``from_fields``, which is
|
``ForeignObject`` ``<field name>`` with multiple ``from_fields``, which is
|
||||||
not supported for that option.
|
not supported for that option.
|
||||||
|
* **models.E050**: The model cannot have related fields with both
|
||||||
|
database-level and Python-level ``on_delete`` variants.
|
||||||
|
|
||||||
Management Commands
|
Management Commands
|
||||||
-------------------
|
-------------------
|
||||||
@@ -921,6 +929,8 @@ The following checks are performed when a model contains a
|
|||||||
* **contenttypes.E004**: ``<field>`` is not a ``ForeignKey`` to
|
* **contenttypes.E004**: ``<field>`` is not a ``ForeignKey`` to
|
||||||
``contenttypes.ContentType``.
|
``contenttypes.ContentType``.
|
||||||
* **contenttypes.E005**: Model names must be at most 100 characters.
|
* **contenttypes.E005**: Model names must be at most 100 characters.
|
||||||
|
* **contenttypes.E006**: ``<field>`` cannot use the database-level
|
||||||
|
``on_delete`` variant.
|
||||||
|
|
||||||
``postgres``
|
``postgres``
|
||||||
------------
|
------------
|
||||||
|
|||||||
@@ -1699,11 +1699,11 @@ relation works.
|
|||||||
|
|
||||||
.. attribute:: ForeignKey.on_delete
|
.. attribute:: ForeignKey.on_delete
|
||||||
|
|
||||||
When an object referenced by a :class:`ForeignKey` is deleted, Django will
|
When an object referenced by a :class:`ForeignKey` is deleted, the
|
||||||
emulate the behavior of the SQL constraint specified by the
|
referring objects need updating. The :attr:`on_delete` argument specifies
|
||||||
:attr:`on_delete` argument. For example, if you have a nullable
|
how this is done, and whether Django or your database makes the updates.
|
||||||
:class:`ForeignKey` and you want it to be set null when the referenced
|
For example, if you have a nullable :class:`ForeignKey` and you want Django
|
||||||
object is deleted::
|
to set it to ``None`` when the referenced object is deleted::
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
@@ -1712,8 +1712,21 @@ relation works.
|
|||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
``on_delete`` doesn't create an SQL constraint in the database. Support for
|
The possible values for :attr:`~ForeignKey.on_delete` are listed below.
|
||||||
database-level cascade options :ticket:`may be implemented later <21961>`.
|
Import them from :mod:`django.db.models`. The ``DB_*`` variants use the
|
||||||
|
database to prevent deletions or update referring objects, whilst the other
|
||||||
|
values make Django perform the relevant actions.
|
||||||
|
|
||||||
|
The database variants are more efficient because they avoid fetching
|
||||||
|
related objects, but ``pre_delete`` and ``post_delete`` signals won't be
|
||||||
|
sent when ``DB_CASCADE`` is used.
|
||||||
|
|
||||||
|
The database variants cannot be mixed with Python variants (other than
|
||||||
|
:attr:`DO_NOTHING`) in the same model and in models related to each other.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.1
|
||||||
|
|
||||||
|
Support for ``DB_*`` variants of the ``on_delete`` attribute was added.
|
||||||
|
|
||||||
The possible values for :attr:`~ForeignKey.on_delete` are found in
|
The possible values for :attr:`~ForeignKey.on_delete` are found in
|
||||||
:mod:`django.db.models`:
|
:mod:`django.db.models`:
|
||||||
@@ -1729,6 +1742,13 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
|
|||||||
:data:`~django.db.models.signals.post_delete` signals are sent for all
|
:data:`~django.db.models.signals.post_delete` signals are sent for all
|
||||||
deleted objects.
|
deleted objects.
|
||||||
|
|
||||||
|
* .. attribute:: DB_CASCADE
|
||||||
|
|
||||||
|
.. versionadded:: 6.1
|
||||||
|
|
||||||
|
Cascade deletes. Database-level version of :attr:`CASCADE`: the database
|
||||||
|
deletes referred-to rows and the one containing the ``ForeignKey``.
|
||||||
|
|
||||||
* .. attribute:: PROTECT
|
* .. attribute:: PROTECT
|
||||||
|
|
||||||
Prevent deletion of the referenced object by raising
|
Prevent deletion of the referenced object by raising
|
||||||
@@ -1782,11 +1802,30 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
|
|||||||
Set the :class:`ForeignKey` null; this is only possible if
|
Set the :class:`ForeignKey` null; this is only possible if
|
||||||
:attr:`~Field.null` is ``True``.
|
:attr:`~Field.null` is ``True``.
|
||||||
|
|
||||||
|
* .. attribute:: DB_SET_NULL
|
||||||
|
|
||||||
|
.. versionadded:: 6.1
|
||||||
|
|
||||||
|
Set the :class:`ForeignKey` value to ``NULL``. This is only possible if
|
||||||
|
:attr:`~Field.null` is ``True``. Database-level version of
|
||||||
|
:attr:`SET_NULL`.
|
||||||
|
|
||||||
* .. attribute:: SET_DEFAULT
|
* .. attribute:: SET_DEFAULT
|
||||||
|
|
||||||
Set the :class:`ForeignKey` to its default value; a default for the
|
Set the :class:`ForeignKey` to its default value; a default for the
|
||||||
:class:`ForeignKey` must be set.
|
:class:`ForeignKey` must be set.
|
||||||
|
|
||||||
|
* .. attribute:: DB_SET_DEFAULT
|
||||||
|
|
||||||
|
.. versionadded:: 6.1
|
||||||
|
|
||||||
|
Set the :class:`ForeignKey` value to its :attr:`Field.db_default` value,
|
||||||
|
which must be set. If a row in the referenced table is deleted, the foreign
|
||||||
|
key values in the referencing table will be updated to their
|
||||||
|
:attr:`Field.db_default` values.
|
||||||
|
|
||||||
|
``DB_SET_DEFAULT`` is not supported on MySQL and MariaDB.
|
||||||
|
|
||||||
* .. function:: SET()
|
* .. function:: SET()
|
||||||
|
|
||||||
Set the :class:`ForeignKey` to the value passed to
|
Set the :class:`ForeignKey` to the value passed to
|
||||||
|
|||||||
@@ -695,7 +695,11 @@ Issues an SQL ``DELETE`` for the object. This only deletes the object in the
|
|||||||
database; the Python instance will still exist and will still have data in
|
database; the Python instance will still exist and will still have data in
|
||||||
its fields, except for the primary key set to ``None``. This method returns the
|
its fields, except for the primary key set to ``None``. This method returns the
|
||||||
number of objects deleted and a dictionary with the number of deletions per
|
number of objects deleted and a dictionary with the number of deletions per
|
||||||
object type.
|
object type. The return value will count instances from related models if
|
||||||
|
Django is emulating cascade behavior via Python :attr:`~ForeignKey.on_delete`
|
||||||
|
variants. Otherwise, for database variants such as
|
||||||
|
:attr:`~django.db.models.DB_CASCADE`, the return value will report only
|
||||||
|
instances of the :class:`.QuerySet`'s model.
|
||||||
|
|
||||||
For more details, including how to delete objects in bulk, see
|
For more details, including how to delete objects in bulk, see
|
||||||
:ref:`topics-db-queries-delete`.
|
:ref:`topics-db-queries-delete`.
|
||||||
@@ -707,6 +711,10 @@ Sometimes with :ref:`multi-table inheritance <multi-table-inheritance>` you may
|
|||||||
want to delete only a child model's data. Specifying ``keep_parents=True`` will
|
want to delete only a child model's data. Specifying ``keep_parents=True`` will
|
||||||
keep the parent model's data.
|
keep the parent model's data.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.1
|
||||||
|
|
||||||
|
Support for the ``DB_*`` variants of ``on_delete`` attribute was added.
|
||||||
|
|
||||||
Pickling objects
|
Pickling objects
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|||||||
@@ -3036,7 +3036,11 @@ unique field in the order that is specified without conflicts. For example::
|
|||||||
|
|
||||||
Performs an SQL delete query on all rows in the :class:`.QuerySet` and
|
Performs an SQL delete query on all rows in the :class:`.QuerySet` and
|
||||||
returns the number of objects deleted and a dictionary with the number of
|
returns the number of objects deleted and a dictionary with the number of
|
||||||
deletions per object type.
|
deletions per object type. The return value will count instances from related
|
||||||
|
models if Django is emulating cascade behavior via Python
|
||||||
|
:attr:`~django.db.models.ForeignKey.on_delete` variants. Otherwise, for
|
||||||
|
database variants such as :attr:`~django.db.models.DB_CASCADE`, the return
|
||||||
|
value will report only instances of the :class:`.QuerySet`'s model.
|
||||||
|
|
||||||
The ``delete()`` is applied instantly. You cannot call ``delete()`` on a
|
The ``delete()`` is applied instantly. You cannot call ``delete()`` on a
|
||||||
:class:`.QuerySet` that has had a slice taken or can otherwise no longer be
|
:class:`.QuerySet` that has had a slice taken or can otherwise no longer be
|
||||||
@@ -3073,13 +3077,20 @@ The ``delete()`` method does a bulk delete and does not call any ``delete()``
|
|||||||
methods on your models. It does, however, emit the
|
methods on your models. It does, however, emit the
|
||||||
:data:`~django.db.models.signals.pre_delete` and
|
:data:`~django.db.models.signals.pre_delete` and
|
||||||
:data:`~django.db.models.signals.post_delete` signals for all deleted objects
|
:data:`~django.db.models.signals.post_delete` signals for all deleted objects
|
||||||
(including cascaded deletions).
|
(including cascaded deletions). Signals won't be sent when ``DB_CASCADE`` is
|
||||||
|
used. Also, ``delete()`` doesn't return information about objects deleted from
|
||||||
|
database variants (``DB_*``) of the
|
||||||
|
:attr:`~django.db.models.ForeignKey.on_delete` argument, e.g. ``DB_CASCADE``.
|
||||||
|
|
||||||
Django needs to fetch objects into memory to send signals and handle cascades.
|
Django won’t need to fetch objects into memory when deleting them in the
|
||||||
However, if there are no cascades and no signals, then Django may take a
|
following cases:
|
||||||
fast-path and delete objects without fetching into memory. For large
|
|
||||||
deletes this can result in significantly reduced memory usage. The amount of
|
#. If related fields use ``DB_*`` options.
|
||||||
executed queries can be reduced, too.
|
#. If there are no cascades and no delete signal receivers.
|
||||||
|
|
||||||
|
In these cases, Django may take a fast path and delete objects without fetching
|
||||||
|
them, which can result in significantly reduced memory usage and fewer executed
|
||||||
|
queries.
|
||||||
|
|
||||||
ForeignKeys which are set to :attr:`~django.db.models.ForeignKey.on_delete`
|
ForeignKeys which are set to :attr:`~django.db.models.ForeignKey.on_delete`
|
||||||
``DO_NOTHING`` do not prevent taking the fast-path in deletion.
|
``DO_NOTHING`` do not prevent taking the fast-path in deletion.
|
||||||
@@ -3087,6 +3098,10 @@ ForeignKeys which are set to :attr:`~django.db.models.ForeignKey.on_delete`
|
|||||||
Note that the queries generated in object deletion is an implementation
|
Note that the queries generated in object deletion is an implementation
|
||||||
detail subject to change.
|
detail subject to change.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.1
|
||||||
|
|
||||||
|
Support for the ``DB_*`` variants of ``on_delete`` attribute was added.
|
||||||
|
|
||||||
``as_manager()``
|
``as_manager()``
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,21 @@ queries:
|
|||||||
|
|
||||||
See :doc:`fetch modes </topics/db/fetch-modes>` for more details.
|
See :doc:`fetch modes </topics/db/fetch-modes>` for more details.
|
||||||
|
|
||||||
|
Database-level delete options for ``ForeignKey.on_delete``
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
:attr:`.ForeignKey.on_delete` now supports database-level delete options:
|
||||||
|
|
||||||
|
* :attr:`~django.db.models.DB_CASCADE`
|
||||||
|
* :attr:`~django.db.models.DB_SET_NULL`
|
||||||
|
* :attr:`~django.db.models.DB_SET_DEFAULT`
|
||||||
|
|
||||||
|
These options handle deletion logic entirely within the database, using the SQL
|
||||||
|
``ON DELETE`` clause. They are thus more efficient than the existing
|
||||||
|
Python-level options, as Django does not need to load objects before deleting
|
||||||
|
them. As a consequence, the :attr:`~django.db.models.DB_CASCADE` option does
|
||||||
|
not trigger the ``pre_delete`` or ``post_delete`` signals.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ArticleProxy(Article):
|
|||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
class Count(models.Model):
|
class Cascade(models.Model):
|
||||||
num = models.PositiveSmallIntegerField()
|
num = models.PositiveSmallIntegerField()
|
||||||
parent = models.ForeignKey("self", models.CASCADE, null=True)
|
parent = models.ForeignKey("self", models.CASCADE, null=True)
|
||||||
|
|
||||||
@@ -48,6 +48,14 @@ class Count(models.Model):
|
|||||||
return str(self.num)
|
return str(self.num)
|
||||||
|
|
||||||
|
|
||||||
|
class DBCascade(models.Model):
|
||||||
|
num = models.PositiveSmallIntegerField()
|
||||||
|
parent = models.ForeignKey("self", models.DB_CASCADE, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.num)
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,17 @@ from django.test.utils import isolate_apps
|
|||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from .models import Article, Car, Count, Event, EventGuide, Location, Site, Vehicle
|
from .models import (
|
||||||
|
Article,
|
||||||
|
Car,
|
||||||
|
Cascade,
|
||||||
|
DBCascade,
|
||||||
|
Event,
|
||||||
|
EventGuide,
|
||||||
|
Location,
|
||||||
|
Site,
|
||||||
|
Vehicle,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NestedObjectsTests(TestCase):
|
class NestedObjectsTests(TestCase):
|
||||||
@@ -34,10 +44,12 @@ class NestedObjectsTests(TestCase):
|
|||||||
Tests for ``NestedObject`` utility collection.
|
Tests for ``NestedObject`` utility collection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
cascade_model = Cascade
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.n = NestedObjects(using=DEFAULT_DB_ALIAS)
|
cls.n = NestedObjects(using=DEFAULT_DB_ALIAS)
|
||||||
cls.objs = [Count.objects.create(num=i) for i in range(5)]
|
cls.objs = [cls.cascade_model.objects.create(num=i) for i in range(5)]
|
||||||
|
|
||||||
def _check(self, target):
|
def _check(self, target):
|
||||||
self.assertEqual(self.n.nested(lambda obj: obj.num), target)
|
self.assertEqual(self.n.nested(lambda obj: obj.num), target)
|
||||||
@@ -103,6 +115,15 @@ class NestedObjectsTests(TestCase):
|
|||||||
n.collect([Vehicle.objects.first()])
|
n.collect([Vehicle.objects.first()])
|
||||||
|
|
||||||
|
|
||||||
|
class DBNestedObjectsTests(NestedObjectsTests):
|
||||||
|
"""
|
||||||
|
Exercise NestedObjectsTests but with a model that makes use of DB_CASCADE
|
||||||
|
instead of CASCADE to ensure proper collection of objects takes place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cascade_model = DBCascade
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(SimpleTestCase):
|
class UtilsTests(SimpleTestCase):
|
||||||
empty_value = "-empty-"
|
empty_value = "-empty-"
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,27 @@ class GenericForeignKeyTests(SimpleTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_content_type_db_on_delete(self):
|
||||||
|
class Model(models.Model):
|
||||||
|
content_type = models.ForeignKey(ContentType, models.DB_CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = GenericForeignKey("content_type", "object_id")
|
||||||
|
|
||||||
|
field = Model._meta.get_field("content_object")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(),
|
||||||
|
[
|
||||||
|
checks.Error(
|
||||||
|
"'Model.content_type' cannot use the database-level on_delete "
|
||||||
|
"variant.",
|
||||||
|
hint="Change the on_delete rule to the non-database variant.",
|
||||||
|
obj=field,
|
||||||
|
id="contenttypes.E006",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_missing_object_id_field(self):
|
def test_missing_object_id_field(self):
|
||||||
class TaggedItem(models.Model):
|
class TaggedItem(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, models.CASCADE)
|
content_type = models.ForeignKey(ContentType, models.CASCADE)
|
||||||
|
|||||||
@@ -41,6 +41,46 @@ class RChildChild(RChild):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedDbOptionGrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedDbOptionParent(models.Model):
|
||||||
|
p = models.ForeignKey(RelatedDbOptionGrandParent, models.DB_CASCADE, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedDbOption(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
db_setnull = models.ForeignKey(
|
||||||
|
RelatedDbOptionParent,
|
||||||
|
models.DB_SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name="db_setnull_set",
|
||||||
|
)
|
||||||
|
db_cascade = models.ForeignKey(
|
||||||
|
RelatedDbOptionParent, models.DB_CASCADE, related_name="db_cascade_set"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetDefaultDbModel(models.Model):
|
||||||
|
db_setdefault = models.ForeignKey(
|
||||||
|
RelatedDbOptionParent,
|
||||||
|
models.DB_SET_DEFAULT,
|
||||||
|
db_default=models.Value(1),
|
||||||
|
related_name="db_setdefault_set",
|
||||||
|
)
|
||||||
|
db_setdefault_none = models.ForeignKey(
|
||||||
|
RelatedDbOptionParent,
|
||||||
|
models.DB_SET_DEFAULT,
|
||||||
|
db_default=None,
|
||||||
|
null=True,
|
||||||
|
related_name="db_setnull_nullable_set",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {"supports_on_delete_db_default"}
|
||||||
|
|
||||||
|
|
||||||
class A(models.Model):
|
class A(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
|
|
||||||
@@ -119,6 +159,15 @@ def create_a(name):
|
|||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def create_related_db_option(name):
|
||||||
|
a = RelatedDbOption(name=name)
|
||||||
|
for name in ["db_setnull", "db_cascade"]:
|
||||||
|
r = RelatedDbOptionParent.objects.create()
|
||||||
|
setattr(a, name, r)
|
||||||
|
a.save()
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
class M(models.Model):
|
class M(models.Model):
|
||||||
m2m = models.ManyToManyField(R, related_name="m_set")
|
m2m = models.ManyToManyField(R, related_name="m_set")
|
||||||
m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
|
m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
|
||||||
|
|||||||
@@ -34,11 +34,16 @@ from .models import (
|
|||||||
RChild,
|
RChild,
|
||||||
RChildChild,
|
RChildChild,
|
||||||
Referrer,
|
Referrer,
|
||||||
|
RelatedDbOption,
|
||||||
|
RelatedDbOptionGrandParent,
|
||||||
|
RelatedDbOptionParent,
|
||||||
RProxy,
|
RProxy,
|
||||||
S,
|
S,
|
||||||
|
SetDefaultDbModel,
|
||||||
T,
|
T,
|
||||||
User,
|
User,
|
||||||
create_a,
|
create_a,
|
||||||
|
create_related_db_option,
|
||||||
get_default_r,
|
get_default_r,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,18 +81,48 @@ class OnDeleteTests(TestCase):
|
|||||||
a = A.objects.get(pk=a.pk)
|
a = A.objects.get(pk=a.pk)
|
||||||
self.assertIsNone(a.setnull)
|
self.assertIsNone(a.setnull)
|
||||||
|
|
||||||
|
def test_db_setnull(self):
|
||||||
|
a = create_related_db_option("db_setnull")
|
||||||
|
a.db_setnull.delete()
|
||||||
|
a = RelatedDbOption.objects.get(pk=a.pk)
|
||||||
|
self.assertIsNone(a.db_setnull)
|
||||||
|
|
||||||
def test_setdefault(self):
|
def test_setdefault(self):
|
||||||
a = create_a("setdefault")
|
a = create_a("setdefault")
|
||||||
a.setdefault.delete()
|
a.setdefault.delete()
|
||||||
a = A.objects.get(pk=a.pk)
|
a = A.objects.get(pk=a.pk)
|
||||||
self.assertEqual(self.DEFAULT, a.setdefault.pk)
|
self.assertEqual(self.DEFAULT, a.setdefault.pk)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_on_delete_db_default")
|
||||||
|
def test_db_setdefault(self):
|
||||||
|
# Object cannot be created on the module initialization, use hardcoded
|
||||||
|
# PKs instead.
|
||||||
|
r = RelatedDbOptionParent.objects.create(pk=2)
|
||||||
|
default_r = RelatedDbOptionParent.objects.create(pk=1)
|
||||||
|
set_default_db_obj = SetDefaultDbModel.objects.create(db_setdefault=r)
|
||||||
|
set_default_db_obj.db_setdefault.delete()
|
||||||
|
set_default_db_obj = SetDefaultDbModel.objects.get(pk=set_default_db_obj.pk)
|
||||||
|
self.assertEqual(set_default_db_obj.db_setdefault, default_r)
|
||||||
|
|
||||||
def test_setdefault_none(self):
|
def test_setdefault_none(self):
|
||||||
a = create_a("setdefault_none")
|
a = create_a("setdefault_none")
|
||||||
a.setdefault_none.delete()
|
a.setdefault_none.delete()
|
||||||
a = A.objects.get(pk=a.pk)
|
a = A.objects.get(pk=a.pk)
|
||||||
self.assertIsNone(a.setdefault_none)
|
self.assertIsNone(a.setdefault_none)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_on_delete_db_default")
|
||||||
|
def test_db_setdefault_none(self):
|
||||||
|
# Object cannot be created on the module initialization, use hardcoded
|
||||||
|
# PKs instead.
|
||||||
|
r = RelatedDbOptionParent.objects.create(pk=2)
|
||||||
|
default_r = RelatedDbOptionParent.objects.create(pk=1)
|
||||||
|
set_default_db_obj = SetDefaultDbModel.objects.create(
|
||||||
|
db_setdefault_none=r, db_setdefault=default_r
|
||||||
|
)
|
||||||
|
set_default_db_obj.db_setdefault_none.delete()
|
||||||
|
set_default_db_obj = SetDefaultDbModel.objects.get(pk=set_default_db_obj.pk)
|
||||||
|
self.assertIsNone(set_default_db_obj.db_setdefault_none)
|
||||||
|
|
||||||
def test_cascade(self):
|
def test_cascade(self):
|
||||||
a = create_a("cascade")
|
a = create_a("cascade")
|
||||||
a.cascade.delete()
|
a.cascade.delete()
|
||||||
@@ -359,6 +394,22 @@ class DeletionTests(TestCase):
|
|||||||
self.assertNumQueries(5, s.delete)
|
self.assertNumQueries(5, s.delete)
|
||||||
self.assertFalse(S.objects.exists())
|
self.assertFalse(S.objects.exists())
|
||||||
|
|
||||||
|
def test_db_cascade(self):
|
||||||
|
related_db_op = RelatedDbOptionParent.objects.create(
|
||||||
|
p=RelatedDbOptionGrandParent.objects.create()
|
||||||
|
)
|
||||||
|
RelatedDbOption.objects.bulk_create(
|
||||||
|
[
|
||||||
|
RelatedDbOption(db_cascade=related_db_op)
|
||||||
|
for _ in range(2 * GET_ITERATOR_CHUNK_SIZE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
with self.assertNumQueries(1):
|
||||||
|
results = related_db_op.delete()
|
||||||
|
self.assertEqual(results, (1, {"delete.RelatedDbOptionParent": 1}))
|
||||||
|
self.assertFalse(RelatedDbOption.objects.exists())
|
||||||
|
self.assertFalse(RelatedDbOptionParent.objects.exists())
|
||||||
|
|
||||||
def test_instance_update(self):
|
def test_instance_update(self):
|
||||||
deleted = []
|
deleted = []
|
||||||
related_setnull_sets = []
|
related_setnull_sets = []
|
||||||
|
|||||||
@@ -3062,3 +3062,64 @@ class ConstraintsTests(TestCase):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@isolate_apps("invalid_models_tests")
|
||||||
|
class RelatedFieldTests(SimpleTestCase):
|
||||||
|
def test_on_delete_python_db_variants(self):
|
||||||
|
class Artist(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Album(models.Model):
|
||||||
|
artist = models.ForeignKey(Artist, models.CASCADE)
|
||||||
|
|
||||||
|
class Song(models.Model):
|
||||||
|
album = models.ForeignKey(Album, models.RESTRICT)
|
||||||
|
artist = models.ForeignKey(Artist, models.DB_CASCADE)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Song.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"The model cannot have related fields with both database-level and "
|
||||||
|
"Python-level on_delete variants.",
|
||||||
|
obj=Song,
|
||||||
|
id="models.E050",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_on_delete_python_db_variants_auto_created(self):
|
||||||
|
class SharedModel(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Child(SharedModel):
|
||||||
|
parent = models.ForeignKey(Parent, on_delete=models.DB_CASCADE)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Child.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"The model cannot have related fields with both database-level and "
|
||||||
|
"Python-level on_delete variants.",
|
||||||
|
obj=Child,
|
||||||
|
id="models.E050",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_on_delete_db_do_nothing(self):
|
||||||
|
class Artist(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Album(models.Model):
|
||||||
|
artist = models.ForeignKey(Artist, models.CASCADE)
|
||||||
|
|
||||||
|
class Song(models.Model):
|
||||||
|
album = models.ForeignKey(Album, models.DO_NOTHING)
|
||||||
|
artist = models.ForeignKey(Artist, models.DB_CASCADE)
|
||||||
|
|
||||||
|
self.assertEqual(Song.check(databases=self.databases), [])
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from unittest import mock
|
|||||||
from django.core.checks import Error
|
from django.core.checks import Error
|
||||||
from django.core.checks import Warning as DjangoWarning
|
from django.core.checks import Warning as DjangoWarning
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
from django.test.testcases import SimpleTestCase
|
from django.test import skipUnlessDBFeature
|
||||||
|
from django.test.testcases import SimpleTestCase, TestCase
|
||||||
from django.test.utils import isolate_apps, modify_settings, override_settings
|
from django.test.utils import isolate_apps, modify_settings, override_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -751,6 +752,29 @@ class RelativeFieldTests(SimpleTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_on_delete_db_set_null_on_non_nullable_field(self):
|
||||||
|
class Person(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Model(models.Model):
|
||||||
|
foreign_key = models.ForeignKey("Person", models.DB_SET_NULL)
|
||||||
|
|
||||||
|
field = Model._meta.get_field("foreign_key")
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"Field specifies on_delete=DB_SET_NULL, but cannot be null.",
|
||||||
|
hint=(
|
||||||
|
"Set null=True argument on the field, or change the on_delete "
|
||||||
|
"rule."
|
||||||
|
),
|
||||||
|
obj=field,
|
||||||
|
id="fields.E320",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_on_delete_set_default_without_default_value(self):
|
def test_on_delete_set_default_without_default_value(self):
|
||||||
class Person(models.Model):
|
class Person(models.Model):
|
||||||
pass
|
pass
|
||||||
@@ -2259,3 +2283,175 @@ class M2mThroughFieldsTests(SimpleTestCase):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@isolate_apps("invalid_models_tests")
|
||||||
|
class DatabaseLevelOnDeleteTests(TestCase):
|
||||||
|
|
||||||
|
def test_db_set_default_support(self):
|
||||||
|
class Parent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
Parent, models.DB_SET_DEFAULT, db_default=models.Value(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
expected = (
|
||||||
|
[]
|
||||||
|
if connection.features.supports_on_delete_db_default
|
||||||
|
else [
|
||||||
|
Error(
|
||||||
|
f"{connection.display_name} does not support a DB_SET_DEFAULT.",
|
||||||
|
hint="Change the on_delete rule to SET_DEFAULT.",
|
||||||
|
obj=field,
|
||||||
|
id="fields.E324",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(field.check(databases=self.databases), expected)
|
||||||
|
|
||||||
|
def test_db_set_default_required_db_features(self):
|
||||||
|
class Parent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
Parent, models.DB_SET_DEFAULT, db_default=models.Value(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {"supports_on_delete_db_default"}
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(field.check(databases=self.databases), [])
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_on_delete_db_default")
|
||||||
|
def test_db_set_default_no_db_default(self):
|
||||||
|
class Parent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, models.DB_SET_DEFAULT)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"Field specifies on_delete=DB_SET_DEFAULT, but has no db_default "
|
||||||
|
"value.",
|
||||||
|
hint="Set a db_default value, or change the on_delete rule.",
|
||||||
|
obj=field,
|
||||||
|
id="fields.E322",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_python_db_chain(self):
|
||||||
|
class GrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(models.Model):
|
||||||
|
grand_parent = models.ForeignKey(GrandParent, models.DB_CASCADE)
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, models.RESTRICT)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"Field specifies Python-level on_delete variant, but referenced "
|
||||||
|
"model uses database-level variant.",
|
||||||
|
hint=(
|
||||||
|
"Use either database or Python on_delete variants uniformly in "
|
||||||
|
"the references chain."
|
||||||
|
),
|
||||||
|
obj=field,
|
||||||
|
id="fields.E323",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_db_python_chain(self):
|
||||||
|
class GrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(models.Model):
|
||||||
|
grand_parent = models.ForeignKey(GrandParent, models.CASCADE)
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, models.DB_SET_NULL, null=True)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"Field specifies database-level on_delete variant, but referenced "
|
||||||
|
"model uses Python-level variant.",
|
||||||
|
hint=(
|
||||||
|
"Use either database or Python on_delete variants uniformly in "
|
||||||
|
"the references chain."
|
||||||
|
),
|
||||||
|
obj=field,
|
||||||
|
id="fields.E323",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_db_python_chain_auto_created(self):
|
||||||
|
class GrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(GrandParent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, on_delete=models.DB_CASCADE)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(
|
||||||
|
field.check(databases=self.databases),
|
||||||
|
[
|
||||||
|
Error(
|
||||||
|
"Field specifies database-level on_delete variant, but referenced "
|
||||||
|
"model uses Python-level variant.",
|
||||||
|
hint=(
|
||||||
|
"Use either database or Python on_delete variants uniformly in "
|
||||||
|
"the references chain."
|
||||||
|
),
|
||||||
|
obj=field,
|
||||||
|
id="fields.E323",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_db_do_nothing_chain(self):
|
||||||
|
class GrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(models.Model):
|
||||||
|
grand_parent = models.ForeignKey(GrandParent, models.DO_NOTHING)
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, models.DB_SET_NULL, null=True)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(field.check(databases=self.databases), [])
|
||||||
|
|
||||||
|
def test_do_nothing_db_chain(self):
|
||||||
|
class GrandParent(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parent(models.Model):
|
||||||
|
grand_parent = models.ForeignKey(GrandParent, models.DB_SET_NULL, null=True)
|
||||||
|
|
||||||
|
class Child(models.Model):
|
||||||
|
parent = models.ForeignKey(Parent, models.DO_NOTHING)
|
||||||
|
|
||||||
|
field = Child._meta.get_field("parent")
|
||||||
|
self.assertEqual(field.check(databases=self.databases), [])
|
||||||
|
|||||||
@@ -971,6 +971,23 @@ class WriterTests(SimpleTestCase):
|
|||||||
("('models.Model', {'from django.db import models'})", set()),
|
("('models.Model', {'from django.db import models'})", set()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_database_on_delete_serializer_value(self):
|
||||||
|
db_level_on_delete_options = [
|
||||||
|
models.DB_CASCADE,
|
||||||
|
models.DB_SET_DEFAULT,
|
||||||
|
models.DB_SET_NULL,
|
||||||
|
]
|
||||||
|
for option in db_level_on_delete_options:
|
||||||
|
self.assertSerializedEqual(option)
|
||||||
|
self.assertSerializedResultEqual(
|
||||||
|
MigrationWriter.serialize(option),
|
||||||
|
(
|
||||||
|
f"('django.db.models.deletion.{option.__name__}', "
|
||||||
|
"{'import django.db.models.deletion'})",
|
||||||
|
set(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_simple_migration(self):
|
def test_simple_migration(self):
|
||||||
"""
|
"""
|
||||||
Tests serializing a simple migration.
|
Tests serializing a simple migration.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from django.db import (
|
|||||||
from django.db.backends.utils import truncate_name
|
from django.db.backends.utils import truncate_name
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
CASCADE,
|
CASCADE,
|
||||||
|
DB_CASCADE,
|
||||||
|
DB_SET_NULL,
|
||||||
PROTECT,
|
PROTECT,
|
||||||
AutoField,
|
AutoField,
|
||||||
BigAutoField,
|
BigAutoField,
|
||||||
@@ -410,6 +412,40 @@ class SchemaTests(TransactionTestCase):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("can_create_inline_fk")
|
||||||
|
def test_inline_fk_db_on_delete(self):
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(Author)
|
||||||
|
editor.create_model(Book)
|
||||||
|
editor.create_model(Note)
|
||||||
|
self.assertForeignKeyNotExists(Note, "book_id", "schema_book")
|
||||||
|
# Add a foreign key from model to the other.
|
||||||
|
with (
|
||||||
|
CaptureQueriesContext(connection) as ctx,
|
||||||
|
connection.schema_editor() as editor,
|
||||||
|
):
|
||||||
|
new_field = ForeignKey(Book, DB_CASCADE)
|
||||||
|
new_field.set_attributes_from_name("book")
|
||||||
|
editor.add_field(Note, new_field)
|
||||||
|
self.assertForeignKeyExists(Note, "book_id", "schema_book")
|
||||||
|
# Creating a FK field with a constraint uses a single statement without
|
||||||
|
# a deferred ALTER TABLE.
|
||||||
|
self.assertFalse(
|
||||||
|
[
|
||||||
|
sql
|
||||||
|
for sql in (str(statement) for statement in editor.deferred_sql)
|
||||||
|
if sql.startswith("ALTER TABLE") and "ADD CONSTRAINT" in sql
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# ON DELETE clause is used.
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
capture_query["sql"].startswith("ALTER TABLE")
|
||||||
|
and "ON DELETE" in capture_query["sql"]
|
||||||
|
for capture_query in ctx.captured_queries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@skipUnlessDBFeature("can_create_inline_fk")
|
@skipUnlessDBFeature("can_create_inline_fk")
|
||||||
def test_add_inline_fk_update_data(self):
|
def test_add_inline_fk_update_data(self):
|
||||||
with connection.schema_editor() as editor:
|
with connection.schema_editor() as editor:
|
||||||
@@ -566,6 +602,63 @@ class SchemaTests(TransactionTestCase):
|
|||||||
editor.alter_field(Author, new_field2, new_field, strict=True)
|
editor.alter_field(Author, new_field2, new_field, strict=True)
|
||||||
self.assertForeignKeyNotExists(Author, "tag_id", "schema_tag")
|
self.assertForeignKeyNotExists(Author, "tag_id", "schema_tag")
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_foreign_keys", "can_introspect_foreign_keys")
|
||||||
|
def test_fk_alter_on_delete(self):
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(Author)
|
||||||
|
editor.create_model(Book)
|
||||||
|
self.assertForeignKeyExists(Book, "author_id", "schema_author")
|
||||||
|
# Change CASCADE to DB_CASCADE.
|
||||||
|
old_field = Book._meta.get_field("author")
|
||||||
|
new_field = ForeignKey(Author, DB_CASCADE)
|
||||||
|
new_field.set_attributes_from_name("author")
|
||||||
|
with (
|
||||||
|
connection.schema_editor() as editor,
|
||||||
|
CaptureQueriesContext(connection) as ctx,
|
||||||
|
):
|
||||||
|
editor.alter_field(Book, old_field, new_field)
|
||||||
|
self.assertForeignKeyExists(Book, "author_id", "schema_author")
|
||||||
|
self.assertIs(
|
||||||
|
any("ON DELETE" in query["sql"] for query in ctx.captured_queries), True
|
||||||
|
)
|
||||||
|
# Change DB_CASCADE to CASCADE.
|
||||||
|
old_field = new_field
|
||||||
|
new_field = ForeignKey(Author, CASCADE)
|
||||||
|
new_field.set_attributes_from_name("author")
|
||||||
|
with (
|
||||||
|
connection.schema_editor() as editor,
|
||||||
|
CaptureQueriesContext(connection) as ctx,
|
||||||
|
):
|
||||||
|
editor.alter_field(Book, old_field, new_field)
|
||||||
|
self.assertForeignKeyExists(Book, "author_id", "schema_author")
|
||||||
|
self.assertIs(
|
||||||
|
any("ON DELETE" in query["sql"] for query in ctx.captured_queries), False
|
||||||
|
)
|
||||||
|
|
||||||
|
@isolate_apps("schema")
|
||||||
|
@skipUnlessDBFeature("supports_foreign_keys", "can_introspect_foreign_keys")
|
||||||
|
def test_create_model_db_on_delete(self):
|
||||||
|
class Parent(Model):
|
||||||
|
class Meta:
|
||||||
|
app_label = "schema"
|
||||||
|
|
||||||
|
class Child(Model):
|
||||||
|
parent_fk = ForeignKey(Parent, DB_SET_NULL, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "schema"
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(Parent)
|
||||||
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(Child)
|
||||||
|
|
||||||
|
self.assertForeignKeyNotExists(Child, "parent_id", "schema_parent")
|
||||||
|
self.assertIs(
|
||||||
|
any("ON DELETE" in query["sql"] for query in ctx.captured_queries), True
|
||||||
|
)
|
||||||
|
|
||||||
@isolate_apps("schema")
|
@isolate_apps("schema")
|
||||||
def test_no_db_constraint_added_during_primary_key_change(self):
|
def test_no_db_constraint_added_during_primary_key_change(self):
|
||||||
"""
|
"""
|
||||||
@@ -4598,6 +4691,7 @@ class SchemaTests(TransactionTestCase):
|
|||||||
"to_table": editor.quote_name(table),
|
"to_table": editor.quote_name(table),
|
||||||
"to_column": editor.quote_name(model._meta.auto_field.column),
|
"to_column": editor.quote_name(model._meta.auto_field.column),
|
||||||
"deferrable": connection.ops.deferrable_sql(),
|
"deferrable": connection.ops.deferrable_sql(),
|
||||||
|
"on_delete_db": "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
@@ -4784,7 +4878,7 @@ class SchemaTests(TransactionTestCase):
|
|||||||
error_messages={"invalid": "error message"},
|
error_messages={"invalid": "error message"},
|
||||||
help_text="help text",
|
help_text="help text",
|
||||||
limit_choices_to={"limit": "choice"},
|
limit_choices_to={"limit": "choice"},
|
||||||
on_delete=PROTECT,
|
on_delete=CASCADE,
|
||||||
related_name="related_name",
|
related_name="related_name",
|
||||||
related_query_name="related_query_name",
|
related_query_name="related_query_name",
|
||||||
validators=[lambda x: x],
|
validators=[lambda x: x],
|
||||||
|
|||||||
Reference in New Issue
Block a user