Skip to content

feat: Require explicit on_delete for ForeignKeyField #1896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Changed
^^^^^^^
- Skip database selection if the router is not configured to improve performance (#1915)
- `.values()`, `.values_list()` and `.only()` cannot be used together (#1923)
- ForeignKeyField: The `on_delete` parameter is now mandatory. Users must now explicitly specify the desired deletion behavior (e.g., `on_delete=fields.RESTRICT`, `on_delete=fields.CASCADE`, etc.) (#1801)

Added
^^^^^
Expand Down
4 changes: 2 additions & 2 deletions docs/contrib/pydantic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ We define our models with a relationship:
created_at = fields.DatetimeField(auto_now_add=True)

tournament = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="The Tournament this happens in"
"models.Tournament", related_name="events", description="The Tournament this happens in", on_delete=fields.RESTRICT
)

Next we create our `Pydantic Model <https://docs.pydantic.dev/latest/concepts/models/>`__ using ``pydantic_model_creator``:
Expand Down Expand Up @@ -573,7 +573,7 @@ Let's add some methods that calculate data, and tell the creators to use them:
created_at = fields.DatetimeField(auto_now_add=True)

tournament = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="The Tournament this happens in"
"models.Tournament", related_name="events", description="The Tournament this happens in", on_delete=fields.RESTRICT
)

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Define the models by inheriting from ``tortoise.models.Model``.
name = fields.CharField(max_length=255)
# References to other models are defined in format
# "{app_name}.{model_name}" - where {app_name} is defined in the tortoise config
tournament = fields.ForeignKeyField('models.Tournament', related_name='events')
tournament = fields.ForeignKeyField('models.Tournament', related_name='events', on_delete=fields.RESTRICT)
participants = fields.ManyToManyField('models.Team', related_name='events', through='event_team')


Expand Down
95 changes: 91 additions & 4 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ With that start describing the models
class Event(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
tournament = fields.ForeignKeyField('models.Tournament', related_name='events')
tournament = fields.ForeignKeyField('models.Tournament', related_name='events', on_delete=fields.RESTRICT)
participants = fields.ManyToManyField('models.Team', related_name='events', through='event_team')
modified = fields.DatetimeField(auto_now=True)
prize = fields.DecimalField(max_digits=10, decimal_places=2, null=True)
Expand Down Expand Up @@ -249,14 +249,14 @@ The ``Meta`` class

.. code-block:: python3

tournament = fields.ForeignKeyField('models.Tournament', related_name='events')
tournament = fields.ForeignKeyField('models.Tournament', related_name='events', on_delete=fields.RESTRICT)
participants = fields.ManyToManyField('models.Team', related_name='events')
modified = fields.DatetimeField(auto_now=True)
prize = fields.DecimalField(max_digits=10, decimal_places=2, null=True)

In event model we got some more fields, that could be interesting for us.

``fields.ForeignKeyField('models.Tournament', related_name='events')``
``fields.ForeignKeyField('models.Tournament', related_name='events', on_delete=fields.RESTRICT)``
Here we create foreign key reference to tournament. We create it by referring to model by it's literal, consisting of app name and model name. ``models`` is default app name, but you can change it in ``class Meta`` with ``app = 'other'``.
``related_name``
Is keyword argument, that defines field for related query on referenced models, so with that you could fetch all tournaments's events with like this:
Expand Down Expand Up @@ -355,6 +355,93 @@ To get the Reverse-FK, e.g. an `event.tournament` we currently only support the
await event.fetch_related('tournament')
tournament = event.tournament

.. _on-delete:

ForeignKeyField: The on_delete Parameter
----------------------------------------

The ``on_delete`` parameter is a **mandatory** argument for the :class:`~tortoise.fields.ForeignKeyField`. It specifies the behavior when a referenced object is deleted. Previously, ``on_delete`` defaulted to ``CASCADE``, but this behavior has been removed to prevent accidental data loss. You *must* now explicitly specify the desired behavior.

Here are the possible values for ``on_delete``:

* **``fields.CASCADE``**: When the referenced object is deleted, also delete the objects that have references to it. **WARNING:** This can lead to data loss if not used carefully. Consider the implications before using ``CASCADE``.

.. code-block:: python3

from tortoise import fields
from tortoise.models import Model

class Parent(Model):
id = fields.IntField(primary_key=True)

class Child(Model):
id = fields.IntField(primary_key=True)
parent = fields.ForeignKeyField("models.Parent", on_delete=fields.CASCADE)
# When a Parent object is deleted, all related Child objects will also be deleted.

* **``fields.RESTRICT``**: Prevent deletion of the referenced object if it is referenced by any other objects. This is the **recommended** option for most cases, as it prevents accidental data loss. Attempting to delete a referenced object will raise a ``tortoise.exceptions.IntegrityError``.

.. code-block:: python3

from tortoise import fields
from tortoise.models import Model

class Parent(Model):
id = fields.IntField(primary_key=True)

class Child(Model):
id = fields.IntField(primary_key=True)
parent = fields.ForeignKeyField("models.Parent", on_delete=fields.RESTRICT)
# Attempting to delete a Parent object that has related Child objects will raise an error.

* **``fields.SET_NULL``**: When the referenced object is deleted, set the foreign key field to ``NULL``. This option is only valid if the ``ForeignKeyField`` has ``null=True``.

.. code-block:: python3

from tortoise import fields
from tortoise.models import Model

class Parent(Model):
id = fields.IntField(primary_key=True)

class Child(Model):
id = fields.IntField(primary_key=True)
parent = fields.ForeignKeyField("models.Parent", on_delete=fields.SET_NULL, null=True)
# When a Parent object is deleted, the 'parent' field in related Child objects will be set to NULL.

* **``fields.SET_DEFAULT``**: When the referenced object is deleted, set the foreign key field to its default value. This option is only valid if the ``ForeignKeyField`` has a ``default`` value specified.

.. code-block:: python3

from tortoise import fields
from tortoise.models import Model

class Parent(Model):
id = fields.IntField(primary_key=True)

class Child(Model):
id = fields.IntField(primary_key=True)
parent = fields.ForeignKeyField("models.Parent", on_delete=fields.SET_DEFAULT, default=1)
# When a Parent object is deleted, the 'parent' field in related Child objects will be set to its default value (1 in this case).

* **``fields.NO_ACTION``**: Take no action on the database when the referenced object is deleted. This means that the database will enforce referential integrity, and you will get an error if you try to delete a referenced object. This option is rarely used and its behavior may depend on the specific database backend. It is generally recommended to use ``RESTRICT`` instead.

.. code-block:: python3

from tortoise import fields
from tortoise.models import Model

class Parent(Model):
id = fields.IntField(primary_key=True)

class Child(Model):
id = fields.IntField(primary_key=True)
parent = fields.ForeignKeyField("models.Parent", on_delete=fields.NO_ACTION)
# Attempting to delete a Parent object that has related Child objects will raise a database error.

**Choosing the Right ``on_delete`` Value:**

The best ``on_delete`` value depends on your specific application and the relationship between your models. In most cases, ``RESTRICT`` is the safest and most appropriate option. Consider the implications of each option carefully before making a decision. Always prioritize data integrity and avoid accidental data loss.

``ManyToManyField``
-------------------
Expand Down Expand Up @@ -426,7 +513,7 @@ all models including fields for the relations between models.
id = fields.IntField(primary_key=True)
name = fields.CharField(max_length=255)
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/complex_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Event(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/complex_prefetching.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Event(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Event(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/global_table_name_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class BlogPost(Model):
id = fields.IntField(primary_key=True)
title = fields.TextField()
author: fields.ForeignKeyRelation[UserProfile] = fields.ForeignKeyField(
"models.UserProfile", related_name="posts"
"models.UserProfile", related_name="posts", on_delete=fields.RESTRICT
)

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion examples/group_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Author(Model):
class Book(Model):
name = fields.CharField(max_length=255)
author: fields.ForeignKeyRelation[Author] = fields.ForeignKeyField(
"models.Author", related_name="books"
"models.Author", related_name="books", on_delete=fields.RESTRICT
)
rating = fields.FloatField()

Expand Down
2 changes: 1 addition & 1 deletion examples/pydantic/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Event(Model):
name = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
tournament: fields.ForeignKeyNullableRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", null=True
"models.Tournament", related_name="events", null=True, on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/pydantic/early_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Event(Model):
name = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
tournament: fields.ForeignKeyNullableRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", null=True
"models.Tournament", related_name="events", null=True, on_delete=fields.RESTRICT
)

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion examples/pydantic/recursive.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Employee(Model):
name = fields.CharField(max_length=50)

manager: fields.ForeignKeyNullableRelation["Employee"] = fields.ForeignKeyField(
"models.Employee", related_name="team_members", null=True
"models.Employee", related_name="team_members", null=True, on_delete=fields.RESTRICT
)
team_members: fields.ReverseRelation["Employee"]

Expand Down
5 changes: 4 additions & 1 deletion examples/pydantic/tutorial_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ class Event(Model):
created_at = fields.DatetimeField(auto_now_add=True)

tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="The Tournament this happens in"
"models.Tournament",
related_name="events",
description="The Tournament this happens in",
on_delete=fields.RESTRICT,
)


Expand Down
5 changes: 4 additions & 1 deletion examples/pydantic/tutorial_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ class Event(Model):
created_at = fields.DatetimeField(auto_now_add=True)

tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="The Tournament this happens in"
"models.Tournament",
related_name="events",
description="The Tournament this happens in",
on_delete=fields.RESTRICT,
)

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion examples/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Event(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
Expand Down
2 changes: 1 addition & 1 deletion examples/relations_recursive.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Employee(Model):
name = fields.CharField(max_length=50)

manager: fields.ForeignKeyNullableRelation["Employee"] = fields.ForeignKeyField(
"models.Employee", related_name="team_members", null=True
"models.Employee", related_name="team_members", null=True, on_delete=fields.RESTRICT
)
team_members: fields.ReverseRelation["Employee"]

Expand Down
2 changes: 1 addition & 1 deletion examples/relations_with_unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Student(Model):
id = fields.IntField(primary_key=True)
name = fields.TextField()
school: fields.ForeignKeyRelation[School] = fields.ForeignKeyField(
"models.School", related_name="students", to_field="id"
"models.School", related_name="students", to_field="id", on_delete=fields.RESTRICT
)


Expand Down
5 changes: 4 additions & 1 deletion examples/schema_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ class Event(Model):
id = fields.IntField(primary_key=True, description="Event ID")
name = fields.CharField(max_length=255, unique=True)
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="FK to tournament"
"models.Tournament",
related_name="events",
description="FK to tournament",
on_delete=fields.RESTRICT,
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team",
Expand Down
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"app.Tournament", related_name="events"
"app.Tournament", related_name="events", on_delete=fields.RESTRICT
)
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel2.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Any] = fields.ForeignKeyField(
"models.Tour", related_name="events"
"models.Tour", related_name="events", on_delete=fields.RESTRICT
)
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel3.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"Tournament", related_name="events"
"Tournament", related_name="events", on_delete=fields.RESTRICT
)
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel4.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.app.Tournament", related_name="events"
"models.app.Tournament", related_name="events", on_delete=fields.RESTRICT
)
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel6.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", to_field="uuid"
"models.Tournament", related_name="events", to_field="uuid", on_delete=fields.RESTRICT
)
2 changes: 1 addition & 1 deletion tests/model_setup/model_bad_rel7.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", to_field="uuids"
"models.Tournament", related_name="events", to_field="uuids", on_delete=fields.RESTRICT
)
4 changes: 2 additions & 2 deletions tests/model_setup/models_dup1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ class Tournament(Model):

class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)


class Party(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
"models.Tournament", related_name="events", on_delete=fields.RESTRICT
)
6 changes: 3 additions & 3 deletions tests/schema/models_cyclic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@

class One(Model):
tournament: fields.ForeignKeyRelation["Two"] = fields.ForeignKeyField(
"models.Two", related_name="events"
"models.Two", related_name="events", on_delete=fields.RESTRICT
)


class Two(Model):
tournament: fields.ForeignKeyRelation["Three"] = fields.ForeignKeyField(
"models.Three", related_name="events"
"models.Three", related_name="events", on_delete=fields.RESTRICT
)


class Three(Model):
tournament: fields.ForeignKeyRelation[One] = fields.ForeignKeyField(
"models.One", related_name="events"
"models.One", related_name="events", on_delete=fields.RESTRICT
)
4 changes: 3 additions & 1 deletion tests/schema/models_fk_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@


class One(Model):
tournament: fields.ForeignKeyRelation[Any] = fields.ForeignKeyField("moo")
tournament: fields.ForeignKeyRelation[Any] = fields.ForeignKeyField(
"moo", on_delete=fields.RESTRICT
)
Loading
Loading