このページでは Django における自己参照型リレーションについて解説していきます。
自己参照型リレーションはリレーションの一種になります。このサイトではリレーションについて下記ページで解説を行なっています。
【Django入門7】リレーションの基本上記ページでも解説しているように、リレーションを利用することで2つのモデルクラスの間に関連性を持たせることができます。
例えば、上記ページの 掲示板アプリでリレーションを利用してみる では、リレーションを利用してユーザーとコメントとを関連付け、ユーザーをコメントの投稿者として扱う例を示しています。
この関連付けは異なるモデルクラスのインスタンス同士で行うことも多いのですが、同じモデルクラスのインスタンス同士を関連づけることも可能で、そのような関連付けのことを自己参照型リレーションと呼びます。
このような関連付けを行うメリットをすぐには思い付かない方もおられるかもしれませんが、実は自己参照型リレーションを利用するメリットは多く、これを利用することで実現可能となる機能も多いです。例えばコメントの返信やフォロー機能なども、自己参照型リレーションを利用することで簡単に実現できます。
同じモデルクラスのインスタンス同士を関連付けるという点にややこしさを感じるかもしれませんが、図やコードで補足しながらできるだけ分かりやすく解説していきますので、是非このページで自己参照型リレーションについて学んでいっていただければと思います。
Contents
自己参照型リレーション
では、自己参照型リレーションについて解説していきます。
前述の通り、一般的なリレーションは「2つのモデルクラスのインスタンスの関連付け」のことになります。それに対し、自己参照型リレーションは「同じモデルクラスのインスタンス同士の関連付け」のことになります。
下記ページでも解説していますが、モデルクラスはデータベースのテーブルであり、モデルクラスのインスタンスはそのテーブルのレコードに該当します。
【Django入門6】モデルの基本つまり、自己参照型リレーションとは、同じテーブル内のレコードを関連付けることであると考えることができます。
このページでは、このような関連付けのことを自己参照型リレーションと呼びますが、他にも自己結合などと呼ぶこともあるようです。
自己参照型リレーションの実現方法
続いて、自己参照型リレーションの実現方法について解説していきます。
スポンサーリンク
自分自身のモデルクラスに対して関連性を持たせる
まず、リレーションは、モデルクラスに対してリレーションフィールドを定義することで実現することができます。
Django においては、リレーションフィールドとして下記の3つが用意されており、これらをモデルクラスに定義する、つまり、これらのインスタンスを値とするクラス変数を定義してやることで、2つのモデルクラスに関連性を持たせ、それらのインスタンス同士を関連付けることができるようになります。
OneToOneField
:1対1のリレーションForeignKey
:多対1のリレーションManyToManyField
:多対多のリレーション
上記の3つのフィールドの違いや詳細に関しては下記ページでまとめていますので、詳しくは下記ページを参照していただければと思います。このページでは、まずは主に ForeignKey
を利用する例を用いて自己参照型リレーションの解説を行なっていきます。その後、ManyToManyField における自己参照型リレーション で ManyToManyField
で自己参照型リレーションを実現する場合の特記事項・注意事項について解説していきます。
さて、リレーションフィールドについての解説に話を戻すと、リレーションフィールドを定義する際には、リレーションフィールドの第1引数にモデルクラスを指定する必要があります。これにより、リレーションフィールドを定義したモデルクラスのインスタンスと、第1引数に指定したモデルクラスのインスタンスとで関連付けを行うことができるようになります。
例えば、下記のようにモデルクラスを定義した場合、Student
のインスタンスと Club
のインスタンスとで多対1の関連付けが行えるようになります。
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=Club)
このように、リレーションフィールドの第1引数に、そのリレーションフィールドを定義するモデルクラスとは異なるモデルクラスを指定すれば、異なるモデルクラスのインスタンスを関連付けることができることになります。
それに対し、リレーションフィールドの第1引数に、そのリレーションフィールドを定義するモデルクラスと同じモデルクラス、すなわち自分自身のモデルクラスを指定すれば、同じモデルクラスのインスタンス同士を関連付けることができることになります。これこそが自己参照型リレーションであり、つまりは自己参照型リレーションを実現するためには、リレーションフィールドの第1引数に自分自身のモデルクラスを指定すればよいことになります。
より具体的には、下記のようにモデルクラスの定義を行えば、自己参照型リレーションを実現することができることになります。
class モデルクラス名(models.Model):
フィールド名 = models.ForeignKey('モデルクラス名', 略)
例えば、下記のように Student
を定義すれば、
class Student(models.Model):
loving = models.ForeignKey('Student', on_delete=models.CASCADE)
Student
のインスタンス同士を関連付けることができることになります。
この辺りの、実際のインスタンス同士の関連付けは、通常のリレーションの時同様にビューやモデルクラスのメソッドで実施することになります。また、自己参照型リレーションでのインスタンスの関連付けには、通常のリレーションには無い特徴がありますので、その点については、後述の 能動的な関連付けと受動的な関連付け の節で解説を行います。
自分自身のモデルクラスは文字列で指定する
ただし、自分自身のモデルクラスをリレーションフィールドの引数に指定する際には、その自分自身のモデルクラスを必ず文字列として指定する必要があります。この点は自己参照型リレーション実現時のポイントになると思います。
そのため、先ほど示したモデルクラスの定義例においては、ForeignKey
の第1引数を Student
ではなく 'Student'
とシングルクォーテーションマークで囲う形で記述しています。もちろんダブルクォーテーションマークで囲っても良いです。重要なのは、文字列として指定することになります。
もしくは、リレーションフィールドの第1引数に 'self'
を指定した場合も自己参照型リレーションが実現できることになります。ただし、この場合も文字列で指定する必要がある点に注意してください。
class モデルクラス名(models.Model):
フィールド名 = models.ForeignKey('self', 略)
下記のようにフィールドの第1引数への指定が文字列ではなく、モデルクラスそのもの(クラスオブジェクト)を指定した場合、マイグレーション実行時等に例外が発生することになるので注意してください。
class Student(models.Model):
loving = models.ForeignKey(Student, on_delete=models.CASCADE)
実際に発生する例外は下記のようなものになります。
name 'モデル名' is not defined
Python インタプリタはモデルクラスの中で定義しているフィールドを全て認識した後にモデルクラス自体を認識するようになっているため、自身の定義の中で自分自身を参照すると、まだ認識していないモデルクラスが使用されているとみなされて例外が発生することになります。
また、以降では、第1引数に '自分自身のモデルクラス名'
or 'self'
を指定したリレーションフィールドのことを『自己参照型リレーションフィールド』と呼ばせていただきます。
自己参照型リレーションの特徴
自己参照型リレーションの実現方法については理解していただけたと思いますので、次は自己参照型リレーションの特徴について説明していきます。
スポンサーリンク
同一のモデルクラスのインスタンスを関連付け可能
まず、自己参照型リレーションにおける特徴の1つ目として、同じモデルクラス(テーブル)のインスタンス同士を関連付けることができる点が挙げられます。これは、ここまで説明してきた通りですね!
ウェブアプリにおいて、同じモデルクラスのインスタンス同士を関連付ける機会は多いです。
例えば、掲示板アプリのコメントの返信機能は、コメント同士の関連付けによって実現されることになります。関連付けの意味合いを「返信する」と解釈すれば、コメントB
と コメントA
を関連付けることで、コメントB
が コメントA
への返信であることが管理できることになります。
この場合、同じコメント同士の関連付けになりますので、自己参照型リレーションによってコメントの返信が実現できることになります。例えば下記のように Comment
を定義すれば、この Comment
を利用するウェブアプリではコメントの返信機能が実現できることになります。
from django.db import models
class Comment(models.Model):
text = models.CharField(max_length=256)
reply = models.ForeignKey('self', on_delete=models.CASCADE)
他にも、SNS 等のフォロー機能に関しても、ユーザーがユーザーをフォローするというユーザー同士の関連付けによって実現可能ですし、親子関係を管理する機能に関しても、人が人の親(もしくは人が人の子)という人同士の関連付けによって実現可能です。
こんな感じで、同じモデルクラスのインスタンスを関連付けることで様々な機能が実現可能で、同じモデルクラスのインスタンスの関連付けが必要になる場面は多いです。そして、この同じモデルクラスのインスタンスの関連付けを実現するのが自己参照型リレーションとなります。
追加されるデータ属性は2つ
自己参照型リレーションの特徴の2つ目として、自己参照型リレーションフィールドを定義することによって、そのフィールドが定義されたモデルクラスのインスタンスに2つのデータ属性が追加される点が挙げられます。
ManyToManyField
を利用する場合、自己参照型リレーションフィールドを定義した場合でも、引数の指定によっては追加されるデータ属性が1つのみとなることがあります
これに関しては、後述の ManyToManyField における自己参照型リレーション で詳細を説明します
下記ページで解説しているように、リレーションフィールドを定義することによって、そのフィールドの定義先のモデルクラスのインスタンスと、そのフィールドの第1引数で指定したモデルクラスのインスタンスのそれぞれにデータ属性が追加されることになります。
【Django入門7】リレーションの基本そして、この追加されるデータ属性の名称やデータ属性の管理対象をまとめた表が下図となります。表内の 関連モデルクラス
は、リレーションフィールドの第1引数に指定したモデルクラスを示しています。また、追加されるデータ属性の 相手のモデルクラス名
の部分はすべて小文字となりますし、関連モデルクラス
に追加されるデータ属性の名称は related_name
引数で変更することも可能です。
例えば下記のように Club
と Student
を定義したのであれば、Student
にはリレーションフィールドのフィールド名、すなわち club
というデータ属性が追加され、さらに Club
には、相手のモデルクラス名_set
、すなわち student_set
というデータ属性が追加されることになります。
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)
自己参照型リレーションフィールドにおいても、結局追加されるデータ属性は先ほど示した表と同様になります。ですが、自己参照型リレーションフィールドの場合、関連モデルクラス = フィールドの定義先のモデルクラス
となりますので、リレーションフィールドを定義したモデルクラスに両方のモデルクラスに対するデータ属性が追加されることになります。
例えば下記のように Student
を定義したのであれば、Student
にはリレーションフィールドのフィールド名、すなわち loving
というデータ属性と、相手のモデルクラス名_set
、すなわち student_set
というデータ属性が追加されることになります。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE)
ということで、一般的なリレーションフィールドを定義する時と同じルールでデータ属性が追加されることになります。ただ、関連モデルクラス = フィールドの定義先のモデルクラス
という点が異なるだけです。関連モデルクラス = フィールドの定義先のモデルクラス
を考慮して追加されるデータ属性について整理した表が下の図になります。
また、上図の表にも記していますが、リレーションフィールドを定義することでインスタンスに追加されるデータ属性は、そのインスタンスに関連付け可能なインスタンスの個数に応じて管理対象となるオブジェクトが異なります。
関連付け可能なインスタンスの個数が 1
であるデータ属性では、相手のモデルクラスの1つのインスタンスを単に管理することになります。また、関連付け可能なインスタンスの個数が 多
であるデータ属性では、相手のモデルクラスのインスタンスの集合を管理することになります。
通常のリレーション同様に、この辺りを意識しながらデータ属性を扱うことが重要となります。
能動的な関連付けと受動的な関連付け
とりあえず、自己参照型リレーションフィールドを定義したモデルクラスに2つのデータ属性が追加されることは理解していただけたのではないかと思います。
問題は、これらの2つのデータ属性をどう使い分けるのか?という点になります。ここがややこしい…。
結論としては、これらの2つのデータ属性は「能動的な関連付け」を行う場合と「受動的な関連付け」を行う場合とで使い分けることになります。
この点について、下記の Student
を例にして解説してきたいと思います。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
使用するデータ属性による関連付けの違い
まず、上記のように Student
を定義すれば、Student
のインスタンスには loving
というデータ属性と student_set
というデータ属性の2つが追加されることになります。
また、loving
のデータ属性で管理するのは1つのインスタンスなので、同じモデルクラス(Student
)の1つのインスタンスと関連付けを行うことができることになります。それに対し、student_set
のデータ属性で管理するのは集合なので、同じモデルクラスの複数のインスタンスとの関連付けを行うことができることになります。
ここで、実際に studentA
と studentB
の2つの Student
のインスタンスで、これらの関連付けの意味合いを確認していきましょう。
まず、studentA
から loving
で studentB
を関連付けることを考えていきましょう!この studentA
からの studentB
の関連付けは、下記のような処理によって実現可能です。
studentA.loving = studentB
studentA.save()
ここで、loving
での関連付けを「好き」という意味合いで捉えるとすると、この関連付けによって「studentA
は studentB
が好き」という関係性が生まれることになります。これは、studentA
で考えると能動的な関連付けであると考えられます。つまり、この時の loving
は能動的な関連付けを管理するためのデータ属性となります。
と、同時に、「studentB
は studentA
から好かれている」という関係性も生まれることになりますね。これは、studentB
で考えると受動的な関連付けであると考えられます。この例だけでなく、一方のインスタンスからの能動的な関連付けが行われると、その裏で、必ず他方のインスタンスでの受動的な関連付けが発生することになります。これは、現実の世界でも同様です。
そして、このような受動的な関連付けは、追加される2つのデータ属性の内、能動的な関連付けを管理するためのデータ属性と “異なる方のデータ属性” で管理されることになります。つまり、今回の場合は student_set
が受動的な関連付けを管理するためのデータ属性ということになります。この例においては、student_set
での関連付けは「好かれている」という意味合いになりますね!そして、前述の通り、一方のインスタンスからの能動的な関連付けが行われると、その裏で、必ず他方のインスタンスでの受動的な関連付けが発生することになりますので、studentA.loving
によって studentB
が能動的に関連付けられると、自動的に、studentB.student_set
によって studentA
が関連付けられることにもなります。
ここまで説明してきたように、自己参照型リレーションフィールドの定義によって、そのフィールドの定義先のモデルクラスのインスタンスには2つのデータ属性が追加されることになり、一方のデータ属性は能動的な関連付けを行い、他方のデータ属性は受動的な関連付けを行うものとなります。そして、一方のインスタンスから能動的な関連付けを行えば、他方のインスタンスでも受動的な関連付けが行われることになります。この辺りが、ここまでの説明のポイントになります。
ただ、通常のリレーションフィールドの追加によって追加されるデータ属性に関しても、追加先が異なるモデルクラスのインスタンスにはなるものの、能動的な関連付けと受動的な関連付けの2つが追加されるという点は同様になります。そして、一方のインスタンスから能動的な関連付けを行えば、他方のインスタンスでも受動的な関連付けが行われるという点も同様です。
ですが、通常のリレーションフィールドを定義した場合は、一方のインスタンスからは能動的な関連付け or 受動的な関連付けの片方しか実施できません。それに対し、自己参照型リレーションフィールドを定義した場合、そのフィールドの定義先のモデルクラスのインスタンスには2つのデータ属性の両方が追加されるため、各インスタンスで能動的な関連付けと受動的な関連付けの両方が実施できることになります。ここは自己参照型リレーションフィールドの特徴であると言えると思います。
ということで、次は、studentA
から student_set
で studentB
を関連付けることを考えていきましょう!この studentA
からの studentB
の関連付けは、下記のような処理によって実現可能です。
studentA.student_set.add(studentB)
この関連付けは、先ほどと同様に studentA
からの studentB
の関連付けとなりますが、意味合いは大きく異なります。先ほどの説明の通り、loving
での関連付けは能動的な「好き」という意味合いになりますが、student_set
での関連付けは受動的な「好かれている」という意味合いになります。なので、上記の関連付けによって「studentA
が studentB
に好かれている」という関係性が生まれることになります。そして、これは studentA
で考えると受動的な関連付けであると考えられます。
で、これによって「studentB
は studentA
が好き」という関係性も生まれるので、studentB.loving
は studentA
を参照することになります。
このように、自己参照型リレーションの場合、1つのインスタンスから能動的な関連付けも受動的な関連付けも実施することが可能です。そして、一方のインスタンスから能動的 or 受動的な関連付けを実施すれば、自動的に他方のインスタンスでの受動的 or 能動的な関連付けも実施され、能動的な関連付けと受動的な関連付けが辻褄が合うように Django フレームワークによって制御されることになります。
また、関連付けられたインスタンスの取得に関しても通常のリレーションと同様の方法で実施することが可能です。ですが、自己参照型リレーションの場合は、1つのインスタンスから能動的な関連付けが行われたインスタンスと、受動的な関連付けが行われたインスタンスの両方を取得することが可能です。
例えば、下記のような処理を実施すれば、studentA
から能動的な関連付けが行われている Student
のインスタンス、つまり 「studentA
が好きな Student
のインスタンス」の name
を出力することができることになります。
print(studentA.loving.name)
また、下記のような処理を実施すれば、studentA
から受動的な関連付けが行われている Student
の全インスタンス、つまり「studentA
のことを好きな全 Student
のインスタンス」の name
を出力することができることになります。
for student in studentA.student_set.all()
print(student.name)
もちろん、上記の処理によって、他のインスタンスから studentA
に能動的な関連付けが行われたインスタンスも出力されることになります。
例えば下記のような処理を実行すれば、studentA
から受動的な関連付けが行われた studentB
だけでなく、studentA
と能動的な関連付けを行なった studentC
と studentD
の name
も出力されることになります。
studentA.student_set.add(studentB)
studentC.loving = studentA
sutndetD.loving = studentA
for student in studentA.student_set.all()
print(student.name)
こんな感じで、自己参照型リレーションフィールドの定義によって追加される2つのデータ属性は「能動的な関連付け管理するデータ属性」 or 「受動的な関連付けを管理するデータ属性」となり、それぞれで役割が異なります。
そして、これらのデータ属性を利用することで、特定のインスタンスから能動的な関連付けと受動的な関連付けが行われることが可能です。ただ、基本的には関連付けの実施に関しては能動的な関連付けを実施するように心がけるので良いと思います。それに対し、関連付けられたインスタンスの取得に関しては、能動的な関連付けが行われているインスタンス or 受動的な関連付けが行われているインスタンスのどちらを取得したいのかによって取得先のデータ属性を使い分ける必要があります。
どちらのデータ属性が能動的 or 受動的になる?
次は、追加される2つのデータ属性の内、どちらが能動的で、どちらが受動的となるのか?という点について解説していきます。
これは結局、ウェブアプリ内での2つのデータ属性の扱い方次第になります。
例えば、先ほどと同じく下記の Student
の例で、今度は loving
を受動的な関連付けを管理するデータ属性として扱うことを考えていきましょう。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
この場合、先ほどとは逆に loving
での関連付けは「好かれている」という意味合いになりますので、下記を実行した場合、「studentA
は studentB
に好かれている」という関係性が生まれることになります。
studentA.loving = studentB
studentA.save()
同様に、下記のように studentA
から student_set
で studentB
を関連付ければ、「studentB
は studentA
が好き」という関係性が生まれることになります。
studentA.student_set.add(studentB)
さらに、下記のような処理を実施すれば、「studentA
のことを好きな Student
のインスタンス」の name
を出力することができることになりますし、
print(student.loving)
下記のような処理を実施すれば、「studentA
が好きな全 Student
のインスタンス」の name
を出力することができることになります。
for student in studentA.student_set.all()
print(student.name)
ここまで示してきた処理は、loving
を能動的な関連付けを管理するデータ属性として説明してきたときに紹介したものと全く同じものになります。もちろん、loving
を受動的な関連付けを管理するデータ属性として扱うようになったことで、各処理の意味合いは異なっていますが、処理内容としては矛盾していません。
このように、2つのデータ属性のどちらを能動的な関連付けを管理するデータ属性として扱ったとしても、別に矛盾なくウェブアプリを開発することは可能です。重要なことは、2つのデータ属性の内、どちらを能動的な関連付けを管理するデータ属性とし、どちらを受動的な関連付けを管理するデータ属性とするのかを事前に開発者自身が決めておき、それに従ってモデル以外(ビューやテンプレートファイルなど)の実装を行うことになります。
関連付け可能なインスタンスの個数とデータ属性の役割
ただ、特に ForeignKey
を自己参照型リレーションフィールドとして定義する場合、それぞれのデータ属性での関連付け可能なインスタンスの個数によって、大体どちらを能動的 or 受動的なものとして扱えば良いのかが自然と決まることになると思います。
例えば、ここまで例として利用してきた下記の Student
で考えると、loving
での関連付け可能なインスタンスの個数は 1
となり、student_set
での関連付け可能なインスタンスの個数は 多
となります。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
したがって、loving
での関連付けを「好き」という能動的な関連付け、さらに student_set
での関連付けを「好かれている」という受動的な関連付けとすると、各 Student
が「好き」になれる人数は 1
のみであり、各 Student
が「好かれる」人数は 多
ということになります。
逆に、loving
での関連付けを「好かれる」という受動的な関連付け、さらに student_set
での関連付けを「好き」という能動的な関連付けとすると、各 Student
が「好き」になれる人数は 多
となり、各 Student
が「好かれる」人数は 1
ということになります。
このように、ForeignKey
を自己参照型リレーションフィールドとして定義する場合は、フィールド名
のデータ属性での関連付けが可能なインスタンスの個数は 1
となり、相手のモデルクラス名_set
のデータ属性での関連付けが可能なインスタンスの個数は 多
となるように決まっています。なので、能動的に関連付け可能なインスタンスの個数を 1
としたい場合は、フィールド名
のデータ属性を能動的な関連付けを管理するデータ属性とすればよいですし、逆に能動的に関連付け可能なインスタンスの個数を 多
としたい場合は、相手のモデルクラス名_set
のデータ属性を能動的な関連付けを管理するデータ属性とすればよいことになります。
このように、ForeignKey
を自己参照型リレーションフィールドとして定義するのであれば、関連付け可能なインスタンスの個数から、どちらを能動的 or 受動的な関連性を管理するデータ属性とするかを決めてやれば良いことになります。
また、能動的に関連付け可能なインスタンスの個数も、受動的に関連付け可能なインスタンスの個数も 1
としたいのであれば、OneToOneField
を自己参照型リレーションフィールドとして定義すればよいですし、両方を 多
としたいのであれば、ManyToManyField
を自己参照型リレーションフィールドとして定義すればよいことになります。
で、ここまで挙げてきた例で考えると、おそらく同時に複数の人から好かれることがあり得るという点は皆さん意見が一致するのではないかと思います。ただ、同時に複数の人を好きになることがあるかどうかは意見が分かれるところかもしれないですね…。とりあえず、同時に複数の人を好きになることが無い前提でウェブアプリを開発するのであれば、下記のように Student
を定義して loving
を能動的な関連付けを管理するデータ属性と考えれば良いと思います。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
もし、同時に複数の人を好きになることがある前提でウェブアプリを開発するのであれば、下記のようにリレーションフィールドを ManyToManyField
で定義するようにしてやれば良いことになります。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ManyToManyField('self')
データ属性の名称が超重要
ここまで説明してきたように、2つのデータ属性の役割は「能動的な関連付けを管理するデータ属性」 or 「受動的な関連付けを管理するデータ属性」のどちらかになります。
“どちらかになる” という点が非常にややこしいので、データ属性の役割が明確に分かるようにデータ属性を名付けることが自己参照型リレーションを使いこなすポイントになります。
というか、役割がデータ属性の名称から判断できないとコーディング時に混乱しますので、必ずデータ属性は能動的なものか受動的なものかが判断できるように名付けるようにしましょう。
例えば、先ほどの説明の中で下記の Student
の loving
を受動的な関連付けを管理するデータ属性として扱う例を示しました。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
ハッキリ言って、これは最悪で、受動的なものとして扱うのにデータ属性の名称が能動的な意味合いの loving
だと、モデルクラスの定義者以外は意味不明に感じますし、データ属性の名称に反対の意味の言葉を用いるとバグが発生する可能性も上がります。また、追加されるもう1つの方のデータ属性も student_set
となり、これが能動的なものとして扱えば良いのか受動的なものとして扱えば良いのかがデータ属性の名称から判断できません。
能動的な関連付け管理するデータ属性であれば「能動的な名前」を、受動的な関連付け管理するデータ属性であれば「受動的な名前」を付けるようにするべきです。
例えば、リレーションフィールドの フィールド名
の名前がつけられるデータ属性を受動的な関連付け(好かれている)を管理するデータ属性とするのであれば、下記のように Student
を定義した方がよいでしょう。下記のようにリレーションフィールドに related_name
引数を指定することで 相手のモデルクラス名_set
の方のデータ属性の名称も任意に設定できますので、特に自己参照型リレーションフィールドの場合は related_name
引数を積極的に利用することをオススメします。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loved_by = models.ForeignKey('self', on_delete=models.CASCADE, null=True, related_name='loving')
これにより、Student
のインスタンスには loved_by
と loving
という名称の2つのデータ属性が追加されることになります。そして、このデータ属性の名称より、loved_by
が受動的な関連付けを管理し、loving
が能動的な関連付けを管理することが明白になります。
また、例えば下記のような処理を読むだけで「studentA
は studentB
に好かれている」という関係性が一目瞭然になりますし、
studentA.loved_by = studentB
例えば下記のような処理を読むだけで「studentA
は studentB
が好き」という関係性が一目瞭然になります。
studentA.loving.add(studentB)
このように、データ属性の名称を適切に設定すれば、その関連付けの意味合いを深く考えなくても理解できるようになり、ソースコードの可読性を飛躍的に向上させることができます。
自己参照型リレーションの場合、2つのデータ属性が追加される点だけでもややこしいのですが、それぞれのデータ属性の意味合いが異なるので余計にややこしくなります。なので、特に自己参照型リレーションの場合は、データ属性の名称から、そのデータ属性の意味合いが理解できるようにデータ属性の名称を設定することが重要となります。
スポンサーリンク
多段階の関連性が簡単に実現できる
自己参照型リレーションの面白いところは、自己参照型リレーションフィールドを定義することで1つのインスタンスに2つのデータ属性が追加され、1つのインスタンスから能動的な関連付けと受動的な関連付けの両方が実施できるという点にあります。
そして、これによって多段階の関連性が簡単に実現することが可能となります。
通常のリレーションフィールドを定義した場合、そのフィールドの定義先のモデルクラスと、そのフィールドの第1引数で指定したモデルクラスの両方にデータ属性が1つずつのみ追加されることになります。そして、一方のモデルクラスに追加されるデータ属性が能動的なインスタンスの関連付けを管理し、他方のモデルクラスに追加されるデータ属性が受動的なインスタンスの関連付けを管理するデータ属性となります。
なので、特定のインスタンスから実施可能なのは能動的な関連付け or 受動的な関連付けの一方のみとなります。
そのため、自己参照型リレーションの時とは異なり、少なくとも一つのリレーションフィールドの定義では多段階の関連付けが実現できません。
例えば、下記のようなモデルクラスを使ってコメントの返信機能を実現することについて考えてみましょう!
from django.db import models
class Comment(models.Model):
text = models.CharField(max_length=256)
class ReplyComment(models.Model):
text = models.CharField(max_length=256)
reply = models.ForeignKey(Comment, related_name='replied_by', on_delete=models.CASCADE)
上記のように Comment
と ReplyComment
を定義した場合、ReplyComment
のインスタンスには reply
というデータ属性が、Comment
のインスタンスには replied_by
というデータ属性が追加されることになり、それぞれのデータ属性からインスタンスの関連付けを実施することが可能となります。
ここで、ReplyComment
からの reply
での関連付けは「返信する」という能動的な意味合いであると考えて説明していきたいと思います。例えば、reply_comment
を ReplyComment
のインスタンス、comment
を Comment
のインスタンスと考えれば、下記を実行することで「reply_comment
は comment
に返信した」という関連性が生まれることになります。
reply_comment.reply = comment
reply_comment.save()
と同時に「comment
は reply_comment
から返信された」という関連性も生まれることになり、上記の処理によって comment.replied_by
の集合に reply_comment
が追加されることになります。
そして、このような関連付けにより、コメント(Comment
のインスタンス)への返信機能を実現することができることになります。
ですが、一点問題があります。それは、返信コメント(ReplyComment
)への返信ができないという点になります。
ReplyComment
のインスタンスは能動的な関連付けを行う reply
を持っているので、前述の通り「返信する」という能動的な関連付けを Comment
のインスタンスに対して行うことは可能です。ですが、ReplyComment
のインスタンスは受動的な関連付けを行う replied_by
は持っていないので「返信される」という受動的な関連付けを行うことはできないのです。
つまり、通常のリレーションフィールドを1つ定義しただけでは、1つのインスタンスから能動的な関連付け or 受動的な関連付けの一方向の関連付けしか行うことができません。1つのインスタンスが両方の関連付けを行うことができないため、例えば下の図のような「返信コメントに返信する」や「返信コメントの返信コメントに返信する」ような多段階な関連付けは実現できないことになります。
それに対し、自己参照型リレーションの場合、1つのインスタンスが能動的な関連付けと受動的な関連付けの両方を実施することが可能なので、上の図のような多段階の関連付けも簡単に実現することができます。
ということで、次は下記の Comment
を使ってコメントの返信機能を実現することについて考えてみましょう!
from django.db import models
class Comment(models.Model):
text = models.CharField(max_length=256)
reply = models.ForeignKey('Comment', related_name='replied_by', on_delete=models.CASCADE)
このように Comment
を定義すれば、Comment
のインスタンスには reply
と replied_by
の2つのデータ属性が追加されることになります。先ほどと同様に、reply
での関連付けは「返信する」という能動的な意味合いであると、この Comment
のインスタンスは、reply
によって能動的な関連付けも実施可能であり、さらに replied_by
によって受動的な関連付けも実施可能となります。
例えば、commentA
と commentB
と commentC
とを Comment
のインスタンスとすれば、まず下記を実行することで「commentB
は commentA
に返信した」という関連性が生まれることになります。
commentB.reply = commentA
commentB.save()
つまり、commentB
は commentA
への返信コメントということになりますね!
さらに、下記の2つの処理のいずれかを実行すれば、次は「commentC
は commentB
に返信した」という関連性が生まれることになります。下記の2つの処理は、関連付けの仕方は異なるものの、関連付けの結果は同じとなります。
commentB.replied_by.add(commentC)
commentC.reply = commentB
commentC.save()
これにより、commentC
は commentB
への返信コメントということになります。そして、commentB
は commentA
への返信コメントですので、返信コメントへの返信が実現できているということになります。
ここでのポイントは commentB
の関連付けで、この commentB
からは commentA
との能動的な関連付けと commentC
との受動的な関連付けの両方が行われているという点になります。このように、一方向に対して多段階の関連付けを行おうとすると、各インスタンスには能動的な関連付けと受動的な関連付けの両方が必要となります。
そして、自己参照型リレーションの場合は、自己参照型リレーションフィールドを定義するだけで各インスタンスがそれを満たすことになるので、一方向に対する多段階の関連付けも簡単に実現することができることになります。
こういった多段階の関連付けが必要になる場面も多いです。例えば Twitter のフォローも多段階の関連付けによって実現されることになります。そのため、自己参照型リレーションによって、多段階の関連付けが容易に実現可能であることも是非覚えておいてください!
ManyToManyField
における自己参照型リレーション
さて、ここまで主に ForeignKey
をリレーションフィールドとした自己参照型リレーションについて解説してきました。
次は、ManyToManyField
をリレーションフィールドとした自己参照型リレーションについて解説していきます。基本的には ManyToManyField
の場合も ForeignKey
の場合と同じ感覚で自己参照型リレーションを利用することも可能なのですが、注意が必要になるのが引数 symmetrical
になります。
symmetrical=False
を指定した場合は、前述の通り ForeignKey
の場合と同じ感覚で自己参照型リレーションを利用することができます。が、symmetrical=True
を指定した場合は、少し特殊なリレーションとなるため、ForeignKey
と同じ感覚で自己参照型リレーションを利用することはできません。ManyToManyField
をリレーションフィールドとする場合は、この点について注意をしながらモデルクラスを定義する必要があります。
ということで、ここでは ManyToManyField
による自己参照型リレーションについて、特に symmetrical
引数に注目しながら解説をしていきたいと思います。
symmetrical
とは
では、まずは、この引数 symmetrical
について解説します。
この symmetrical
は ManyToManyField
のコンストラクタに指定可能な引数になります。
そして、この symmetrical
は、自己参照型リレーションでの「対称な関連付け」を強制するかどうかを指定するための引数になります。symmetrical=True
を指定した場合は対称な関連付けが強制されることになります。したがって、非対称な関連付けを実現したいのであれば、ManyToManyField
のコンストラクタには symmetrical=False
を指定する必要があります。
ちなみに、symmetrical
のデフォルト値は、ManyToManyField
のコンストラクタの第1引数に 'self'
を指定している場合は True
となり、第1引数に '自分自身のモデルクラス名'
を指定している場合は False
となります。ちょっと分かりにくいので基本的には明示的に引数へ symmetrical=False
or symmetrical=True
を指定するのが良いと思います。
非対称な関連付けと対称な関連付け
非対称な関連付けとは、2つのインスタンスの内、一方のインスタンスからのみ能動的(or 受動的)な関連付けが行われている状態のことを言います。
ここでは、下記の Student
を例に挙げて「非対称な関連付け」について説明していきます。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ManyToManyField('self', symmetrical=False, related_name='loved_by')
この Student
では ManyToManyField
の symmetrical=False
を指定しているため、非対称な関連付けが実現可能です。そして、この場合は ForeignKey
と同様の感覚、つまり、関連付けを行うと 能動的な関連付けと受動的な関連付け と同様の結果が得られることになります。
まず、loving
を「好き」という能動的な関連付けとして考え、さらに studentA
と studentB
を Student
のインスタンスとすると、下記を実行すれば studentA
から studentB
への関連付けを行うことができます。
studentA.loving.add(studentB)
そして、この関連付けによって「studentA
は studentB
が好き」という関係性が生まれることになります。同時に、「studentB
は studentA
から好かれている」という関係性が生まれることになり、studentB.loved_by
の集合に studentA
が追加されることになります。
なんですが、「studentB
は studentA
から好かれている」は、あくまでも「studentA
は studentB
が好き」を言い換えた言葉であり、関係性としては同じです。能動的な関係性で言えば、「studentA
は studentB
が好き」が成立しているだけで、「studentB
は studentA
が好き」という関係性はありません。この場合は、studentA
の studentB
への片思いという状態ということになります。
このような、一方のインスタンスからのみ能動的(or 受動的)な関連付けが行われている状態の関連付けのことを「非対称な関連付け」と呼びます。
今度は下記の処理について考えてみましょう。
studentA.loving.add(studentB)
studentA.loved_by.add(studentB)
この場合、studentA
の loving
によって studentB
が関連付けられ、さらに studentB
の loving
によって studentA
が関連付けられるため、「studentA
は studentB
が好き」と「studentA
は studentB
から好かれている」の2つの関連性が生まれることになります。また、後者に関しては、能動的な関連付けで考えると「studentB
は studentA
が好き」という関係性と捉えられます。つまり、この場合、studentA
から studentB
が能動的に関連付けられ、さらに studentB
から studentA
が能動的に関連付けられていることになります。つまり、この場合は両想いということになります。
このような、双方のインスタンスからの両方で能動的(or 受動的)な関連付けが行われている状態の関連付けのことを「対称な関連付け」と呼びます。
このように、能動的 or 受動的な関連付けの一方にのみ着目し、その関連付けが両方向から行われていれば「対称な関連付け」となりますし、一方向から行われていれば「非対称な関連付け」となります。
互いに関連付けが行われていない場合も「対象な関連付け」に含まれます
そして、前述の通り、symmetrical
は ManyToManyField
への引数の1つであり、自己参照型リレーションでの「対称な関連付け」を強制するかどうかを指定するための引数になります。symmetrical=False
が指定されている場合、自己参照型リレーションでの「対称な関連付け」は強制されません。
つまり、symmetrical=False
を指定した場合、非対称な関連付け、すなわち一方のインスタンスからのみ能動的(or 受動的)な関連付けが行われている状態の関連付けが実現可能ですし、対称な関連付け、すなわち双方のインスタンスからの両方で能動的(or 受動的)な関連付けが行われている状態の関連付けも実現可能です。
このように、非対称・対象の両方の関連付けが実現できるのが symmetrical=False
の効果ということになります。
それに対し、symmetrical=True
を指定した場合、もしくは symmetrical
引数を指定しなかった場合は、対称な関連付けが強制されることになります。つまり、一方のインスタンスから非対称な関連付けを行うと、対称な関連付けとなるよう、自動的に他方側のインスタンスからの関連付けが行われることになります。そのため、この場合は、一方のインスタンスからのみ能動的(or 受動的)な関連付けが行われている状態が存在しないことになります。
ここからは、下記の Student
を例にして、symmetrical=True
の効果を確認したいと思います。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ManyToManyField('self', symmetrical=True)
先ほどと同様に、loving
を能動的な関連付けとして考え、さらに studentA
と studentB
を Student
のインスタンスとすると、下記を実行すれば studentA
から studentB
への関連付けを行うことができます。
studentA.loving.add(studentB)
ですが、この状態だと、能動的な関連付けに関しては studentA
から studentB
の一方向に対してのみ関連付けが行われていることになり、非対称な関連付けとなっています。
そのため、symmetrical=True
を指定している場合は、この非対称な関連付けを解消するため、studentB.loving
の集合への studentA
の追加、すなわち studentB
から studentA
への能動的な関連付けも自動的に行われることになります。つまり、これによって能動的な関連付け、すなわち loving
での関連付けが両方向から行われた状態、すなわち対称な関連付けとなります。
また、対象な関連付けの状態で、特定のインスタンスから相手のインスタンスへの関連付けが解除されたときには、相手のインスタンス側からの特定のインスタンスへの関連付けも自動的に解除されることになります。このように、symmetrical=True
を指定している場合、常に非対称な関連付けにならないよう関連付けに対する制御が自動的に実施されるようになっています。
このように、非対称な関連付けが実施された際に、非対称な関連付けのまま維持されるのが symmetrical=False
を指定した効果であり、非対称な関連付けが実施された際に、対称な関連付けとなるよう自動的に関連付けに関する制御が実施されるのが symmetrical=True
の効果となります。
スポンサーリンク
symmetrical=True
指定時のデータ属性
また、ここまで、自己参照型リレーションフィールドを定義することで、インスタンスに2つのデータ属性が追加されると説明してきましたが、リレーションフィールドが ManyToManyField
、かつ、引数に symmetrical=True
が指定されている場合は、そのモデルクラスのインスタンスに追加されるデータ属性は1つのみとなります。リレーションフィールドのフィールド名のデータ属性のみが追加されることになります。
例えば、下記のように Student
を定義した場合、Student
のインスタンスには loving
のデータ属性のみが追加されることになります。
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=32)
loving = models.ManyToManyField('self', symmetrical=True)
追加されるデータ属性が1つとなる理由は、symmetrical=True
が指定されている場合、もはや能動的な関連付けの管理と受動的な関連付けの管理を区別して行うことが無意味となるためです。
先ほど、symmetrical=True
が指定されている場合、特定のインスタンスから相手に対して能動的な関連付けを行えば、対称な関連付けを維持するため、自動的に相手からその特定のインスタンスに対する能動的な関連付けが行われると説明しました。この自動的に実施される関連付けを受動的な観点で考えた場合、これは、その特定のインスタンスからの相手に対する受動的な関連付けとなります。
つまり、特定のインスタンスから能動的な関連付けを行えば、同時に同じ相手との受動的な関連付けも行われることになります。したがって、symmetrical=True
が指定されている場合、各インスタンスにおいては能動的に関連付けられたインスタンスと受動的に関連付けられたインスタンスとが常に同じになることになります。
能動的に関連付けられたインスタンスと受動的に関連付けられたインスタンスとが常に同じなのであれば、もはやそれらを区別して管理する必要はありません。そのため、リレーションフィールドが ManyToManyField
、かつ、引数に symmetrical=True
が指定されている場合、能動的な関連付けを管理するデータ属性と受動的な関連付けを管理するデータ属性の2つが追加されるのではなく、単なる関連付けを管理するデータ属性が1つのみ追加されることになります。
また、自己参照型リレーションフィールドとして ManyToManyField
のフィールドを定義する場合、symmetrical=True
を指定した状態で引数 related_name
を指定すると、ウェブアプリ起動時等に下記のような警告文が表示されることになります。これは、前述の通り、この場合は related_name
によって名付けられるデータ属性が追加されないため、意味のない引数指定となるからになります。
WARNINGS: lovers.Student.lovings: (fields.W345) related_name has no effect on ManyToManyField with a symmetrical relationship, e.g. to "self".
symmetrical=False
と symmetrical=True
の使い分け
最後に、symmetrical=False
と symmetrical=True
の使い分けについて説明しておきます。
ここまでの説明の通り、ManyToManyField
を利用して自己参照型リレーションを実現する場合、symmetrical=False
を指定しないと非対称な関連付けを実現することができません。なので、非対称な関連付けを行いたいのであれば symmetrical=False
の指定がマストとなります。
それに対し、対称な関連付けは symmetrical=False
でも symmetrical=True
でも実現可能です。ですが、非対称な関連付けが不要な場合 or 常に対象な関連付けである必要がある場合は symmetrical=True
を利用する方が良いと思います。ここまで説明してきたように、symmetrical=True
を指定すれば追加されるデータ属性が1つのみとなるので、コーディングが楽&シンプルになります。また、関連付けの漏れなども発生しにくくなります。つまり、symmetrical=True
を指定することで開発効率が向上し、さらにバグも減らすことができます。そのため、非対称な関連付けが不要なのであれば symmetrical=True
を指定することをオススメします。
例えば、前述の Student
の場合であれば、一方の生徒が好きになったからといって、その生徒が相手からも好きになってもらえるとは限りません。そのため、現実世界に合わせたモデルクラスを定義したいのであれば symmetrical=False
を指定するのが自然だと思います。ですが、例えば Student
を利用して生徒の相思相愛の関係性だけを管理したいのであれば、symmetrical=True
を指定するのが良いと思います。
また、他の例として、私がよく遊んでいたパワサカというアプリには(サ終になってしまいました…)、他のユーザーをフォローする「フォロー機能」と他のユーザーとフレンドになる「フレンド機能」が存在しています。
「フォロー機能」は Twitter などと同じで、特定のユーザーが他のユーザーに対して一方向的に関係性を持たせるものになります。私が他のユーザーに対してフォローを行ったとしても、相手が私をフォローしてくれるとは限りません。つまり、このようなフォロー機能を実現するためには非対称な関連付けが必要となります。
それに対して「フレンド機能」の場合は、相手にフレンド申請を出し、承認された場合のみ、お互いにフレンドになれる機能となります。承認さえされれば、相手から見ても自分から見てもフレンドの関係となります。
また、フレンド関係を解消するようなことも可能で、一方からフレンドが解消されれば、他方からのフレンド関係も解消されるようになっています。つまり、このフレンド関係においては非対称な関連付けは存在しません。
したがって、上記のようなフォロー機能を実現したい場合は symmetrical=False
を指定する必要があります。それに対し、フレンド機能を実現したい場合は symmetrical=True
を指定した方がコーディングが楽&シンプルになります。ですので、このようなフォロー機能とフレンド機能を実現する際には、例えば下記のようにモデルクラスを定義することになると思います。
class User(models.Model):
name = models.CharField(max_length=32)
followings = models.ManyToManyField('self', symmetrical=False, related_name='followed_by')
friends = models.ManyToManyField('self', symmetrical=True)
結局重要なのは、開発対象のウェブアプリや機能において、インスタンス同士の非対称な関連付けが必要かどうかを考え、それに応じて symmetrical=False
と symmetrical=True
を適切に選択することになります。そのためにも、非対称な関連付けと対称な関連付けの特徴、および、symmetrical=False
と symmetrical=True
の違いについてはしっかり理解しておきましょう!
まとめ
このページでは、Django における自己参照型リレーションについて解説を行いました。
自己参照型リレーションは、同じモデルクラスのインスタンス同士の関連付けのことで、リレーションフィールドの第1引数に '自分自身のモデルクラス名'
もしくは 'self'
を指定することで自己参照型リレーションを実現することができます。
そして、自己参照型リレーションを利用することで、下記のような機能を Django で簡単に実現することができるようになります。
- コメント返信
- フォロー
- フレンド
同じモデルクラスのインスタンス同士を関連付けたくなるケースも多いですし、自己参照型リレーションの場合は、多段階の関連付けも簡単に実現することができ、この多段階の関連付けを利用するためにも利用されることが多いです。
いろんな場面で利用する機会のあるリレーションとなりますので、是非このページで解説した内容に関しては頭に入れておいていただければと思います!