このページでは、Django における on_delete
について解説します。
Django ではモデルとモデルの間にリレーションを設定することが可能であり、リレーションを設定する際には下記のようなフィールドをモデルに持たせることになります。
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.CASCADE)
このようなモデルの定義を、皆さんも実際に自身で実装したり、ウェブサイトのソースコード等で見かけたりしたことがあるのではないでしょうか?
今回注目したいのが、上記における on_delete
引数になります。この on_delete
引数は、ForeignKey
もしくは OneToOneField
を利用してリレーションを設定する際に必須の引数となります。
では、この on_delete
引数の設定によってアプリの動作はどのように変化するのでしょうか?
この点について、本ページで解説していきたいと思います!
Contents
on_delete
では、まずは on_delete
について解説していきます。
結論を先に言っておくと、on_delete
は、リレーションを構築した2つのオブジェクトにおいて、子オブジェクトを持つ親オブジェクト削除時の「アプリの動作」を設定する引数になります。
いきなりこの結論を見ただけだと意味合いが分かりにくいかもしれないので、リレーションや親子関係等について説明をしたのちに、再度 on_delete
の意味合いについて説明していきたいと思います。
リレーションの設定と構築
前述でも少し触れたように、Django においてはモデルとモデルの間にリレーションを設定することが可能です。
このリレーションの設定は「リレーションを示すフィールド」をモデルに持たせることで実現することが出来ます。
「リレーションを示すフィールド」の代表例が ForeignKey
や OneToOneField
であり、これらの第1引数(もしくは to
引数)で指定したモデルと、このフィールドを持たせたモデルの間にリレーションが設定されることになります。
このリレーションを設定するモデル定義の例は下記のようになります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.CASCADE)
この場合、ForeignKey
の第1引数には User
を指定しているため、Comment
と User
の間にリレーションが設定されることになります(User
は Django に標準で用意されているユーザー管理モデルになります)。
モデル定義で行えるのは「リレーションの設定のみ」となります。要はモデルのオブジェクト同士でリレーションを構築可能にするための設定を行うだけとなります。
実際には、モデルのオブジェクトを生成し、そのオブジェクト同士にリレーションを構築していくことになります。そして、そのオブジェクト同士のリレーションは、モデルに持たせたリレーションを示すフィールドに「リレーションを構築したいオブジェクト」をセットすることで実現することが出来ます。
例えば、上記のモデルの定義であれば、Comment
のオブジェクトがリレーションを示すフィールドを contributor
として持っているため、この contributor
に User
のオブジェクトをセットすることで、Comment
のオブジェクトと User
のオブジェクトとの間にリレーションが構築されることになります。
このリレーションの構築の処理を簡単なスクリプトの例で示すと下記のようなものになります(実際には、同様の処理を views.py
や models.py
等で実施することになりますし、今回は省略していますがデータベースに反映するためには save
メソッドの実行も必要になります)。
from django.contrib.auth.models import User
from . import models
user_x = User(username='山田太郎')
comment_1 = models.Comment()
comment_1.contributor = user_x
comment_2 = models.Comment()
comment_2.contributor = user_x
上記のスクリプトの例においては、comment_1
& comment_2
と user_x
との間にリレーションが構築されることになります。
この場合、1つのオブジェクト(user_x
)と複数のオブジェクト(comment_1
と comment_2
)との間にリレーションが構築されることになります。このような1対多のリレーションを構築することが出来るのはリレーションを示すフィールドとして ForeignKey
を指定しているためです。OneToOneField
を利用した場合、リレーションが構築できるのは1対1の関係のもののみとなります。
スポンサーリンク
リレーションにおける親子関係
on_delete
の意味合いを理解しやすくするため、次はリレーションにおける親子関係について説明していきます。
リレーションには親子関係が存在し、モデルの定義において、ForeignKey
(or OneToOneField
)の第1引数に指定したモデルが親モデルとなります(もしくは to
引数で指定したモデル)。そして、定義しているモデル自体が子モデルとなります。
例えば前述の下記の例であれば、親モデルが User
、子モデルが Comment
となります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.CASCADE)
そして、同様に、リレーションが構築されたオブジェクト間でも親子関係が存在することになります。
例えば前述の下記の例であれば、user_x
が親オブジェクトとなり、user_x
は子オブジェクトとして comment_1
と comment_2
を持つことになります。
from django.contrib.auth.models import User
from . import models
user_x = User(username='山田太郎')
comment_1 = models.Comment()
comment_1.contributor = user_x
comment_2 = models.Comment()
comment_2.contributor = user_x
以降に紹介する図では、子オブジェクト側から親オブジェクト側に対する矢印で親子関係を図示していきたいと思います。例えば、上記の例の場合は次の図のように表すことが出来ることになります。
on_delete
とは
で、ここからが本題の on_delete
についての解説になります。
まず、on_delete
引数は ForeignKey
と OneToOneField
における必須引数となります。これらに対して on_delete
引数を指定しなかった場合は、下記のような例外が発生することになります。
TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete'
そして、この on_delete
とは「子オブジェクトを持つ親オブジェクトが削除される際のアプリの動作を設定する引数」となります。
前述の例であれば、comment_1
と comment_2
の子オブジェクトを持つ「親オブジェクト user_x
」が削除される際のアプリの動作が、ForeignKey
における on_delete
引数によって設定されることになります。
子オブジェクトを持つ親オブジェクト削除時のアプリの適切な動作は開発するアプリによって大きく異なります。そのため、この削除時のアプリの動作を on_delete
引数によって設定できるようになっています。そして、この動作はリレーションごとに設定することが出来ます(ForeignKey
や OneToOneField
毎に設定可能。というか、そもそも必須で設定が必要)。
例えば、前述の例で示した Comment
モデルを「コメント投稿が可能なウェブサイト」で利用することを考えてみましょう。User
モデルはウェブサイトの会員として考えていきたいと思います。
この場合、User
オブジェクトを削除するケースとしてはウェブサイト会員(User
)の退会が考えられると思います。この会員退会時にアプリにどのように動作して欲しいかを設定するのが on_delete
になります。
例えば、退会する会員が今まで投稿してきたコメントは会員の退会時に一緒に削除してしまいたい場合があると思います。
「コメント投稿が可能なウェブサイト」が日々の近況をつぶやく目的で利用されているのであれば、退会した会員のコメントは削除してしまっても問題ないはずです。
このような場合、on_delete
には CASCADE
を設定しておくのが良いです。これにより、User
オブジェクト削除時に(会員退会時に)、User
オブジェクトの持つ Comment
オブジェクトが全て自動的に削除されることになります。
また、ウェブサイトによっては会員退会後も他の会員からコメントが閲覧できるよう、会員退会後もコメントのみは残しておきたい場合もあると思います。
例えば「コメント投稿が可能なウェブサイト」が技術的な議論を行うサイトであった場合、退会した会員のコメントであっても他の会員にとって有益なものも多いはずで、サイトとしてもコメントを残していった方が有益な情報を発信することができ、サイトの価値も向上するはずです。そもそもコメントを消してしまうと議論の流れが分からなくなってしまう可能性も高いです。
このような場合、on_delete
には SET_DEFAULT
(もしくは SET_NULL
or SET
) を設定しておくのが良いです。これにより、User
オブジェクト削除時にも(会員退会時にも)、User
オブジェクトの持つ Comment
オブジェクトを残すことが出来ます(後述で解説しますが、SET_DEFAULT
を設定しておくと親オブジェクト削除時に子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトが設定されることになります)。
このように、開発するアプリの目的や方向性によって子オブジェクトを持つ親オブジェクト削除時の適切な動作は異なります。そのため、開発するアプリの目的や方向性に応じて on_delete
を適切に設定することが重要となります。
逆に、on_delete
が適切に設定されていない場合、親オブジェクト削除時に本来削除すべきでないオブジェクト(データ)が削除されてしまう可能性もあるので注意が必要となります。
また、あくまでも on_delete
は「子オブジェクトを持つ親オブジェクト」が削除される際のアプリの動作を設定するものであり、「子オブジェクト自体」を削除する際や「子オブジェクトを持たない親オブジェクト」を削除する際の動作には影響がありません。
例えば on_delete
の設定値によっては「子オブジェクトを持つ親オブジェクト」の削除を禁止するようなものがあります。この場合、子オブジェクトを持っている限り親オブジェクトは削除することができません。
ですが、あらかじめ子オブジェクトを削除したり、子オブジェクトとの親子関係を解消したりすれば、その親オブジェクトは「子オブジェクトを持つ親オブジェクト」ではなくなるため、on_delete
の影響がなくなってオブジェクトを削除することが出来るようになります。
on_delete
に設定可能な値
さて、ここまで on_delete
の設定値をいくつか紹介しましたが、ここで on_delete
に設定可能な値を全て紹介しておきたいと思います。
Django 4.0 時点では on_delete
には下記の7つを設定可能です。
CASCADE
:子オブジェクトも一緒に削除するSET_DEFAULT
:子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトを設定するSET_NULL
:子オブジェクトの新たな親オブジェクトとしてNULL
を設定するSET
:子オブジェクトの新たな親オブジェクトとして特定の関数の返却値等を設定するDO_NOTHING
:何もしないPROTECT
:親オブジェクトの削除を禁止するRESTRICT
:親オブジェクトの削除を制限する
一応、各設定値の意味合いを一言で表していますが、これだけだと結局どう動作するかのイメージが湧かない方も多いと思います。
ということで、ここからは上記で挙げた7つの設定値それぞれの違いについて、具体的な例を挙げながら解説をしていきたいと思います。
スポンサーリンク
CASCADE
まずは、CASCADE
について説明していきます。CASCADE
は一番意味合いも分かりやすく、そして使いやすい設定値だと思います。
CASCADE
は親オブジェクト削除時に、その親オブジェクトの持つ子オブジェクトを全て自動的に削除するための設定値となります。
例えば、下記のようにモデルを定義した場合、
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.CASCADE)
User
オブジェクトが削除された際に、そのオブジェクトが子オブジェクトとして持つComment
オブジェクトも全て削除されることになります。
ここまでの例で示してきたような「コメント投稿可能なウェブサイト」の場合、会員(User
)が退会して削除される際に、その会員が今まで投稿してきたコメント(Comment
)も一緒に削除したいような場合に便利な設定値となります。
SET_DEFAULT
CASCADE
が親オブジェクト削除時に「自動的に子オブジェクトを削除する」設定値であるのに対し、ここから説明する SET_DEFAULT
、SET_NULL
、SET
に関しては、親オブジェクト削除時にも「子オブジェクトを残しておく」設定値となります。
まず SET_DEFAULT
は、親オブジェクト削除時に、その親オブジェクトの持つ子オブジェクトの新たな親オブジェクトとして「デフォルトオブジェクト」を設定する設定値となります。
つまり、子オブジェクトの親オブジェクトが自動的にデフォルトオブジェクトに付け替えられることになります。
どのオブジェクトをデフォルトオブジェクトとするかは default
引数の設定によって指定可能です(pk
を指定してデフォルトオブジェクトを設定する)。
on_delete
への SET_DEFAULT
の設定例は下記のようになります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.SET_DEFAULT, default=1)
CASCADE
では親オブジェクト削除時に子オブジェクトも一緒削除されるのに対し、SET_DEFAULT
ではデフォルトオブジェクトの子オブジェクトとして残ることになります。
そのため、「コメント投稿可能なウェブサイト」の例で考えれば、会員(User
)が退会して削除された後でも、その会員が今まで投稿してきたコメントを表示し続けたいような場合は便利な設定値になると思います。
SET_DEFAULT
の意味合いは分かりやすいと思うのですが、使い方は結構難しいのかなぁというのが私の印象です。
まず、on_delete
に SET_DEFAULT
を設定する場合は、上記のように必ず default
引数としてデフォルトオブジェクトの pk
も一緒に設定する必要があります。
上記のようにモデルを定義した場合、Comment
オブジェクトの親オブジェクト(User
)が削除された際に、そのオブジェクトの子オブジェクト(Comment
オブジェクト)の新たな親オブジェクトとしてデフォルトオブジェクト、つまり pk
が 1
である User
オブジェクトが設定されることになります。
この新たな親オブジェクトの設定は自動的に行われるのですが、この際に「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」場合、親オブジェクト削除時に例外が発生することになるので注意してください。
「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」ケースの1つ目として、デフォルトオブジェクト自体が存在しない場合が挙げられます。上記のモデル定義の例であれば、pk
が 1
である User
オブジェクトが存在しない場合がそれに当たります。
この場合、親オブジェクト削除時には下記のような例外が発生することになります。
django.db.utils.IntegrityError: FOREIGN KEY constraint failed
「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」ケースの2つ目として、親モデルと子モデルとの間のリレーションが1対1に設定されている場合が挙げられます。
この場合、デフォルトオブジェクトが既に子オブジェクトを持っていると、新たな子オブジェクトを追加した際に1対1の関係性が崩れてしまうことになるため、下記のような例外が発生することになります。
UNIQUE constraint failed: on_delete_app_comment.user_id
例えば、下記のようにモデルを定義した場合、親モデルと子モデルの間には1対1のリレーションが設定されます。つまり、1つの親オブジェクトが持つことのできる子オブジェクトは1つのみとなります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.OneToOneField(User, on_delete=models.SET_DEFAULT, default=1)
デフォルトオブジェクトが子オブジェクトを持っていない場合、最初の User
オブジェクト削除は成功しますが、この時点でデフォルトオブジェクトは1つの子オブジェクトを持っていることになります。
ですので、この状態で子オブジェクトを持つ他の User
オブジェクトを削除した場合、その子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトが設定されることになりますが、この時に1対1の関係性で無くなってしまうため、例外が発生することになります。
こういった注意点があるため、on_delete
に SET_DEFAULT
を設定する場合は、OneToOneField
は利用しない方が良いと思います。
SET_NULL
次に SET_NULL
について説明していきます。
SET_NULL
は、親オブジェクトが削除された際に、子オブジェクトの親オブジェクトを「なし(NULL
)」に設定するための設定値となります。
つまり、on_delete
に SET_NULL
が指定されている子オブジェクトは、親オブジェクトが削除された際に親オブジェクトが存在しない状態になります。子オブジェクトは、親オブジェクトが存在しない状態でデータベースに存在し続けることになります。
on_delete
に SET_NULL
を設定する場合、必ず null=True
の引数指定も必要となるので注意してください。
on_delete
への SET_NULL
の設定例は下記のようになります。
from django.db import models
from django.contrib.auth.models import User
class History(models.Model):
customer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
上記のモデルを用いて、「ショッピングウェブアプリ」で顧客(User
)の購入履歴(History
)を管理し、その購入履歴から毎月の売上高の集計や在庫数の管理などを行う例を考えてみましょう(下図において、user_x
と user_y
は User
オブジェクト、history_1
と history_2
は History
オブジェクトとなります)。
この例の場合、顧客(User
)が退会した際にも、その顧客の購入履歴(History
)は残しておく必要があります。購入履歴から売上高の集計や在庫数の管理を行なっているのですから、顧客が退会したからといって購入履歴まで消してしまうと、その分売上高や在庫数が減ってしまって計算が合わなくなってしまいますよね…。
そんな時に、on_delete
に SET_NULL
を設定しておけば、顧客退会時にも購入履歴は残すことができ、正しく売上や在庫数を計算することができるようになります。
こんな感じで、親オブジェクトがいない状態であっても子オブジェクトのみは残しておきたい場合に SET_NULL
は便利だと思います。
スポンサーリンク
SET
続いて SET
について説明していきます。
SET
は親オブジェクト削除時に、子オブジェクトの新たな親オブジェクトとして SET
の引数で指定した pk
のオブジェクトを設定する or SET
の引数で指定した関数の戻り値の pk
のオブジェクトを設定する設定値となります。
例えば、下記のようにモデルを定義すれば、親オブジェクト削除時に、子オブジェクトの新たな親オブジェクトとして pk
が 1
の User
オブジェクトが設定されるようになります。この場合は、SET_DEFAULT
と同様の動作が実現されることになります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.SET(1))
また、前述のように SET
には関数を指定することもでき、例えば下記のようにモデルを定義すれば、親オブジェクト削除時に関数 get_first_user
が実行され、子オブジェクトの新たな親オブジェクトとして get_first_user
の返却値を pk
とするオブジェクトが設定されることになります。
from django.db import models
from django.contrib.auth.models import User
def get_first_user():
users = User.objects.order_by('username')
return users[0].pk
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.SET(get_first_user))
上記の get_first_user
は、username
がアルファベット順で一番早い User
オブジェクトを親として設定するような関数にしていますが(username
はユーザー名を示す User
のフィールド)、もちろん関数の作り方によって異なる User
オブジェクトを親として設定することも可能です。
また、適当な親オブジェクトが存在しない場合に None
を返却することで、親オブジェクトが存在しない子オブジェクトにすることも可能です。
関数で行う処理によって動的に親オブジェクトを変更したいような場合に SET
は便利な設定値になると思います。ただし、親オブジェクトを付け替えるわけですから、SET_DEFAULT
と同様の注意点に気をつける必要があります(None
を返却する場合は SET_NULL
と同様の注意点に気をつける必要があります)。
DO_NOTHING
on_delete
には DO_NOTHING
を設定することも可能です。
名前の通り、on_delete
に DO_NOTHING
が設定されている場合、親オブジェクト削除時には何も行われません。
つまり、子オブジェクトの親は削除された親オブジェクトのままとなります。
ですが、デフォルトでは存在しないオブジェクトを親オブジェクトとすることができないようになっているため、on_delete
に DO_NOTHING
が設定されている場合、子オブジェクトを持つ親オブジェクトを削除しようとすると、例外 IntegrityError
が発生して親オブジェクトの削除に失敗することになります。
この例外は ForeignKey
等の引数に db_constraint=False
を指定することで防ぐことができ、これにより親オブジェクトの削除ができるようになります。ですが、それでも子オブジェクトが「存在しない親オブジェクト」を参照し続けることになるため、そのままアプリを動作させ続けるとデータベース内で不整合が発生する可能性が高くなってしまうので注意してください。
on_delete
への DO_NOTHING
の設定例は下記のようになります。
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
contributor = models.ForeignKey(User, on_delete=models.DO_NOTHING, db_constraint=False)
DO_NOTHING
は、親オブジェクト削除時の動作をアプリに任せるのではなく、開発者やアプリ管理者自らが動作を制御したいような場合に有効な設定値になると思います。ですが、データベース内の不整合を防ぐための実装やデータベースの操作が必要になるため、特に Django やデータベース初心者の方にとっては使用難易度の高い設定値となります。ですので、まずは DO_NOTHING
ではなく他の設定値を利用することを考えるのが良いと思います。
PROTECT
ここまで紹介してきた設定値は全て、親オブジェクト削除時の子オブジェクトの扱い方を決めるためのものでした。
それに対し、ここから紹介する PROTECT
と RESTRICT
は子オブジェクトを持つ親オブジェクトの削除を禁止したり、制限したりする設定値となります。
まず PROTECT
は、子オブジェクトを持つ親オブジェクトの削除を禁止する設定値となります。子オブジェクトを持つ親オブジェクトを削除しようとした場合、例外 ProtectedError
が発生して削除に失敗します。
つまり、PROTECT
が設定されている場合、子オブジェクトを持つ親オブジェクトを削除するためには、まず「子オブジェクトを持たない状態」にする必要があります。そうしなければ、親オブジェクトを削除することができません。
親オブジェクトを「子オブジェクトを持たない状態」にするためには、持っている子オブジェクトを他のオブジェクトの子に設定したり、持っている子オブジェクトを削除したりする必要があります。
子オブジェクトを持つ親オブジェクトの削除を防ぎたいような場合に便利な設定値となります。
on_delete
に PROTECT
を設定する例は下記のようなものになります。
from django.db import models
from django.contrib.auth.models import User
class Employee(User):
pass
class Work(models.Model):
contractor = models.OneToOneField(Employee, on_delete=models.PROTECT)
requester = models.OneToOneField(User, on_delete=models.CASCADE)
上記は「仕事の依頼ウェブサービス」で利用することを想定したモデルになります。
Work
は Employee
と User
それぞれの子モデルとなっており、User
は Work
(仕事)の依頼者、Employee
は Work
の請負者というイメージになります。
この場合、Work
オブジェクトを子オブジェクトとして持つ Employee
オブジェクトは、Work
と Employee
のリレーションの on_delete
に PROTECT
が設定されているため削除することができません。
Employee
オブジェクトを削除するためには、そのオブジェクトの子オブジェクト(Work
オブジェクト)を事前に削除する必要があります。イメージとしては請け負った仕事を完了させてから、依頼者の User
オブジェクトに Work
オブジェクトを削除してもらう感じになります。
つまり、PROTECT
を設定することで、請負者(Employee
)が請け負った仕事を完了させずに退会して削除されてしまうことを防ぐことができることになります。仕事を請け負ったまま退会されてしまうと、仕事の依頼者は困ってしまいますよね…。
このように、on_delete
に PROTECT
を設定することで、子オブジェクトを持つ親オブジェクトの削除を禁止することができるようになります。
ちなみに、User
と Work
のリレーションには on_delete=CASCADE
が設定されているため、子オブジェクトとして Work
オブジェクトを持っていたとしても、User
オブジェクトに関しては削除することが可能です(User
オブジェクト削除時に、そのオブジェクトの子の Work
オブジェクトも一緒に削除されることになります)。
スポンサーリンク
RESTRICT
最後に RESTRICT
について解説していきます。
RESTRICT
は PROTECT
に似た動作をする設定値であり、この2つの違いを理解するのは結構難しいのではないかと思います。そのため、RESTRICT
と PROTECT
の違いに重点を置いて説明していきたいと思います。
RESTRICT
の解説は(他の設定値の解説もそうですが…)、Django 公式チュートリアル が一番正確だと思います
ですが、割と説明があっさりしていて理解しきれない方がおられる可能性もありますので、私なりの解釈の仕方も踏まえて RESTRICT
について解説していきたいと思います
そのため、私の推測も含まれるので、その点はご了承いただければと思います
例えば、下記のようなモデルを定義した場合、Music
オブジェクトを子として持つ Album
オブジェクトを削除した際のアプリの動作は、on_delete
に PROTECT
を設定した時とほぼ同じになります。
from django.db import models
from django.contrib.auth.models import User
class Music(models.Model):
pass
class Album(models.Model):
music = models.ForeignKey(Music, on_delete=models.RESTRICT)
上記のようにモデルを定義した場合、on_delete
に PROTECT
を設定した時と同様に、子オブジェクトを持つ親オブジェクトを削除しようとしても例外が発生して削除に失敗することになります。ただし、発生する例外は RistrictedError
となり、この点は PROTECT
とは異なることになります。
では、異なる点が例外だけかというと、そうではありません。
上記のモデル定義においては子モデルの親モデルは1種類のみであり、このような場合は on_delete
に PROTECT
を設定した場合と RESTRICT
を設定した場合とで動作はほとんど同じです。
ですが、例えば下記の条件を全て満たす場合、PROTECT
と RESTRICT
のどちらを設定するかで動作が大きく異なることになります。
- 1つのモデルに対して複数種の親モデルが存在する
- それらのリレーションの
on_delete
の設定が「PROTECT
orRESTRICT
」と「CASCADE
」とで混在している - 複数種の親モデルのオブジェクトが同一操作の中で同時に削除される
ちょっとややこしいですね…。もう少し具体的な例で PROTECT
と RESTRICT
の例を考えていきましょう!
まず、実際に上記条件を満たすモデルの定義は次のようになります。
class ModelA(models.Model):
pass
class ModelB(models.Model):
model_a = models.ForeignKey(ModelA, on_delete=models.CASCADE)
class ModelC(models.Model):
model_a = models.ForeignKey(ModelA, on_delete=models.CASCADE)
model_b = models.ForeignKey(ModelB, on_delete=models.PROTECT)
また、下図のような親子関係のオブジェクトを用いて PROTECT
と RESTRICT
の違いを説明していきたいと思います。
まず、ModelC
には ModelA
と ModelB
という複数種の親モデルが存在しています(上記条件の 1.)。
さらに、ModelA
と ModelC
のリレーションの on_delete
は CASCADE
であり、ModelB
と ModelC
のリレーションの on_delete
は PROTECT
となっています(上記条件の 2.)。
また、ModelA
と ModelB
も親子関係にあり、これらのリレーションの on_delete
は CASCADE
となっています。
つまり、ModelA
のオブジェクトを削除する操作を行うと、同時にそのオブジェクトが子として持つ ModelB
のオブジェクトも削除されることになります(上記条件の 3.)。
このようなモデル・リレーションの構成の場合、ModelB
と ModelC
のリレーションの on_delete
を PROTECT
or RESTRICT
のどちらに設定するかで ModelA
のオブジェクト削除時の動作が大きく異なります。
まずは、ModelB
と ModelC
のリレーションの on_delete
を PROTECT
と設定した場合の動作について考えていきましょう!
この場合、ModelA
のオブジェクトを削除すると、どのオブジェクトが削除されることになるでしょうか?
結論を先に言っておくと、正解は「全てのオブジェクトが削除されない」になります。
まず、ModelA
のオブジェクトを削除しようとすると、そのオブジェクトが子として持つ ModelB
のオブジェクトが削除されることになります。これは、ModelA
と ModelB
のリレーションの on_delete
が CASCADE
に設定されているからです。
しかし、ModelB
と ModelC
とのリレーションの on_delete
が PROTECT
であるため、ModelB
のオブジェクトは削除することができないことになります(ModelC
のオブジェクトを子として持っているため)。つまり、ModelB
のオブジェクトを削除しようと動作したときに例外が発生し、全てのオブジェクトの削除に失敗することになります。
ですが、考え方によっては「全てのオブジェクトが削除される」と捉えることもできます。
この際の考え方は次のようになります。
まず、ModelA
のオブジェクトと ModelC
のオブジェクトとのリレーションの on_delete
は CASCADE
です。ですので、ModelA
のオブジェクトを削除した場合、そのオブジェクトを親とする ModelC
のオブジェクトは全て削除されることになります。
そうなると、ModelB
のオブジェクトの子オブジェクト(つまり ModelC
のオブジェクト)は削除されて存在しないことになります。したがって、ModelB
のオブジェクトは削除可能となり、ModelA
のオブジェクトが削除されるのと同時に ModelB
のオブジェクトも削除されることになります(ModelA
と ModelB
のリレーションの on_delete
が CASCADE
のため)。
そして、削除操作が行われた ModelA
オブジェクトの削除も行われ、結果的に全てのオブジェクトが削除されることになります。
この考え方も別に不自然ではないですよね?
つまり、あるオブジェクトに対して複数種の親オブジェクトが存在し、さらにそれらの親オブジェクトが同一操作の中で削除される場合、どの親オブジェクトとのリレーションの on_delete
を優先するかによって削除に成功 or 削除に失敗が変わることになります。
要は、オブジェクトの削除結果(成功 or 失敗)は、オブジェクト削除時に優先する経路に依存するということになります。
こういった優先する経路を開発者が選択できるよう、PROTECT
だけでなく、RESTRICT
の設定値が用意されているのだと思います。そして、上記の2つの考え方における1つ目の考え方に従ってオブジェクトの削除を行なっていくのが PROTECT
であり、2つ目の考え方に従ってオブジェクトの削除を行なっていくのが RESTRICT
となります。
より具体的には、on_delete=PROTECT
を設定したリレーションが存在する場合、そのリレーションを経由する経路が最優先されて削除が行われていくことになります。したがって、on_delete=PROTECT
を設定したリレーションにおいて「子オブジェクトを持つ親オブジェクト」が存在する場合、必ず例外が発生して削除に失敗することになります。
それに対し、on_delete=RESTRICT
が設定されたリレーションは優先されません。他のリレーションが優先されます。したがって、on_delete=RESTRICT
を設定したリレーションにおいて「子オブジェクトを持つ親オブジェクト」が存在したとしても、例外が発生するとは限りません。
そのため、リレーションの構成の仕方によっては、on_delete=RESTRICT
を設定したリレーションにおいても「子オブジェクトを持つ親オブジェクト」が削除されることになります。つまり、on_delete=RESTRICT
を設定したリレーションにおいて、「子オブジェクトを持つ親オブジェクト」が削除されるかどうかはリレーションの構成の仕方に依存します。
それに対して PROTECT
の場合、on_delete=PROTECT
を設定したリレーションにおいて、「子オブジェクトを持つ親オブジェクト」が削除されるかどうかはリレーションの構成の仕方に依存せず、絶対に削除されることはありません。
したがって、PROTECT
に関しては、この on_delete
が設定されたリレーションにおいて子オブジェクトを持つ親オブジェクトの削除が完全に禁止されますが、RESTRICT
に関しては禁止ではなく制限するだけになります。
これらの点が、RESTRICT
と PROTECT
の違いとなります。
上記のような違いがあるため、開発したいアプリに合わせて PROTECT
と RESTRICT
とは適切に使い分けすることが望ましいです。
例えば、下記のようにモデル群を定義したとします。このモデルは Django 公式チュートリアル で RESTRICT
の説明時に用いられているものを引用したものになります(少し変更しています)。このモデルを音楽配信ウェブサービスで使用することを考えていきましょう!
class Artist(User):
pass
class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
class Song(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
album = models.ForeignKey(Album, on_delete=models.RESTRICT)
Album
と Song
は Artist
が発表したものであり、Artist
の子モデルとなります。
また、Album
の中には複数の Song
が存在するため、Song
は Album
の子モデルでもあります。
さらに、Album
には複数の Artist
の Song
が含まれる可能性があり、この場合、Album
の削除をすると他の Artist
の Song
が削除されることになってしまうため、Album
と Song
のリレーションの on_delete
は RESTRICT
となっています。
具体的なオブジェクト構成例は下の図のようになります。
ちょっとややこしいですが、album_b
は artist_y
が子として持つ Album
オブジェクトになりますが、この album_b
は artist_x
の子オブジェクトである song_2
を子として持っています。artist_x
が album_b
に楽曲提供しているイメージになるかと思います。
そのため、もし album_b
が削除されると artist_x
の曲まで削除されることになってしまいます。これを防ぐために Album
と Song
のリレーションの on_delete
は RESTRICT
となっています。
ここで、音楽配信ウェブサービスと artsit_x
との契約が切れ、artist_x
の持つ Album
オブジェクトと Song
オブジェクトを全て削除しなければならなくなった場合のことを考えてみましょう。
もし、Album
と Song
のリレーションの on_delete
が PROTECT
であれば artist_x
を削除しようとしても失敗することになります。なぜなら、artist_x
を削除しようとすると on_delete=PROTECT
のリレーションを経由する経路上のオブジェクトが優先されて削除されていくことになり、album_a
削除時に例外が発生することになるからです。
したがって、artist_x
との契約が切れた場合、Album
と Song
のリレーションの on_delete
が PROTECT
であれば、まずは album_a
が子として持つ Song
オブジェクト全てを削除してから artist_x
を削除する必要があることになります。図では削除必要な Song
オブジェクトは1つですが、この Song
オブジェクトが大量にある場合、 Song
オブジェクトを削除をするのは結構大変ですよね…。
それに対し、Album
と Song
のリレーションの on_delete
が RESTRICT
である場合、artist_x
の削除に成功することになります。なぜなら、artist_x
を削除しようとすると on_delete=RESTRICT
のリレーションを経由する経路上のオブジェクトが優先されず、先に artist_x
の子の Song
オブジェクトが削除されることになるからです(Artist
と Song
のリレーションの on_delete
は CASCADE
)。
そして、これによって album_a
の子オブジェクトが存在しなくなるため、artist_x
の削除時に album_a
も削除されることになります(Artist
と Album
のリレーションの on_delete
は CASCADE
)。
こういった動作から、PROTECT
よりもRESTRICT
の方がオブジェクトの削除が楽である点を確認していただけると思います。
さて、ここまでは artist_x
を削除する例について考えましたが、artist_x
を削除せずに artist_y
の削除をしようとした際にはどのような動作になるでしょう?
この場合、Album
と Song
のリレーションの on_delete
が RESTRICT
であっても artist_y
の削除には失敗することになります。artist_x
削除時の時と同様に、先に artist_y
が子として持つ Song
オブジェクトを先に削除するように動作していくのですが、artist_y
が子として持つ Song
オブジェクトを全て削除したとしても album_b
の子オブジェクトは全てなくなりません。
そのため、album_b
は on_delete=RESTRICT
の影響によって削除することが出来ないことになります。したがって、album_b
を削除しようとした際に例外が発生し、オブジェクトの削除に失敗することになります(削除途中で例外が発生する場合、その過程で削除されようとしたオブジェクトは全て削除されないことになります)。
言い方を変えれば、他の Artist
オブジェクトを持つ Album
オブジェクトの削除の防止に成功しているとも言えます。
このように、on_delete=RESTRICT
は、無関係なオブジェクトの削除を防ぎつつ、関連性のあるオブジェクトのみを一括して削除するのに適した便利な設定値になります。
ちなみに、上記の例において、artist_x
を削除してから artist_y
を削除した場合は削除に成功することになります。
また、下の図のようなオブジェクト・リレーション構成の場合、artist_y
を直接削除した場合は削除に失敗しますが、company
(レコード会社)を削除した場合は全てのオブジェクトの削除に成功することになります。
これらの理由についても、on_delete=RESTRICT
を経由する経路を “優先せず” にオブジェクトを削除していくことを考えていけば理解していただけるのではないかと思います(もちろん RESTRICT
を PROTECT
とした場合は company
を削除すると例外が発生することになります)。
なんとなく PROTECT
と RESTRICT
の違いについては理解していただけたでしょうか?
ただ、この2つは使い分けは難しいので、迷ったらまずは PROTECT
を利用するので良いのではないかと思います。これらの設定値を利用する目的は「子オブジェクトを持つ親オブジェクトの削除を防ぐ」ことだと思います。PROTECT
を利用すれば、確実にその目的を達成することができます。
その上で、もしオブジェクトの削除が面倒であると感じるのであれば、その際に RESTRICT
の採用を検討してみるので良いと思います。
まとめ
このページでは、Django における on_delete
について解説しました!
on_delete
はリレーションに対して設定を行う引数であり、「子オブジェクトを持つ親オブジェクト削除時のアプリの動作」を設定することのできる引数となります。
この on_delete
には、Django 4.0 時点で下記の7つを設定することが可能です。
CASCADE
:子オブジェクトも一緒に削除するSET_DEFAULT
:子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトを設定するSET_NULL
:子オブジェクトの新たな親オブジェクトとしてNULL
を設定するSET
:子オブジェクトの新たな親オブジェクトとして特定の関数の返却値等を設定するDO_NOTHING
:何もしないPROTECT
:親オブジェクトの削除を禁止するRESTRICT
:親オブジェクトの削除を制限する
これらの設定値の中でどれを使用すべきかは開発したいアプリによって異なります。それぞれの設定値の意味合いをしっかり理解し、開発したいアプリに合わせて on_delete
を適切に設定していくようにしましょう!