【Django】自己参照型リレーションについて分かりやすく解説

Djangoにおける自己参照型リレーションの解説ページアイキャッチ

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

このページでは Django における自己参照型リレーションについて解説していきます。

自己参照型リレーションはリレーションの一種になります。このサイトではリレーションについて下記ページで解説を行なっています。

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

上記ページでも解説しているように、リレーションの仕組みを利用することで2つのモデルクラスの間に関連性を持たせることができます。

例えば上記ページの 掲示板アプリでリレーションを利用してみる では、掲示板アプリで利用しているユーザーモデルクラスとコメントモデルクラスとの間にリレーションを設定することで、ユーザーとコメントとを関連付け、ユーザーを投稿者として扱う例を示しています。

このページで紹介する自己参照型リレーションは、2つのモデルクラスではなく、特定のモデルクラスから、そのモデルクラス自身に対してリレーションを設定する特殊なものとなります。メリットがすぐに思い付かないかもしれませんが、実は自己参照型リレーションを利用するメリットは多く、これを利用することで簡単に実現可能となる機能も多いです。例えばコメントの返信やフォロー機能なども簡単に実現できます。

自分自身を参照するという点にややこしさを感じるかもしれませんが、図やコードで補足しながらできるだけ分かりやすく解説していきますので、是非このページで自己参照型リレーションについて学んでいっていただければと思います。

自己参照型リレーション

前述の通り、一般的なリレーションは「2つのモデルクラスの間に関連性を持たせる仕組み」になります。

それに対し、自己参照型リレーションは「自分自身のモデルクラスに対して関連性を持たせる仕組み」になります。自分自身のモデルクラスの “インスタンス” に対して関連性を持たせる仕組みであると考えた方が分かりやすいかもしれないです。

下記ページでも解説していますが、モデルクラスはデータベースのテーブルであり、モデルクラスのインスタンスはそのテーブルのレコードに該当します。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

つまり、自己参照型リレーションとは、同じテーブル内のレコード間で関連性を持たせることであると考えることができます。

同じテーブル内のレコード間に関連性を持たせる様子

このページでは、このような関係性を自己参照型リレーションと呼びますが、他にも自己結合などと呼ぶこともあるようです。

自己参照型リレーションの実現方法

続いて、自己参照型リレーションの実現方法について解説していきます。

スポンサーリンク

自分自身に対してリレーションを設定する

リレーション自体の実現は、モデルクラスに対してリレーション設定用のフィールドを持たせることで実現する事ができます。

Django においては、リレーション設定用のフィールドとして下記の3つが用意されており、これらをモデルクラスに持たせる、つまり、これらのインスタンスを右辺とするクラス変数を定義してやることで、2つのモデルクラスに対してリレーションが設定されることになります。

  • OneToOneField:1対1のリレーション
  • ForeignKey:1対多のリレーション
  • ManyToManyField:多対多のリレーション

上記の3つのフィールドの違いや詳細に関しては下記ページでまとめていますので、詳しくは下記ページを参照していただければと思います。このページでは、まずは ForeignKey を利用する例を用いて解説を行なっていきます。その後、ManyToManyField における自己参照型リレーションManyToManyField で自己参照型リレーションを利用する場合の特記事項・注意事項について解説していきます。

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

さて、リレーションの設定方法についての解説に話を戻すと、2つのモデルクラスの間でリレーションを設定するためには、上記のフィールドの第1引数に、そのフィールドを持たせるモデルクラスと関連性を持たせたい相手となるモデルクラスを指定する必要があります。

例えば、下記のようにモデルクラスを定義した場合、StudentClub の間に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)

このように、リレーション設定用のフィールドを持つモデルクラス(Student)とは異なるモデルクラス(Club)を第1引数で指定した場合、2つのモデルクラスの間にリレーションが設定されることになります。

それに対し、第1引数に自分自身のモデルクラスを指定した場合、自分自身のモデルクラスに対してリレーションが設定されることになります。これこそが自己参照型リレーションであり、つまり、自己参照型リレーションを実現するためには、上記のフィールドの第1引数に自分自身のモデルクラスを指定すれば良いことになります。

したがって、下記のように定義を行えば自己参照型リレーションを実現することができることになります。

自己参照型リレーションの設定1
class モデルクラス名(models.Model):
    フィールド名 = models.ForeignKey('モデルクラス名', on_delete=models.CASCADE)

on_delete の部分は、アプリの特徴や機能等に応じて適当に変えてもらうので良いです。on_delete については下記ページで詳しく解説していますので、必要に応じて参照していただければと思います。

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

自分自身のモデルクラスは文字列で指定する

ただし、自分自身のモデルクラスを指定する際には、その自分自身のモデルクラスを文字列として指定する必要があります。この点は自己参照型リレーション実現時のポイントになると思います。

そのため、先ほど示したモデルクラスの定義において、ForeignKey の第1引数を モデルクラス名 ではなく 'モデルクラス名' とシングルクォーテーションマークで囲う形で記述しています。もちろんダブルクォーテーションマークで囲っても良いです。大事なことは文字列として指定することになります。

もしくは、'モデルクラス名' の部分を 'self' に置き換えるのでも OK です。ただし、この場合も文字列で指定する必要がある点に注意してください。

自分自身をselfで指定する例
class モデル名(models.Model):
    フィールド名 = models.ForeignKey('self', on_delete=models.CASCADE)

とにかく自分自身のモデルクラスを文字列としてフィールドの第1引数として指定してやれば、自己参照型リレーションは実現することが可能です。ただし、下記のようにフィールドの第1引数への指定が文字列ではなく、モデルクラスそのものを指定した場合、マイグレーション実行時等に例外が発生するので気をつけてください。

自己参照型リレーションの設定(NG)
class Student(models.Model):
    loving = models.ForeignKey(Student, on_delete=models.CASCADE)

実際に発生する例外は下記のようなものになります。

name 'モデル名' is not defined

おそらく Python インタプリタはモデルクラスの中で定義しているフィールドを全て認識した後にモデルクラス自体を認識するようになっているため、自身の定義の中で自分自身を参照すると、まだ認識していないモデルクラスが使用されていると考えて例外になるのだと思います。

自己参照型リレーションの特徴

実現方法については理解していただけたと思いますので、次は自己参照型リレーションの特徴について説明していきます。

スポンサーリンク

親モデルと子モデルが同一のモデルクラスとなる

まず、自己参照型リレーションにおける特徴の1つ目として、親モデルと子モデルが同一のモデルクラスとなる点が挙げられます。

これも下記ページで解説していますが、自己参照型でないリレーションを設定した2つのモデルクラスの間には親モデルと子モデルという関係があります。

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

具体的には、フィールドの第1引数に指定したモデルクラスが親モデルとなり、そのフィールドを定義したモデルクラスが子モデルとなります。

フィールドへの引数で親モデルを指定する様子

例えば、ForeignKey を利用した場合は1対多のリレーションが設定されることになるのですが、親モデル側が1対多における1の関係となり、子モデル側が多の関係となります。つまり、1つの親モデルのインスタンスに対し、複数の子モデルのインスタンスが関係性を持つことになります。

1対多のリレーションにおいて親モデル側が1の関係であり、子モデル側が多の関係であることを示す図

ですが、自己参照型リレーションの場合、自分自身へのリレーションが設定されることになるため、自己参照型リレーションを持つモデルクラスは親モデルでもあり子モデルでもあることになります。

ただし、自己参照型リレーションの場合でも、各インスタンスが親側のものであるか、子側のものであるかをしっかり意識して開発を行う必要があります。この辺りは後述で解説していきます。

データ属性が2つ追加される

自己参照型リレーションの特徴の2つ目として、このリレーションを持つモデルクラスのインスタンスには2つのデータ属性が追加される点が挙げられます。

これに関しても下記ページで解説していますが、2つのモデルクラスの間にリレーションを設定することにより、親モデル側のインスタンスと子モデル側のインスタンスにそれぞれ1つずつデータ属性が追加されることになります。

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

そして、追加されたデータ属性から、インスタンスに対するリレーションの構築相手にアクセスすることができ、リレーション構築相手を取得したり、インスタンスと他のインスタンスの間にリレーションを構築するようなことができます。

MEMO

このサイトでは、モデルクラスでリレーション設定用のフィールドを持たせてモデルクラス間に関係性を持たせることを「リレーションの設定」、インスタンス同士に関係性を持たせることを「リレーションの構築」と呼んでいます

分かりやすいのが子モデル側で、子モデル側ではモデルクラスにリレーションを設定するために追加したフィールドの クラス変数名 のデータ属性が追加されることになります。

それに対し、親モデル側に追加されるデータ属性は少しややこしく、これは利用するフィールド(OneToOneFieldForeignKeyManyToManyField)によって異なるのですが、今回例に挙げている ForeignKey の場合は、親モデル側には 子モデル(小文字)のクラス名_set or related_name 引数で指定した名前のデータ属性が追加されることになります。

ここでのポイントは、2つのモデルクラスの間にリレーションを設定した場合、子モデル側と親モデル側に追加されるデータ属性は1つずつである点になります。

それに対し、自己参照型リレーションにおいては、親モデルと子モデルが同じモデルクラスとなります。したがって、親モデルと子モデルの区別が付きません。自己参照型リレーションを設定した場合、そのモデルクラスは親モデルであり子モデルでもあることになります。

自己参照型リレーションによって1つのモデルクラスが親と子の両方の役割を果たす様子を示す図

そのため、自己参照型リレーションが設定されたモデルクラスのインスタンスには、リレーションの設定によって前述で紹介した親モデル側に追加されるデータ属性と子モデル側に追加されるデータ属性の両方が追加されることになります。

つまり、ForeignKey で自己参照型リレーションを設定した場合、まずインスタンスには ForeignKey のフィールドの クラス変数名 のデータ属性が追加されます。さらに、子モデル(小文字)のクラス名_set or related_name 引数で指定した名前のデータ属性が追加されることになります。

MEMO

ManyToManyField を利用する場合、自己参照型リレーションを利用する場合でも、引数の指定によってモデルクラスに追加されるデータ属性が1つのみとなることがあります

これに関しては、後述の ManyToManyField における自己参照型リレーション で詳細を説明します

使用するデータ属性によって立場が変わる

このように、自己参照型のリレーションでは通常のリレーションに比べて親と子の区別が付きにくいという特徴があり、これが少しややこしいです…。ですが、自己参照型のリレーションにおいても、基本的にはインスタンス同士には親と子の関係があると考えられます。そして、自己参照型のリレーションを利用する場合には、特に親と子の関係を意識しながら実装すること重要となります。

例えば、リレーションの設定により追加されるデータ属性は2つとなりますが、どちらのデータ属性を利用するかによって、各インスタンスの立場、すなわち親であるか子であるかが変化することになります。

インスタンスを子として扱う

まず、2つのモデルクラスの間にリレーションを設定することで、子モデル側にはそのリレーションのフィールドの クラス変数名 のデータ属性が追加されることになりますが、このデータ属性は、あくまでも子から親にアクセスするためのデータ属性となります。したがって、このデータ属性から親の関係となるインスタンスとの間にリレーションを構築したり、リレーションを構築している親を取得したりすることができます。

そして、重要なのは、このデータ属性を利用するインスタンスは子として扱われるという点になります。

クラス変数名のデータ属性を利用することで、インスタンスが子の立場となることを示す図

インスタンスを親として扱う

同様に、子モデル(小文字)のクラス名_set or related_name 引数で指定した名前のデータ属性は親から子にアクセスするためのデータ属性となります。そして、このデータ属性を利用するインタスタンスは親として扱われることになります。

クラス名_setのデータ属性を利用することで、インスタンスが親の立場となることを示す図

つまり、自己参照型リレーションを持つモデルクラスのインスタンスは、親と子の両方の側面を持ちますが、使用するデータ属性によって親の立場であるか、子の立場であるかが明確に決まることになります。そして、このデータ属性の使い分けによって、親から子にアクセスするか、子から親にアクセスするかが変化することになります。

親と子の使い分けの具体例

具体例で考えていきましょう!

今回は例として、クラス内の生徒の「恋愛事情」を管理するウェブアプリを考えていきたいと思います。このウェブアプリでは Student モデルを定義し、この Student モデルの各インスタンスの間でリレーションを構築していくことでクラス内の各生徒の恋愛事情、つまり、誰が誰を好きなのかを管理していくものとしたいと思います。

例として扱うモデルの説明図

特定の生徒が他の生徒のことを好きになることを管理するわけですから、StudentStudent の間にリレーションを設定することになります。つまり、Student に自己参照型のリレーションを設定します。

まずは、制限として「各生徒が好きな人は一人のみ」としたいと思います。ですが、この場合でも、一人の生徒が複数の生徒から好かれる可能性もあります(上の図のように studentCstudentAstudentB から好かれている)。

つまり、生徒と生徒の間には一対多のリレーションが存在することになりますので、ForeignKey を利用してリレーションを設定します(各生徒の好きな人が複数である可能性がある場合は、ForeignKey の代わりに ManyToManyField を利用することになります。この例は後述で紹介します)。

また、1対多の関係においては、親側が1の関係となり、子側が多の関係となります。すなわち、子側が好きになる生徒は最大一人のみということになります(子が関連性を持つ親は1つのみ)。また、親側が好かれる生徒の数は複数であるという可能性があります(親が関連性を持つ子は複数)。

生徒間の恋愛感情の間の関係に親子関係が存在することを示す図

上記のポイントを踏まえ、次のように各生徒の関連性を管理するモデルクラス Studentmodels.py に定義したいと思います。

Student
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=32)
    loving = models.ForeignKey('self', null=True, related_name='loved_by', on_delete=models.SET_NULL)

この Student は1対多の自己参照型リレーションを持つモデルクラスとなります。自己参照型リレーションは、ForeignKey の第1引数に 'self' を指定することで実現しています。

MEMO

上記の ForeignKey では null=True を指定することで、loving フィールドが空(None)でもデータベースに保存可能となるようにしています

また、on_delete=models.SET_NULL とすることで、親(好きな相手)が削除されたような場合には自動的に loving フィールドが空に設定されるようにしています

つまり、各生徒は好きな人が無しでも良いし、好きな相手が転校等でいなくなった際には自動的に好きな人が無しに設定されるようになっています

そして、リレーションの設定により、Student のインスタンスには、まず loving データ属性が追加されることになります。この loving は、子から親にアクセスするためのデータ属性となり、これを利用するインスタンスは子として扱われることになります。そして、この loving データ属性を利用して、子から親へのリレーションの構築を行うことができます。

例えば下記関数は、子から親へのリレーションの構築を行う処理の一例となります。views.py に実装することを想定しており、一応 HttpResponse() を返却しているので、ビューの関数としては成立していますがレスポンスのボディは空文字列となります。

子から親へのリレーションの構築1
from .models import Student
from django.http import HttpResponse

def loving_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')

    studentA.save()

    studentB.loving = studentA
    studentB.save()

    return HttpResponse('')

上記関数では、2つの Student のインスタンスを生成し、一方のインスタンスから他方のインスタンスにリレーションを構築するようにしています。

上記に関しては、studentB.loving = studentA で2つのインスタンス同士にリレーションを構築しているのですが、loving は子から親にアクセスするためのデータ属性となりますので、このデータ属性を利用している studentB が子、studentA が親として扱われることになります。つまり、studentB.loving = studentA により、studentBstudentA を好きであるという関連性が生まれたことになります。

studentBからstudentAに対してリレーションを構築する様子

では下記の場合はどうでしょう?ちなみに、リレーションの構築を行うために studentAstudentBsave メソッドを実行させているのは、各インスタンスのプライマリーキーを確定させるためです。

子から親へのリレーションの構築2
from .models import Student
from django.http import HttpResponse

def loving_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')

    studentA.save()
    studentB.save()

    studentB.loving = studentA
    studentA.loving = studentB

    studentB.save()
    studentA.save()

    return HttpResponse('')

この場合、先ほどと同様に studentB.loving = studentA においては、子である studentB から親である studentA に対してリレーションを構築していることになります。それに対し、studentA.loving = studentB においては、studentA が子となり、子から親である studentB に対してリレーションを構築していることになります。先ほどと立場が逆転していますね!

studentAからstudentBに対してリレーションを構築する様子

このように、自己参照型リレーションを持つモデルクラスの場合、「子から親にアクセスするためのデータ属性」を利用するインスタンスが子として扱われることになります。したがって、各インスタンスは子にもなり得ますし、親にもなり得ます。

また、前述の通り、「子から親にアクセスするためのデータ属性」はリレーション設定時に設けるフィールドの名称と同じデータ属性となります。上記の例の場合は loving になります。

同様に、「親から子にアクセスするためのデータ属性」を利用した場合、その利用したインスタンスが親として扱われることになります。

前述の通り、「親から子にアクセスするためのデータ属性」は 子モデル(小文字)のクラス名_set or related_name 引数で指定した名前のデータ属性となります。上記の例の場合は related_name='loved_by' を指定しているため、このデータ属性は loved_by になります。この「親から子にアクセスするためのデータ属性」を利用しても、リレーションの構築等を行うことが可能です。

例えば下記のような処理を行なった場合、studentB から studentA に対してリレーションが構築されることになります。

親から子へのリレーション構築例
from .models import Student
from django.http import HttpResponse

def loved_by_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')

    studentA.save()
    studentB.save()

    studentB.loved_by.add(studentA) 

    return HttpResponse('')

上記の処理では studentBrelated_name 引数で指定した loved_by という名前のデータ属性を利用しているため、studentB は親として扱われることになります。

そして、親は1対多のリレーションにおいて1の関係にあります。この場合、リレーション構築相手が多の関係にあることになりますので、リレーション構築相手となるインスタンスは1つとは限りません。そのため、複数のインスタンスとリレーションが構築できるよう、リレーション構築相手は集合として扱い、その集合にインスタンスを加えるという意味合いで add メソッドを実行させることでリレーションの構築が行われることになります。

また、今回のリレーションの例においては、好きになる側を子、好きになられる側を親として考えているため、上記の studentB.loved_by.add(studentA) では studentBstudentA に好きになられたことを示すためのリレーションが構築されることになります。

studentBからstudentAに対して逆参照でリレーションを構築する様子

ただ、これは逆の視点で考えれば studentAstudentB を好きになったことになりますので、studentB.loving = studentA でも同じ意味合いの関連性が生まれることになります。つまり、上記の処理は studentB.loving = studentA で代替することも可能です。

このように、これは2つのモデルクラス間でリレーションを設定している場合も同様ですが、自己参照型のリレーションにおいても親からも子からもリレーションの構築は可能となります。

MEMO

ただし、add メソッドが実行された際にはデータベースへのレコードの保存も行われるのに対し、単に = でインスタンスを参照しただけではデータベースへのレコードが行われないという違いはあります

これも自己参照型リレーションに限った話ではなく、2つのモデルクラス間でリレーションを設定している場合も同様になります

スポンサーリンク

多段階的な関連性が簡単に実現できる

自己参照型リレーションの面白いところは、やはりインスタンスが親にも子にもなれるという点になると思います。2つのモデルクラス間でリレーションを設定した場合、必ず一方のモデルクラスが親モデルとなり、他方のモデルクラスが子モデルとなります。これは固定です。

親モデルからも小モデルからもリレーションを構築可能であるという点は自己参照型リレーションに限った話ではないですが、1つのインスタンスが親にも子にもなり得るのは自己参照型リレーションならではの特徴になります。

そして、この親にも子にもなれるという特徴を持つため、自己参照型リレーションでは多段階的なリレーションの構築が簡単に実現することができます。

例えば、下記のような処理について考えてみましょう!

子から親へのリレーションの構築2
from .models import Student
from django.http import HttpResponse

def loving_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')
    studentC = Student(name='YamadaJiro')
    studentD = Student(name='YamadaMichiko')

    studentA.save()
    studentB.save()
    studentC.save()
    studentD.save()

    studentA.loving = studentB
    studentB.loving = studentC
    studentC.loving = studentD

    studentA.save()
    studentB.save()
    studentC.save()

    return HttpResponse('')

上記の処理のポイントは、studentBstudentC が親と子の両方の立場になっているという点になります。

studentA.loving = studentB においては、studentB が親として扱われることになりますが、studentB.loving = studentC においては studentB が子として扱われることになります。studentC も同様に親としても子としても扱われています。

親としても子としても扱うことが可能であるため、これらのインスタンスは親も子も両方持つことが可能となります。そしてこれにより、各インスタンスは親と子だけでなく、孫や祖父母のような多段階的な関係性を持たせることが可能になります。

各インスタンスの間で多段階的な関係性を構築する様子

このように、1つのリレーションによって多段階的なリレーションが構築可能なのは、各インスタンスが親にも子にもなれるからです。もし、子の立場のインスタンスが親になれないのであれば、子を持つことはできないことになります。

親が子になれない場合に親が親を持てないことを示す関係図

自己参照型リレーション以外のリレーションの場合、どちらかのインスタンス(モデル)が親 or 子で固定となるため、1つのリレーションによって多段階的なリレーションの構築はできません。これは、親は子になれないため、親に対する親のインスタンスが設定できないからになります。

それに対し、自己参照型リレーションの場合、各インスタンスは使用するデータ属性に応じて親にも子にもなれるため、特定の子に対する親は、他の親に対する子になれます。そのため、前述で紹介したように多段階的な関係を構築するのに便利です。

親と子の立場を変更することで、もともと親だったインスタンスが親を持てるようになることを示す関係図

今回は恋愛事情管理アプリという謎なアプリで実例を示していますが、このような多段階的なリレーションの構築が必要なケースはそれなりに存在するのではないかと思っています。

例えば、掲示板に投稿されるコメントと、そのコメントへの返信について考えると、特定のコメントに対する返信コメントはコメントの一種であり、返信先のコメントの子であると考えられます。さらに、その返信コメントは他のコメントによって返信される可能性もあります。つまり、前述の返信コメントは、他の返信コメントに対する親でもあることになります。したがって、各コメントは下の図のように多段階的なリレーションによって管理されることになると考えられます。

コメントへの返信機能が多段階的なリレーションの構築によって実現されることを示す図

このような関係性に関しても、自己参照型リレーションを導入すれば簡単に実現できます。実際に、下記のようなコメント管理モデルを定義してやれば、返信コメントに対して返信を行うことが可能なモデルとして扱うことができます。

Comment
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=1024)
    reply = models.ForeignKey('self', null=True, related_name='replied_by', on_delete=models.CASCADE)

実は、自己参照型リレーションを利用せず、下記のように2つのモデルクラスを定義し、これらの間でリレーションを設定することでも返信コメント自体の管理は可能となります。

CommentとReply
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=1024)

class Reply(models.Model):
    text = models.CharField(max_length=1024)
    comment = models.ForeignKey(Comment, related_name='replied_by', on_delete=models.CASCADE)

ですが、この場合、Reply は子モデルに固定され、Reply のインスタンスは親にはなれません。そのため、返信コメントに対する返信が管理できなくなります。もちろん、コメントへの返信は1段階のみで、返信コメントへの返信はできないような掲示板アプリを実現したい場合はこのようなモデル定義でも良いのですが、返信コメントへの返信も管理できるようにしたいのであれば、自己参照リレーションを利用する方が良いでしょう。

ManyToManyField における自己参照型リレーション

さて、ここまで ForeignKey を用いた自己参照リレーションについて解説してきました。

次は、ManyToManyField を用いた際の自己参照リレーションについて解説していきます。基本的には ManyToManyField の場合も ForeignKey を用いた場合と同じ感覚で自己参照型リレーションを利用することも可能なのですが、注意が必要になるのが引数 symmetrical になります。

symmetrical=False を指定した場合は、 ForeignKey を用いた場合と同じ感覚で自己参照リレーションを利用することができます。が、symmetrical=True を指定した場合 or symmetrical を引数指定しなかった場合は、ForeignKey を用いた場合とは異なる意味合いのリレーションとなります。ManyToManyField の場合は、この点について注意をしながらモデルを設計する必要があります。

ということで、ここでは ManyToManyField による自己参照型リレーションについて、特に symmetrical 引数に注目しながら解説をしていきたいと思います。

symmetrical とは

では、まずは、この引数 symmetrical について解説します。

この symmetricalManyToManyField のコンストラクタに指定可能な引数になります。

この symmetrical は、自己参照型リレーションを構築するインスタンス間の関係性が対称であることを強制するかどうかを指定するための引数になります。symmetrical 引数を指定しなかった場合、デフォルト設定として symmetrical=True が指定されることになります。

自己参照型リレーションを持つモデルクラスにおいて、「対称である」とは、2つのインスタンス間において互いのインスタンスの両方から他方のインスタンスに対してリレーションが構築されていることを意味します。

自己参照型リレーションにおける対称の説明図

それに対し、一方のインスタンスからのみリレーションが構築されている場合は対称ではありません。この場合は非対称となります。

自己参照型リレーションにおける非対称の説明図

そして、symmetrical=True が指定された場合は、この「対象である」ことが強制されるため、2つのインスタンスの一方のインスタンスから他方のインスタンスに対してリレーションを構築すれば、そのインスタンスに対して自動的に他方のインスタンスからもリレーションが構築されることになります。

スポンサーリンク

symmetrical=False での自己参照型リレーション

この symmetrical の意味合いに関しては、前述で示した恋愛事情の管理モデル Student で考えると分かりやすいと思います。

前述で示した Student では、生徒が好きになれる人数は一人のみでした。それに対し、今回は生徒が好きになれる人数は複数である可能性があるモデルを考えたいと思います。

多対多のリレーションにおける自己参照の例

この場合は、多対多のリレーションとなるため、下記のように Student を定義することになります。まずは、symmetrical=False を指定した場合の動作について解説していきます。

symmetrical=Falseの例
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=32)
    lovings = models.ManyToManyField('self', symmetrical=False, related_name='loved_by')

    def __str__(self):
        return self.name

この Student モデルにおいて、上記のように symmetrical=False を指定した場合、この Sutdent で管理されるリレーションでは生徒間での恋愛関係で「対称である」ことは強制されません。これは、このモデルの場合、片想いも管理可能であることを意味します。

もう1段階掘り下げて考えていきましょう。上記のように symmetrical=False を指定した場合、次の処理はどのような意味合いになるでしょうか?

symmetrical=Falseの場合の動作
def loving_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')

    studentA.save()
    studentB.save()

    studentA.lovings.add(studentB)

    print(studentA.lovings.all())
    print(studentB.lovings.all())

    return HttpResponse('')

上記の意味合いを処理的な観点のみで考えれば、studentA.lovings.add(studentB) により、studentA から studentB に対してリレーションが構築されることになります。

MEMO

ManyToManyField でリレーションを設定した場合、子から親へのリレーションを構築する際にも add メソッドを利用する必要があります

studentAlovings という集合に studentB が加えられるわけですから、上記の処理は studentAstudentB を好きであることを示しているということになります。ですが、studentB.lovings.add(StudentA) は実行されていないため、studentBsutndentA のことを好きではありません。

つまり、studentA.lovings.add(studentB) を実行しただけだと、studentA から studentB の方向に対してのみリレーションが構築されるだけで、逆方向のリレーションは構築されません。この例の場合は、studentA から studentB の片想い状態であることになります。この状態はまさに非対称となります。

また、上記では print 関数で各インスタンスが好きな生徒のインスタンスの集合を出力していることになりますが、実際の出力結果は次のようなものになります。ポイントは、studentB.lovings.all() の出力結果が空である点です。まぁ studentB からは他のインスタンスに対してリレーションを構築していないので当たり前と言えば当たり前ですかね?

<QuerySet [<Student: YamadaHanako>]>
<QuerySet []>

このように、symmetrical=False の場合、片方向からのみのリレーションを構築しているような状態が存在し得ます。

インスタンスの関係が非対称であることを示す図

そして、symmetrical=False の場合、両方向からのリレーションの構築を行いたいのであれば、別途そのための処理を行う必要があります。

上記の例であれば、studentB から studentA の方向に対してリレーションを構築するために別途 studentB.lovings.add(studentA)studentA.loved_by.add(studentB) を実行する必要があります。

別途リレーションを構築するための処理を行うことで関係性が対象となる様子

つまり、symmetrical=False の場合、対称の関係性にするためには、別途そのための処理を行う必要があります。

symmetrical=True での自己参照型リレーション

これに対し、symmetrical=True を指定した場合、2つのインスタンスのリレーションは必ず対称となります。すなわち、片方向からのリレーションのみのリレーションが構築されている状態は存在しません。これは、片方向からのリレーションが構築される際に、逆方向からのリレーションも同時に構築されるようになるからです。

symmetrical=Trueの場合に一方向からリレーションを構築すれば逆方向からのリレーションも自動的に構築される様子を示す図

例えば、先程紹介した Student を下記のように変更してみましょう。先ほどからの変更点は symmetrical=False の引数指定を削除した点のみとなります。symmetrical 引数を指定しなかった場合、デフォルト設定である symmetrical=True が適用されることになります。

symmetrical=Trueの例
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=32)
    lovings = models.ManyToManyField('self', related_name='loved_by')

    def __str__(self):
        return self.name

そして、このモデルクラスを利用した下記の処理について考えてみましょう!下記の print 関数での出力結果はどのようなものになるでしょうか?

symmetrical=Trueの場合の動作
def loving_student(request):
    studentA = Student(name='YamadaTaro')
    studentB = Student(name='YamadaHanako')

    studentA.save()
    studentB.save()

    studentA.lovings.add(studentB)

    print(studentA.lovings.all())
    print(studentB.lovings.all())

    return HttpResponse('')

ここまでの説明を読んでくださった方であれば容易に結果は予想できたかもしれません。実際の出力結果は下記のようなものになります。

<QuerySet [<Student: YamadaHanako>]>
<QuerySet [<Student: YamadaTaro>]>

再掲となりますが、symmetrical=False を指定していた場合の結果は下記となっていました。

<QuerySet [<Student: YamadaHanako>]>
<QuerySet []>

この結果の違いが symmetrical 引数の意味合いを端的に示していると思います。symmetrical=True の場合、片方向からリレーションを構築すれば、逆方向からもリレーションが構築されることになります。

従って、上記のように studentA.lovings.add(studentB) を実行すれば、studentA から studentB へのリレーションが構築されると同時に、studentB から studentA へのリレーションが自動的に構築されることになります。これにより、非対称の状態が存在しないようになっています。常に、2つのインスタンスの関係は対称となります。

この例で考えれば、studentA が studentB を好きになれば、必ず studentB も studentA も好きになってくれることになります。なんて素晴らしい世界なんでしょう…。

まぁただ、現実的にはそうは上手くいかず、studentA が studentB を好きになっても studentB が studentA を好きになってくれるとは限りません。2つのインスタンスの関係は非対称である可能性があります。そんな、非対称な関係性は symmetrical=True を指定すると実現できません。そのため、このような場合は symmetrical=False を指定する必要があります。

symmetrical~Trueの場合に実現不可な関係性の一例を示す図

また、symmetrical=True の場合は2つのインスタンスの間の関係が対称であることが強制されるため、一方からのリレーションが解消された場合は他方からのリレーションも解消されることになります。

symmetrical=True の場合に追加されるデータ属性

このように、symmetrical 引数の指定によって、自己参照型リレーションで管理できる関係性の意味合いが大きく変わることになります。

さらに、もう1つ重要な点があって、これは symmetrical=True が指定されている or symmetrical 引数が指定されていない場合はリレーションの設定によって追加されるデータ属性が1つのみという点になります。追加されるデータ属性は「子から親にアクセスするためのデータ属性」のみとなります。具体的には、リレーション設定用のフィールドのクラス変数名のデータ属性が追加されることになります。

また、related_name は「親から子にアクセするためのデータ属性」の名前を指定するための引数になりますが、symmetrical=False を指定しない限りはこのデータ属性が追加されないため、related_name 引数を指定しても単に無視されることなります。

下記は前述でも示した symmetrical=True の場合(symmetrical を引数指定しない場合)のモデルの定義例となります。

symmetrical=Trueの例
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=32)
    lovings = models.ManyToManyField('self', related_name='loved_by')

    def __str__(self):
        return self.name

このような 自己参照を行う ManyToManyField に対し、symmetrical=False を指定しない状態で 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=True の場合、利用できるデータ属性が減るのでなんだか不便な気もしますが、別にそんなこともありません。

symmetrical=True の場合、自己参照型リレーションにおける2つのインスタンスの関係性は必ず対象となります。したがって、片方向からリレーションを構築すれば、逆方向に対しても自動的にリレーションが構築されることになります。つまり子から親に対してリレーションを構築さえしてやれば、親から子に対してもリレーションが自動的に構築されることになるため、わざわざ逆参照を利用して親から子にリレーションを構築する必要がないだけです。

例えば、上記の Student の場合は、lovings のデータ属性を利用してリレーションを構築すれば良いことになります。

スポンサーリンク

symmetrical の選び方

ここまで symmetrical 引数の指定によるアプリの動作の違いについて解説してきました。

ManyToManyField を利用して自己参照型リレーションを実現する場合、この symmetrical 引数への指定の違いによって実現可能な機能に大きな差が出ることになります。

では、この symmetrical 引数には、具体的には True or False のどちらを指定するのが良いでしょうか?

結論としては、どちらを指定するのかは実現したい機能によって異なります。またモデルクラスの意味合いによっても異なります。

例えば、前述の Student の場合であれば、一方の生徒が好きになったからといって、その生徒が相手からも好きになってもらえるとは限りません。そのため、現実世界に合わせたモデルを定義したいのであれば symmetrical=False を指定する方が自然だと思います。ですが、例えば Student を利用して生徒の相思相愛の関係性だけを管理したいのであれば、symmetrical=True を指定した方が実装や各インスタンスの管理は楽になるでしょう。

また、他の例として、私がよく遊んでいるパワサカというアプリには、他のユーザーをフォローする「フォロー機能」と他のユーザーとフレンドになる「フレンド機能」が存在しています。

「フォロー機能」は Twitter などと同じで、特定のユーザーが他のユーザーに対して一方向的に関係性を持たせるものになります。私が他のユーザーに対してフォローを行ったとしても、相手が私をフォローしてくれるとは限りません。つまり、このフォロー機能は2つのインスタンスの間で非対称の関係性を構築するための機能と考えられます。

フォロー機能の説明図

それに対して「フレンド機能」の場合は、相手にフレンド申請を出し、承認された場合のみ、お互いにフレンドになれる機能となります。承認さえされれば、相手から見ても自分から見てもフレンドの関係となります。つまり、このフレンド機能は2つのインスタンスの間で対象の関係性を構築するための機能と考えられます。

フレンド機能の説明図

また、フレンド関係を解消するようなことも可能で、一方からフレンドが解消されれば、他方からのフレンドも解消されるようになっています。この辺りも2つのインスタンスの関係が対象であることを強制するために自動的に処理されるようになっています。

一方からリレーションが解消された場合の説明図

したがって、上記のようなフォロー機能を実現したい場合は 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 を適切に選択する必要があります。symmetrical=Falsesymmetrical=True の一番の違いは、2つのインスタンス間の関係性において非対称であることを許可するか否かの点にあると思います。この点に注目し、どちらの引数を指定するかを決めると良いと思います。

まとめ

このページでは、Django における自己参照型リレーションについて解説を行いました。

自己参照型リレーションは、自分自身のモデルクラスに対してリレーションを設定することで実現することができます。具体的には、リレーションの設定を行うフィールドの第1引数に「自分自身のモデルクラス名」もしくは「self」を指定してやれば良いです。ただし、どちらの場合でも文字列で指定する必要がある点に注意してください。

この自己参照型リレーションを利用することで、下記のような機能を簡単に実現することができます。

  • コメント返信
  • フォロー
  • フレンド

特に、自己参照型リレーションの場合、各インスタンスが親にも子にもなれるため、多段階的な関係が簡単に実現することができます。実は、結構利用したくなる場面も多い仕組みとなりますので、是非このページで解説した内容に関しては頭に入れておいていただければと思います!

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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です