【Python/Django】on_deleteについて分かりやすく解説

このページにはプロモーションが含まれています

このページでは、Django における on_delete について解説します。

Django ではモデルとモデルの間にリレーションを設定することが可能であり、リレーションを設定する際には下記のようなフィールドをモデルに持たせることになります。

リレーションの設定
class Comment(models.Model):
    contributor = models.ForeignKey(User, on_delete=models.CASCADE)

このようなモデルの定義を、皆さんも実際に自身で実装したり、ウェブサイトのソースコード等で見かけたりしたことがあるのではないでしょうか?

今回注目したいのが、上記における on_delete 引数になります。この on_delete 引数は、ForeignKey もしくは OneToOneField を利用してリレーションを設定する際に必須の引数となります。

では、この on_delete 引数の設定によってアプリの動作はどのように変化するのでしょうか?

この点について、本ページで解説していきたいと思います!

on_delete

では、まずは on_delete について解説していきます。

結論を先に言っておくと、on_delete は、リレーションを構築した2つのオブジェクトにおいて、子オブジェクトを持つ親オブジェクト削除時の「アプリの動作」を設定する引数になります。

いきなりこの結論を見ただけだと意味合いが分かりにくいかもしれないので、リレーションや親子関係等について説明をしたのちに、再度 on_delete の意味合いについて説明していきたいと思います。

リレーションの設定と構築

前述でも少し触れたように、Django においてはモデルとモデルの間にリレーションを設定することが可能です。

このリレーションの設定は「リレーションを示すフィールド」をモデルに持たせることで実現することが出来ます。

「リレーションを示すフィールド」の代表例が ForeignKeyOneToOneField であり、これらの第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 を指定しているため、CommentUser の間にリレーションが設定されることになります(User は Django に標準で用意されているユーザー管理モデルになります)。

UserとCommentの間にリレーションが設定される様子

モデル定義で行えるのは「リレーションの設定のみ」となります。要はモデルのオブジェクト同士でリレーションを構築可能にするための設定を行うだけとなります。

実際には、モデルのオブジェクトを生成し、そのオブジェクト同士にリレーションを構築していくことになります。そして、そのオブジェクト同士のリレーションは、モデルに持たせたリレーションを示すフィールドに「リレーションを構築したいオブジェクト」をセットすることで実現することが出来ます。

例えば、上記のモデルの定義であれば、Comment のオブジェクトがリレーションを示すフィールドを contributor として持っているため、この contributorUser のオブジェクトをセットすることで、Comment のオブジェクトと User のオブジェクトとの間にリレーションが構築されることになります。

このリレーションの構築の処理を簡単なスクリプトの例で示すと下記のようなものになります(実際には、同様の処理を views.pymodels.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_1comment_2user_x との間にリレーションが構築されることになります。

user_xとcomment_1・comment_2の間にリレーションが構築される様子

この場合、1つのオブジェクト(user_x)と複数のオブジェクト(comment_1comment_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_1comment_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 引数は ForeignKeyOneToOneField における必須引数となります。これらに対して on_delete 引数を指定しなかった場合は、下記のような例外が発生することになります。

TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete'

そして、この on_delete とは「子オブジェクトを持つ親オブジェクトが削除される際のアプリの動作を設定する引数」となります。

前述の例であれば、comment_1comment_2 の子オブジェクトを持つ「親オブジェクト user_x」が削除される際のアプリの動作が、ForeignKey における on_delete 引数によって設定されることになります。

on_deleteの意味合いを表した図

子オブジェクトを持つ親オブジェクト削除時のアプリの適切な動作は開発するアプリによって大きく異なります。そのため、この削除時のアプリの動作を on_delete 引数によって設定できるようになっています。そして、この動作はリレーションごとに設定することが出来ます(ForeignKey や OneToOneField 毎に設定可能。というか、そもそも必須で設定が必要)。

例えば、前述の例で示した Comment モデルを「コメント投稿が可能なウェブサイト」で利用することを考えてみましょう。User モデルはウェブサイトの会員として考えていきたいと思います。

この場合、User オブジェクトを削除するケースとしてはウェブサイト会員(User)の退会が考えられると思います。この会員退会時にアプリにどのように動作して欲しいかを設定するのが on_delete になります。

例えば、退会する会員が今まで投稿してきたコメントは会員の退会時に一緒に削除してしまいたい場合があると思います。

「コメント投稿が可能なウェブサイト」が日々の近況をつぶやく目的で利用されているのであれば、退会した会員のコメントは削除してしまっても問題ないはずです。

退会した会員のコメントを削除する様子

このような場合、on_delete には CASCADE を設定しておくのが良いです。これにより、User オブジェクト削除時に(会員退会時に)、User オブジェクトの持つ Comment オブジェクトが全て自動的に削除されることになります。

on_deleteの設定によって親オブジェクト削除時に子オブジェクトが自動的に削除される様子

また、ウェブサイトによっては会員退会後も他の会員からコメントが閲覧できるよう、会員退会後もコメントのみは残しておきたい場合もあると思います。

退会した会員のコメントを残す様子

例えば「コメント投稿が可能なウェブサイト」が技術的な議論を行うサイトであった場合、退会した会員のコメントであっても他の会員にとって有益なものも多いはずで、サイトとしてもコメントを残していった方が有益な情報を発信することができ、サイトの価値も向上するはずです。そもそもコメントを消してしまうと議論の流れが分からなくなってしまう可能性も高いです。

このような場合、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 に設定可能な値

さて、ここまで 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 は親オブジェクト削除時に、その親オブジェクトの持つ子オブジェクトを全て自動的に削除するための設定値となります。

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_DEFAULTSET_NULLSET に関しては、親オブジェクト削除時にも「子オブジェクトを残しておく」設定値となります。

まず SET_DEFAULT は、親オブジェクト削除時に、その親オブジェクトの持つ子オブジェクトの新たな親オブジェクトとして「デフォルトオブジェクト」を設定する設定値となります。

つまり、子オブジェクトの親オブジェクトが自動的にデフォルトオブジェクトに付け替えられることになります。

SET_DEFAULTの説明図

どのオブジェクトをデフォルトオブジェクトとするかは default 引数の設定によって指定可能です(pk を指定してデフォルトオブジェクトを設定する)。

on_delete への SET_DEFAULT の設定例は下記のようになります。

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_deleteSET_DEFAULT を設定する場合は、上記のように必ず default 引数としてデフォルトオブジェクトの pk も一緒に設定する必要があります。

上記のようにモデルを定義した場合、Comment オブジェクトの親オブジェクト(User)が削除された際に、そのオブジェクトの子オブジェクト(Comment オブジェクト)の新たな親オブジェクトとしてデフォルトオブジェクト、つまり pk1 である User オブジェクトが設定されることになります。

SET_DEFAULT利用時の注意点を解説する図1

この新たな親オブジェクトの設定は自動的に行われるのですが、この際に「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」場合、親オブジェクト削除時に例外が発生することになるので注意してください。

「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」ケースの1つ目として、デフォルトオブジェクト自体が存在しない場合が挙げられます。上記のモデル定義の例であれば、pk1 である User オブジェクトが存在しない場合がそれに当たります。

SET_DEFAULT利用時の注意点を解説する図2

この場合、親オブジェクト削除時には下記のような例外が発生することになります。

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つの子オブジェクトを持っていることになります。

SET_DEFAULT利用時の注意点を解説する図3

ですので、この状態で子オブジェクトを持つ他の User オブジェクトを削除した場合、その子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトが設定されることになりますが、この時に1対1の関係性で無くなってしまうため、例外が発生することになります。

SET_DEFAULT利用時の注意点を解説する図4

こういった注意点があるため、on_deleteSET_DEFAULT を設定する場合は、OneToOneField は利用しない方が良いと思います。

SET_NULL

次に SET_NULL について説明していきます。

SET_NULL は、親オブジェクトが削除された際に、子オブジェクトの親オブジェクトを「なし(NULL)」に設定するための設定値となります。

つまり、on_deleteSET_NULL が指定されている子オブジェクトは、親オブジェクトが削除された際に親オブジェクトが存在しない状態になります。子オブジェクトは、親オブジェクトが存在しない状態でデータベースに存在し続けることになります。

SET_NULLの説明図

on_delete に SET_NULL を設定する場合、必ず null=True の引数指定も必要となるので注意してください。

on_delete への SET_NULL の設定例は下記のようになります。

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_xuser_y は User オブジェクト、history_1history_2 は History オブジェクトとなります)。

ショッピングアプリでHistoryのオブジェクトから売上を集計する様子

この例の場合、顧客(User)が退会した際にも、その顧客の購入履歴(History)は残しておく必要があります。購入履歴から売上高の集計や在庫数の管理を行なっているのですから、顧客が退会したからといって購入履歴まで消してしまうと、その分売上高や在庫数が減ってしまって計算が合わなくなってしまいますよね…。

Userの削除に伴ってHistoryを削除してしまうと売上の集計が上手くできなくなってしまう様子

そんな時に、on_delete に SET_NULL を設定しておけば、顧客退会時にも購入履歴は残すことができ、正しく売上や在庫数を計算することができるようになります。

User削除時にも親オブジェクトがいない状態でHistoryを残すことで売上の集計が正しく行える様子

こんな感じで、親オブジェクトがいない状態であっても子オブジェクトのみは残しておきたい場合に SET_NULL は便利だと思います。

スポンサーリンク

SET

続いて SET について説明していきます。

SET は親オブジェクト削除時に、子オブジェクトの新たな親オブジェクトとして SET の引数で指定した pk のオブジェクトを設定する or SET の引数で指定した関数の戻り値の pk のオブジェクトを設定する設定値となります。

SETの説明図

例えば、下記のようにモデルを定義すれば、親オブジェクト削除時に、子オブジェクトの新たな親オブジェクトとして pk1User オブジェクトが設定されるようになります。この場合は、SET_DEFAULT と同様の動作が実現されることになります。

SETの設定例1
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 とするオブジェクトが設定されることになります。

SETの設定例2
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 が設定されている場合、親オブジェクト削除時には何も行われません。

つまり、子オブジェクトの親は削除された親オブジェクトのままとなります。

DO_NOTHINGの説明図

ですが、デフォルトでは存在しないオブジェクトを親オブジェクトとすることができないようになっているため、on_delete に DO_NOTHING が設定されている場合、子オブジェクトを持つ親オブジェクトを削除しようとすると、例外 IntegrityError が発生して親オブジェクトの削除に失敗することになります。

この例外は ForeignKey 等の引数に db_constraint=False を指定することで防ぐことができ、これにより親オブジェクトの削除ができるようになります。ですが、それでも子オブジェクトが「存在しない親オブジェクト」を参照し続けることになるため、そのままアプリを動作させ続けるとデータベース内で不整合が発生する可能性が高くなってしまうので注意してください。

on_delete への DO_NOTHING の設定例は下記のようになります。

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の説明図

つまり、PROTECT が設定されている場合、子オブジェクトを持つ親オブジェクトを削除するためには、まず「子オブジェクトを持たない状態」にする必要があります。そうしなければ、親オブジェクトを削除することができません。

親オブジェクトを「子オブジェクトを持たない状態」にするためには、持っている子オブジェクトを他のオブジェクトの子に設定したり、持っている子オブジェクトを削除したりする必要があります。

PROTECT設定時の親オブジェクトの削除手順

子オブジェクトを持つ親オブジェクトの削除を防ぎたいような場合に便利な設定値となります。

on_delete に PROTECT を設定する例は下記のようなものになります。

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)

上記は「仕事の依頼ウェブサービス」で利用することを想定したモデルになります。

WorkEmployeeUser それぞれの子モデルとなっており、UserWork(仕事)の依頼者、EmployeeWork の請負者というイメージになります。

仕事以来WebサービスでのPROTECTの利用例1

この場合、Work オブジェクトを子オブジェクトとして持つ Employee オブジェクトは、WorkEmployee のリレーションの on_deletePROTECT が設定されているため削除することができません。

仕事依頼ウェブサービスでのPROTECTの利用例2

Employee オブジェクトを削除するためには、そのオブジェクトの子オブジェクト(Work オブジェクト)を事前に削除する必要があります。イメージとしては請け負った仕事を完了させてから、依頼者の User オブジェクトに Work オブジェクトを削除してもらう感じになります。

仕事依頼ウェブサービスでのPROTECTの利用例3

つまり、PROTECT を設定することで、請負者(Employee)が請け負った仕事を完了させずに退会して削除されてしまうことを防ぐことができることになります。仕事を請け負ったまま退会されてしまうと、仕事の依頼者は困ってしまいますよね…。

仕事依頼ウェブサービスでのPROTECTの利用例4

このように、on_deletePROTECT を設定することで、子オブジェクトを持つ親オブジェクトの削除を禁止することができるようになります。

ちなみに、UserWork のリレーションには on_delete=CASCADE が設定されているため、子オブジェクトとして Work オブジェクトを持っていたとしても、User オブジェクトに関しては削除することが可能です(User オブジェクト削除時に、そのオブジェクトの子の Work オブジェクトも一緒に削除されることになります)。

スポンサーリンク

RESTRICT

最後に RESTRICT について解説していきます。

RESTRICT は PROTECT に似た動作をする設定値であり、この2つの違いを理解するのは結構難しいのではないかと思います。そのため、RESTRICT と PROTECT の違いに重点を置いて説明していきたいと思います。

MEMO

RESTRICT の解説は(他の設定値の解説もそうですが…)、Django 公式チュートリアル が一番正確だと思います

ですが、割と説明があっさりしていて理解しきれない方がおられる可能性もありますので、私なりの解釈の仕方も踏まえて RESTRICT について解説していきたいと思います

そのため、私の推測も含まれるので、その点はご了承いただければと思います

例えば、下記のようなモデルを定義した場合、Music オブジェクトを子として持つ Album オブジェクトを削除した際のアプリの動作は、on_delete に PROTECT を設定した時とほぼ同じになります。

RESTRICTの設定例
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 を設定した場合とで動作はほとんど同じです。

ですが、例えば下記の条件を全て満たす場合、PROTECTRESTRICT のどちらを設定するかで動作が大きく異なることになります。

  1. 1つのモデルに対して複数種の親モデルが存在する
  2. それらのリレーションの on_delete の設定が「PROTECT or RESTRICT」と「CASCADE」とで混在している
  3. 複数種の親モデルのオブジェクトが同一操作の中で同時に削除される

ちょっとややこしいですね…。もう少し具体的な例で PROTECTRESTRICT の例を考えていきましょう!

まず、実際に上記条件を満たすモデルの定義は次のようになります。

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)

また、下図のような親子関係のオブジェクトを用いて PROTECTRESTRICT の違いを説明していきたいと思います。

PROTECTとRESTRICTの違いを説明する図1

まず、ModelC には ModelAModelB という複数種の親モデルが存在しています(上記条件の 1.)。

さらに、ModelAModelC のリレーションの on_deleteCASCADE であり、ModelBModelC のリレーションの on_deletePROTECT となっています(上記条件の 2.)。

また、ModelAModelB も親子関係にあり、これらのリレーションの on_deleteCASCADE となっています。

つまり、ModelA のオブジェクトを削除する操作を行うと、同時にそのオブジェクトが子として持つ ModelB のオブジェクトも削除されることになります(上記条件の 3.)。

このようなモデル・リレーションの構成の場合、ModelBModelC のリレーションの on_deletePROTECT or RESTRICT のどちらに設定するかで ModelA のオブジェクト削除時の動作が大きく異なります。

まずは、ModelBModelC のリレーションの on_delete を PROTECT と設定した場合の動作について考えていきましょう!

この場合、ModelA のオブジェクトを削除すると、どのオブジェクトが削除されることになるでしょうか?

結論を先に言っておくと、正解は「全てのオブジェクトが削除されない」になります。

まず、ModelA のオブジェクトを削除しようとすると、そのオブジェクトが子として持つ ModelB のオブジェクトが削除されることになります。これは、ModelAModelB のリレーションの on_deleteCASCADE に設定されているからです。

PROTECTとRESTRICTの違いを説明する図2

しかし、ModelBModelC とのリレーションの on_deletePROTECT であるため、ModelB のオブジェクトは削除することができないことになります(ModelC のオブジェクトを子として持っているため)。つまり、ModelB のオブジェクトを削除しようと動作したときに例外が発生し、全てのオブジェクトの削除に失敗することになります。

PROTECTとRESTRICTの違いを説明する図3

ですが、考え方によっては「全てのオブジェクトが削除される」と捉えることもできます。

この際の考え方は次のようになります。

まず、ModelA のオブジェクトと ModelC のオブジェクトとのリレーションの on_delete は CASCADE です。ですので、ModelA のオブジェクトを削除した場合、そのオブジェクトを親とする ModelC のオブジェクトは全て削除されることになります。

PROTECTとRESTRICTの違いを説明する図4

そうなると、ModelB のオブジェクトの子オブジェクト(つまり ModelC のオブジェクト)は削除されて存在しないことになります。したがって、ModelB のオブジェクトは削除可能となり、ModelA のオブジェクトが削除されるのと同時に ModelB のオブジェクトも削除されることになります(ModelAModelB のリレーションの on_delete が CASCADE のため)。

PROTECTとRESTRICTの違いを説明する図5

そして、削除操作が行われた ModelA オブジェクトの削除も行われ、結果的に全てのオブジェクトが削除されることになります。

PROTECTとRESTRICTの違いを説明する図6

この考え方も別に不自然ではないですよね?

つまり、あるオブジェクトに対して複数種の親オブジェクトが存在し、さらにそれらの親オブジェクトが同一操作の中で削除される場合、どの親オブジェクトとのリレーションの on_delete を優先するかによって削除に成功 or 削除に失敗が変わることになります。

要は、オブジェクトの削除結果(成功 or 失敗)は、オブジェクト削除時に優先する経路に依存するということになります。

優先する経路によって削除の成功or失敗が変わる様子を示す図

こういった優先する経路を開発者が選択できるよう、PROTECT だけでなく、RESTRICT の設定値が用意されているのだと思います。そして、上記の2つの考え方における1つ目の考え方に従ってオブジェクトの削除を行なっていくのが PROTECT であり、2つ目の考え方に従ってオブジェクトの削除を行なっていくのが RESTRICT となります。

より具体的には、on_delete=PROTECT を設定したリレーションが存在する場合、そのリレーションを経由する経路が最優先されて削除が行われていくことになります。したがって、on_delete=PROTECT を設定したリレーションにおいて「子オブジェクトを持つ親オブジェクト」が存在する場合、必ず例外が発生して削除に失敗することになります。

on_delete=PROTECTのリレーションが存在する場合の削除が優先される経路を示す図

それに対し、on_delete=RESTRICT が設定されたリレーションは優先されません。他のリレーションが優先されます。したがって、on_delete=RESTRICT を設定したリレーションにおいて「子オブジェクトを持つ親オブジェクト」が存在したとしても、例外が発生するとは限りません。

on_delete=RESTRICTのリレーションが存在する場合の削除が優先される経路を示す図

そのため、リレーションの構成の仕方によっては、on_delete=RESTRICT を設定したリレーションにおいても「子オブジェクトを持つ親オブジェクト」が削除されることになります。つまり、on_delete=RESTRICT を設定したリレーションにおいて、「子オブジェクトを持つ親オブジェクト」が削除されるかどうかはリレーションの構成の仕方に依存します。

それに対して PROTECT の場合、on_delete=PROTECT を設定したリレーションにおいて、「子オブジェクトを持つ親オブジェクト」が削除されるかどうかはリレーションの構成の仕方に依存せず、絶対に削除されることはありません。

したがって、PROTECT に関しては、この on_delete が設定されたリレーションにおいて子オブジェクトを持つ親オブジェクトの削除が完全に禁止されますが、RESTRICT に関しては禁止ではなく制限するだけになります。

これらの点が、RESTRICT と PROTECT の違いとなります。

PROTECTとRESTRICTの違いを示す図

上記のような違いがあるため、開発したいアプリに合わせて PROTECTRESTRICT とは適切に使い分けすることが望ましいです。

例えば、下記のようにモデル群を定義したとします。このモデルは Django 公式チュートリアルRESTRICT の説明時に用いられているものを引用したものになります(少し変更しています)。このモデルを音楽配信ウェブサービスで使用することを考えていきましょう!

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)

AlbumSongArtist が発表したものであり、Artist の子モデルとなります。

また、Album の中には複数の Song が存在するため、SongAlbum の子モデルでもあります。

さらに、Album には複数の ArtistSong が含まれる可能性があり、この場合、Album の削除をすると他の ArtistSong が削除されることになってしまうため、AlbumSong のリレーションの on_delete は RESTRICT となっています。

具体的なオブジェクト構成例は下の図のようになります。

RESTRICTの詳細の説明図1

ちょっとややこしいですが、album_bartist_y が子として持つ Album オブジェクトになりますが、この album_bartist_x の子オブジェクトである song_2 を子として持っています。artist_xalbum_b に楽曲提供しているイメージになるかと思います。

そのため、もし album_b が削除されると artist_x の曲まで削除されることになってしまいます。これを防ぐために AlbumSong のリレーションの on_delete は RESTRICT となっています。

ここで、音楽配信ウェブサービスと artsit_x との契約が切れ、artist_x の持つ Album オブジェクトと Song オブジェクトを全て削除しなければならなくなった場合のことを考えてみましょう。

もし、AlbumSong のリレーションの on_delete が PROTECT であれば artist_x を削除しようとしても失敗することになります。なぜなら、artist_x を削除しようとすると on_delete=PROTECT のリレーションを経由する経路上のオブジェクトが優先されて削除されていくことになり、album_a 削除時に例外が発生することになるからです。

RESTRICTの詳細の説明図2

したがって、artist_x との契約が切れた場合、AlbumSong のリレーションの on_delete が PROTECT であれば、まずは album_a が子として持つ Song オブジェクト全てを削除してから artist_x を削除する必要があることになります。図では削除必要な Song オブジェクトは1つですが、この Song オブジェクトが大量にある場合、 Song オブジェクトを削除をするのは結構大変ですよね…。

RESTRICTの詳細の説明図3

それに対し、AlbumSong のリレーションの on_delete が RESTRICT である場合、artist_x の削除に成功することになります。なぜなら、artist_x を削除しようとすると on_delete=RESTRICT のリレーションを経由する経路上のオブジェクトが優先されず、先に artist_x の子の Song オブジェクトが削除されることになるからです(ArtistSong のリレーションの on_delete は CASCADE)。

RESTRICTの詳細の説明図4

そして、これによって album_a の子オブジェクトが存在しなくなるため、artist_x の削除時に album_a も削除されることになります(ArtistAlbum のリレーションの on_delete は CASCADE)。

こういった動作から、PROTECT よりもRESTRICT の方がオブジェクトの削除が楽である点を確認していただけると思います。

さて、ここまでは artist_x を削除する例について考えましたが、artist_x を削除せずに artist_y の削除をしようとした際にはどのような動作になるでしょう?

この場合、AlbumSong のリレーションの on_delete が RESTRICT であっても artist_y の削除には失敗することになります。artist_x 削除時の時と同様に、先に artist_y が子として持つ Song オブジェクトを先に削除するように動作していくのですが、artist_y が子として持つ Song オブジェクトを全て削除したとしても album_b の子オブジェクトは全てなくなりません。

RESTRICTの詳細の説明図5

そのため、album_b は on_delete=RESTRICT の影響によって削除することが出来ないことになります。したがって、album_b を削除しようとした際に例外が発生し、オブジェクトの削除に失敗することになります(削除途中で例外が発生する場合、その過程で削除されようとしたオブジェクトは全て削除されないことになります)。

言い方を変えれば、他の Artist オブジェクトを持つ Album オブジェクトの削除の防止に成功しているとも言えます。

このように、on_delete=RESTRICT は、無関係なオブジェクトの削除を防ぎつつ、関連性のあるオブジェクトのみを一括して削除するのに適した便利な設定値になります。

ちなみに、上記の例において、artist_x を削除してから artist_y を削除した場合は削除に成功することになります。

また、下の図のようなオブジェクト・リレーション構成の場合、artist_y を直接削除した場合は削除に失敗しますが、company(レコード会社)を削除した場合は全てのオブジェクトの削除に成功することになります。

RESTRICTの詳細の説明図6

これらの理由についても、on_delete=RESTRICT を経由する経路を “優先せず” にオブジェクトを削除していくことを考えていけば理解していただけるのではないかと思います(もちろん RESTRICTPROTECT とした場合は company を削除すると例外が発生することになります)。

なんとなく PROTECTRESTRICT の違いについては理解していただけたでしょうか?

ただ、この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 を適切に設定していくようにしましょう!

同じカテゴリのページ一覧を表示