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

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

このページでは、Django のリレーションフィールドに指定可能な on_delete 引数について解説します。

下記ページで解説している通り、Django ではモデルクラスにリレーションフィールドを定義することで、2つのモデルクラスのインスタンス同士を関連付けることができるようになります。

Djangoのリレーションの解説ページアイキャッチ 【Django入門7】リレーションの基本

例えば下記のように ClubStudent を定義すれば、Student のインスタンスと Club のインスタンスの間で多対1の関連付けを行うことが可能となります。そして、これによって、各生徒(Student のインスタンス)が所属するクラブ(Club のインスタンス)を管理することができるようになります。

OneToOneFieldの定義
from django.db import models

class Club(models.Model):
    name = models.CharField(max_length=32)

class Student(models.Model):
    name = models.CharField(max_length=32)
    club = models.ForeignKey(Club, on_delete=models.CASCADE, null=True)

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

今回注目したいのが、上記における on_delete 引数になります。この on_delete 引数は、リレーションフィールドの ForeignKey と OneToOneField に必須となる引数となります。

この on_delete 引数には、どういう意味があるのでしょうか?

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

on_delete

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

結論を先に言っておくと、on_delete は、子オブジェクトを持つ親オブジェクト削除時のアプリの動作を設定する引数になります。

この結論だけだと意味不明と感じる方も多いと思いますので、リレーションについてまず振り返りを行い、その中で子オブジェクトと親オブジェクトの意味合いや、on_delete の効果について説明していきたいと思います。

リレーション

リレーションとは、2つのインスタンスを関連付けることを言い、Django においてはモデルクラスにリレーションフィールドを定義することで、この関連付けを行うことができるようになります。

リレーションフィールドとは、具体的には下記の3つのことを指しており、リレーションフィールドとしてどれを定義するのかによって、実現可能な関連付けが異なります。

  • OneToOneField:1対1の関連付け
  • ForeignKey:多対1の関連付け
  • ManyToManyField:多対多の関連付け

下記は、モデルクラス Comment にリレーションフィールドを定義する具体例となります。

リレーションフィールドの定義
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    contributor = models.ForeignKey(User, on_delete=models.CASCADE)

この場合、ForeignKey の第1引数に User を指定しているため、Comment のインスタンスと User のインスタンスとの関連付けを行うことが可能となります。この場合は、Comment のインスタンスに関連付けられた User のインスタンスが、そのコメントの投稿者であると考えると関連付けのイメージが湧きやすくなると思います。また、リレーションフィールドが ForeignKey であるため、この場合は1つの User のインスタンスに対し、複数の Comment のインスタンスを関連付けすることが可能となります。

リレーションの説明図

実際のインスタンスの関連付けに関してはビューやモデルクラスのメソッドで実施可能数rことになります。例えば、先ほど示したモデルクラスの定義において、user_xUser のインスタンス、comment_1comment_2 を Comment のインスタンスとすれば、下記のように、リレーションフィールドのフィールド名(contributor)のデータ属性を利用して、これらのインスタンスの関連付けを実施することができます。 

インスタンスの関連付け
comment_1.contributor = user_x
comment_2.contributor = user_x
comment_1.save()
comment_2.save()

関連付け後の、各インスタンスの関係を図で表せば下図のようになります。

インスタンス同士の関連付けを示す図

今回は、Comment に対して ForeignKey を定義しているため、User のインスタンスとの間で多対1の関連付けが可能となりますが、ManyToManyField を定義すれば多対多の関連付けが、OneToOneField を定義すれば1対1の関連付けを行うことができるようになります。

スポンサーリンク

親オブジェクトと子オブジェクト

on_delete の意味合いが理解しやすくなるよう、次は関連付けに親子関係を導入していきたいと思います。

具体的には、2つの関連付けられたインスタンスに対し、リレーションフィールドが定義されたモデルクラスのインスタンスのことを「子オブジェクト」と記し、それに対してリレーションフィールドの第1引数に指定されたモデルクラスのインスタンスのことを「親オブジェクト」と記して解説を進めていきたいと思います。

子オブジェクトと親オブジェクトの説明図

例えば前述の下記のように定義したモデルクラスの場合、User のインスタンスと Comment のインスタンスが関連付けられているのであれば、User のインスタンスが「親オブジェクト」となり、Comment のインスタンスが「子オブジェクト」ということになります。

リレーションフィールドの定義
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    contributor = models.ForeignKey(User, on_delete=models.CASCADE)

また、例えば下記のような処理を行った場合、user_x が親オブジェクトとなり、この user_x は comment1comment_2 の2つの子オブジェクトを持つことになります。

インスタンスの関連付け
comment_1.contributor = user_x
comment_2.contributor = user_x
comment_1.save()
comment_2.save()

こんな感じで、親オブジェクトと子オブジェクトという位置づけで関連付けについて整理していくと、on_delete の意味合いが理解しやすくなると思います。

また、以降では、インスタンス同士が関連付けされている状態を示す図では、子オブジェクト側から親オブジェクト側を指す矢印で親子関係を図示するようにしていきたいと思います。例えば、上記の例の場合は次の図のように親子関係が表されることになります。

親オブジェクトと子オブジェクトの関係を示す図

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 毎に設定可能。というか、そもそも必須で設定が必要)。

例えば、前述の例で示した CommentUser を「コメント投稿が可能なウェブサイト」で利用することを考えてみましょう。User のインスタンスはウェブサイトの会員として扱い、Comment のインスタンスはコメントとして扱っていきます。さらに、Comment のインスタンスに関連付けられた 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 に指定する設定値によって、子オブジェクトを持つ親オブジェクト削除時のウェブアプリの動作が大きく変化しますし、それによって開発されるウェブアプリも大きく異なることになります。

そのため、Django でウェブアプリを開発するのであれば、on_delete に指定可能な設定値、および、設定値に応じたウェブアプリの動作についてはしっかり理解しておく必要があります。

on_delete に指定可能な設定値

ということで、まずは on_delete に指定可能な設定値の一覧を、ここで示しておきたいと思います。

Django 5.1 時点では 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

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    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

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    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つ目として、デフォルトオブジェクト自体が存在しない場合が挙げられます。上記のモデル定義の例であれば、pk1 である User のインスタンスが存在しない場合がそれに当たります。

SET_DEFAULT指定時に例外が発生するケースを説明する図1

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

django.db.utils.IntegrityError: FOREIGN KEY constraint failed

「デフォルトオブジェクトに新たな子オブジェクトを持たせることができない」ケースの2つ目として、リレーションフィールドが OneToOneField として定義されている、かつ、デフォルトオブジェクトが既に他の子オブジェクトを持っている場合が挙げられます

この場合、デフォルトオブジェクトに新たな子オブジェクトが追加されることになり、OneToOneField で実現可能な「1対1の関連付け」の関係性が崩れてしまうことになるため例外が発生します。具体的には、下記のような例外が発生することになります。

UNIQUE constraint failed: on_delete_app_comment.user_id

例えば、下記のようにモデルクラスを定義した場合、User のインスタンスと Comment のインスタンスとの間では1対1の関連付けのみが実施可能となります。つまり、1つの親オブジェクトが持つことのできる子オブジェクトは1つのみとなります。

例外が発生する例
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    contributor = models.OneToOneField(User, on_delete=models.SET_DEFAULT, default=1)

デフォルトオブジェクトが子オブジェクトを持っていない場合、最初の User のインスタンスの削除は成功します。ですが、この時点でデフォルトオブジェクトは1つの子オブジェクトを持っていることになります。

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

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

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

こういった注意点があるため、リレーションフィールドを OneToOneField で定義する場合は、on_delete への SET_DEFAULT の指定は控えた方が良いと思います。

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

class User(models.Model):
    name = models.CharField(max_length=32)

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 のインスタンスとなります)。

SET_NULLの説明図1

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

SET_NULLの説明図2

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

SET_NULLの説明図3

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

スポンサーリンク

SET

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

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

SETの説明図

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

SETの設定例1
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    contributor = models.ForeignKey(User, on_delete=models.SET(1))

また、SET には関数を指定することも可能で、例えば下記のようにモデルを定義すれば、親オブジェクト削除時に関数 get_first_user が実行され、子オブジェクトの新たな親オブジェクトとして get_first_user の返却値を pk とするオブジェクトが設定されることになります。

SETの設定例2
from django.db import models

def get_first_user():
    users = User.objects.order_by('name')
    return users[0].pk

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    contributor = models.ForeignKey(User, on_delete=models.SET(get_first_user))

上記の get_first_user は、User の全インスタンスの中から name がアルファベット順で一番早いインスタンスを親オブジェクトとして設定するような関数にしていますが、もちろん関数の作り方によって異なる User のインスタンスを親オブジェクトとして設定することも可能です。

また、適当な親オブジェクトが存在しない場合に None を返却することで、親オブジェクトが存在しない子オブジェクトにすることも可能です。

関数で行う処理によって動的に親オブジェクトを変更したいような場合に SET は便利な設定値になると思います。ただし、親オブジェクトを付け替えるわけですから、SET_DEFAULT と同様の注意点に気をつける必要があります(None を返却する場合は SET_NULL と同様の注意点に気をつける必要があります)。

DO_NOTHING

on_delete には DO_NOTHING を設定することも可能です。

名前の通り、DO_NOTHING は、親オブジェクト削除時には何も実施しない設定値となります。

つまり、on_delete=DO_NOTHING が指定されている場合、親オブジェクトが削除されたとしても、子オブジェクトの親は削除された親オブジェクトのままとなります。

DO_NOTHINGの説明図

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

ただ、これはデフォルト設定の話で、リレーションフィールドに db_constraint=False を指定すれば、存在しないインスタンスを親オブジェクトに設定できるようになり、上記の例外も発生しないようになります。ですが、それでも子オブジェクトが「存在しないインスタンス」を親オブジェクトとして参照し続けることになるため、そのままアプリを動作させ続けるとデータベース内で不整合が発生する可能性が高くなってしまうので注意してください。

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

DO_NOTHINGの設定例
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)

class Comment(models.Model):
    text = models.CharField(max_length=256)
    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

class User(models.Model):
    name = models.CharField(max_length=32)

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 が仕事の依頼者としてそれぞれ扱われることを想定した定義となっています。さらに、1つの Work のインスタンスに対し、1つの Employee のインスタンスと User のインスタンスが関連付けられるようになっています。そして、これらが関連付けされた際には、User のインスタンスと User のインスタンスの両方が、Work のインスタンスの親オブジェクトということになります。

PROTECTの利用例の説明図1

この場合、Work のインスタンスの親オブジェクトである Employee のインスタンスは、contractorOneToOneField の引数に on_delete=PROTECT が指定されているため、削除することができないことになります。つまり、仕事の請負人は、依頼者から依頼された仕事が存在する間は退会(削除)できないことになります。

PROTECTの利用例の説明図2

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

PROTECTの利用例の説明図3

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

PROTECTの利用例の説明図4

このように、on_deletePROTECT を指定することで、子オブジェクトを持つ親オブジェクトの削除を禁止することができるようになります。上記の例のように、子オブジェクトを持っている状態の親オブジェクトを削除されてしまうとウェブアプリの動作やウェブアプリの運営が破綻してしまうような場合は、それを禁止するために on_deletePROTECT を指定してやるのが良いと思います。

ちなみに、requester のリレーションフィールドには on_delete=CASCADE が指定されているため、User のインスタンスに関しては、子オブジェクトとして Work のインスタンスを持っていたとしても削除することが可能です。on_delete=CASCADE が指定されているので、User のインスタンス削除時には、その子オブジェクトの Work のインスタンスに削除されることになります。

スポンサーリンク

RESTRICT

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

RESTRICT は PROTECT に似た設定値になりますので、PROTECT と比較しながら解説を進めていきたいと思います。

また、この章の解説では、下記の ArtistAlbumSong の3つのモデルクラスを使って解説を進めていきます。より具体的には、下記の Songalbum フィールドにおける on_delete に(つまり 設定値 部分に)、PROTECTRESTRICT のそれぞれを指定したときの動作の違いを説明していきます。

RESTRICTとPROTECTの違いを示すモデルクラス
from django.db import models

class Artist(models.Model):
    name = models.CharField(max_length=10)


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=設定値)

ちなみに、上記のモデルクラスの定義は Django の公式サイトRESTRICT の解説時に用いられている定義を引用したものになります。Song は曲、Album はアルバム、Artist はアーティストを表現するクラスとなっており、これらを利用して音楽の配信サービスを実現することをイメージしてもらえると、各クラスの関係性がイメージしやすくなると思います。

RESTRICTPROTECT の共通点

前述のとおり、RESTRICT は PROTECT に似た設定値となります。例えば、Songalbum フィールドにおける on_delete を PROTECT とし、下の図のようにインスタンスが関連付けられている場合、album_1 の削除を実行すると例外 ProtectedError が発生して削除に失敗することになります。

PROTECTを指定した場合に親オブジェクトの削除に失敗する様子

ここで、図について補足しておくと、ここからは各リレーションフィールドに指定される on_delete の設定値が異なる場合があるため、上の図のように、親子関係を表す矢印(矢印の根本側が子、矢側が親)の近くに、その親子関係の関連付けを行うリレーションフィールドの on_delete への指定値を示すようにしていきます。上図は、song_1song_2album_1 の子オブジェクトであり、これらを関連付けるリレーションフィールドの on_delete には PROTECT が指定されていることを示していることになります。

で、話を戻すと、先ほどと同様のインスタンスの関連付けの構成においては、Songalbum フィールドにおける on_deleteRESTRICT を指定した場合でも、album_1 の削除を実行すると例外が発生して削除に失敗することになります。ただし、on_deleteRESTRICT を指定している場合に発生する例外は RestrictedError なります。

RESTRICTを指定した場合にも親オブジェクトの削除に失敗する様子

RESTRICTPROTECT の違い

このように、RESTRICT に関しても、基本的には PROTECT 同様に、子オブジェクトを持つ親オブジェクトとの削除を禁止する設定値と考えて良いです。ですが、異なる設定値である以上、やはり、これらの設定値には違いがあります。

この違いが出るのは、on_delete に PROTECT or RESTRICT が指定されているリレーションフィールドによって関連付けられる親オブジェクトと子オブジェクトの全てが同一の親オブジェクトを持ち、さらに、その同一の親オブジェクトと関連付けられるリレーションフィールドの on_delete に CASCADE が指定されている場合になります。以降では、この同一の親オブジェクトのことを「マスターオブジェクト」と呼ばせていただきます(ちょっと、しっくりこないかもしれませんが)。

PROTECTとRESTRICTとでアプリの動作が変化する場合のインスタンスの関連付けの構成図

この場合、マスターオブジェクトを削除すると、親オブジェクトと子オブジェクトとを関連付けるリレーションフィールドの on_delete に PROTECT を指定している場合は削除に失敗して例外が発生することになります。それに対し、RESTRICT を指定している場合は削除に成功することになります。

なぜ、このように削除の結果(成功 or 失敗)が変化するかというと、on_deletePROTECT を指定した場合と RESTRICT を指定した場合とでは、削除が実行される順序が異なるからになります。

PROTECT を指定している場合は、削除に失敗する順番で削除が実施されることになり、RESTRICT を指定している場合は削除に成功できるのであれば削除に成功する順番で削除が実施されることになります。なので、 on_delete への指定値によって、削除の結果が変化することになります。

この削除の結果が変化する様子を、前述で示した ArtistAlbumSong の3つのモデルクラスを使って具体的に確認していきたいと思います。ここでは、先ほど示したインスタンスの関連付けの構成に artist_1 を新規に導入し、さらに artist_1album_1song_1song_2 のそれぞれを関連付ける例で説明を行っていきます。

削除する順序によって削除の成功/失敗が変化することを示す図1

この構成は、先ほど示した下記条件を満たす関連付けの構成となります。

on_delete に PROTECT or RESTRICT が指定されているリレーションフィールドによって関連付けられる親オブジェクトと子オブジェクトの全てが同一の親オブジェクトを持ち、さらに、その親オブジェクトと関連付けられるリレーションフィールドの on_delete に CASCADE が指定されている

この構成において、マスターオブジェクトとなる artist_1 の削除を実行したときの動作を考えてみましょう。

まず、この artist_1 の削除を実行すると、この artist_1on_deleteCASCADE が指定されたリレーションフィールドで関連付けられたインスタンスの削除が行われることになります。具体的には、album_1 と song_1song_2 の削除が行われることになります。ただし、これらは同時に削除されるのではなく、順々に削除が行われていくことになります。で、ここでポイントになるのが、前述の通り、これらの削除を行う順序によって削除の成功 / 失敗が変化するという点になります。今回の例の場合は、album_1 を先に削除するパターンと、song_1 / song_2 を先に削除するパターンとで削除の結果が変わることになります。

album_1 を先に削除するパターンで考えると、この場合は削除には失敗することになります。なぜなら、album_1 には、on_deletePROTECT or  RESTRICT が指定されたリレーションフィールドで関連付けられた子オブジェクトが存在するからです。

削除する順序によって削除の成功/失敗が変化することを示す図2

それに対し、song_1 / song_2 を先に削除するパターンで考えると、この場合は、song_1 と song_2 が先に削除されることになります。song_1 と song_2 には子オブジェクトが存在しないため、on_delete には無関係に削除に成功することになります。そうなると、album_1 にも子オブジェクトが存在しなくなります。

削除する順序によって削除の成功/失敗が変化することを示す図3

したがって、album_1 の削除にも成功し、もちろん artist_1 の削除にも成功します。したがって、全インスタンスの削除に成功することになります。

削除する順序によって削除の成功/失敗が変化することを示す図4

この例のように、on_deletePROTECT を指定した場合も RESTRICT を指定した場合も「子オブジェクトを持つ親オブジェクトの削除が禁止される」という点は同じになるのですが、削除の順序が異なるため、それによって削除の結果(成功 or 失敗)が変わることがあります。

そして、わざと削除に失敗する順序で削除が実行されるのが、on_deletePROTECT を指定した場合になります。このような順序で削除を実施することで、on_deletePROTECT が指定されているリレーションフィールドによって親オブジェクトと子オブジェクトとが関連付けられている場合、その親オブジェクトの削除には必ず失敗することになります。すなわち、PROTECT は子オブジェクトを持つ親オブジェクトの削除を完全に禁止する設定値となります。

そのため、PROTECT の場合は、子オブジェクトを持つ親オブジェクトを削除する場合は、必ず事前に子オブジェクトを削除したり、他の親オブジェクトに関連付け直したりする必要があります。なので、削除の手間はかかります。

それに対し、on_deleteRESTRICT が指定されている場合は、極力削除に成功する順序で削除が実施されることになります。そのため、on_deleteRESTRICT が指定されているリレーションフィールドによって親オブジェクトと子オブジェクトが関連付けられていたとしても、それらが同一のマスターオブジェクトに関連付けられているのであれば、マスターオブジェクトの削除という1つの操作によって、それらの親オブジェクトと子オブジェクトも同時に削除することが可能となります。

ただし、それらの親オブジェクトと子オブジェクトが同一のマスターオブジェクトに関連付けられていないのであれば、マスターオブジェクトの削除時にも削除に失敗することになります。

例えば、音楽配信サービスで、特定のアーティストとの契約が切れて、そのアーティストが発表したアルバムと曲の配信を停止するような場合、前述の条件を満たすように、そのアーティストと、そのアーティストが発表したアルバム・曲を関連付けておけば、アーティストを削除した際に、そのアーティストが発表したアルバム・曲も一緒に削除することが可能となります。

全てのインスタンスが削除される例

なんですが、そのアーティストが発表したアルバムではあるものの、そのアルバムに他のアーティストが提供した曲が含まれている場合、その曲は異なる親オブジェクトと関連付けられていることになるため、そのアルバムを発表したアーティストを削除しても削除に失敗することになり、その他のアーティストが提供した曲を含むアルバムの削除を防ぐことができます。

song_2が他のArtistのインスタンスの子オブジェクトの場合はartist_1の削除に失敗することを示す図

ということで、基本的には RESTRICT or PROTECT のどちらを指定したとしても、基本的には子オブジェクトを持つ親オブジェクトの削除は禁止されることになります。ただし、下記の条件を満たす場合のみ、PROTECT を指定することでマスターオブジェクト削除時の各インスタンスの削除に成功するようになります。

on_delete に PROTECT or RESTRICT が指定されているリレーションフィールドによって関連付けられる親オブジェクトと子オブジェクトの全てが同一の親オブジェクトを持ち、さらに、その親オブジェクトと関連付けられるリレーションフィールドの on_delete に CASCADE が指定されている

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

ハッキリ言って、これらの PROTECTRESTRICT との使い分けはかなり難しいと思います。なので、とりあえず、子オブジェクトを持つ親オブジェクトの削除を防ぎたいのであれば、PROTECT を指定するので良いと思います。PROTECT の方が仕組みがシンプルですので、削除に手間がかかろうがコーディングは楽になるのではないかと思います。そして、PROTECT を指定した状態で関連付けを実施し、その関連付けによって上記の条件を満たすのであれば、その時に初めて RESTRICT の採用を検討してみるくらいで良いと思います。

ということで、まずは RESTRICT に関しては、特定の条件を満たさない限りは PROTECT と同じ作用をもたらす設定値であると考えるくらいで良いと思います。

スポンサーリンク

まとめ

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

on_delete はリレーションに対して設定を行う引数であり、「子オブジェクトを持つ親オブジェクト削除時のアプリの動作」を設定することのできる引数となります。

この on_delete には、Django 5.1 時点で下記の7つを設定することが可能です。

  • CASCADE:子オブジェクトも一緒に削除する
  • SET_DEFAULT:子オブジェクトの新たな親オブジェクトとしてデフォルトオブジェクトを設定する
  • SET_NULL:子オブジェクトの新たな親オブジェクトとして NULL を設定する
  • SET:子オブジェクトの新たな親オブジェクトとして特定の関数の返却値等を設定する
  • DO_NOTHING:何もしない
  • PROTECT:子オブジェクトを持つ親オブジェクトの削除を禁止する
  • RESTRICT:子オブジェクトを持つ親オブジェクトの削除を禁止する(子オブジェクトが先に削除可能なら親オブジェクトも削除する)

これらの設定値の中でどれを使用すべきかは開発したいアプリによって異なります。それぞれの設定値の意味合いをしっかり理解し、開発したいアプリに合わせて on_delete を適切に設定していくようにしましょう!

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