このページでは Django における自己参照型リレーションについて解説していきます。
自己参照型リレーションはリレーションの一種になります。このサイトではリレーションについて下記ページで解説を行なっています。
【Django入門7】リレーションの基本上記ページでも解説しているように、リレーションの仕組みを利用することで2つのモデルクラスの間に関連性を持たせることができます。
例えば上記ページの 掲示板アプリでリレーションを利用してみる では、掲示板アプリで利用しているユーザーモデルクラスとコメントモデルクラスとの間にリレーションを設定することで、ユーザーとコメントとを関連付け、ユーザーを投稿者として扱う例を示しています。
このページで紹介する自己参照型リレーションは、2つのモデルクラスではなく、特定のモデルクラスから、そのモデルクラス自身に対してリレーションを設定する特殊なものとなります。メリットがすぐに思い付かないかもしれませんが、実は自己参照型リレーションを利用するメリットは多く、これを利用することで簡単に実現可能となる機能も多いです。例えばコメントの返信やフォロー機能なども簡単に実現できます。
自分自身を参照するという点にややこしさを感じるかもしれませんが、図やコードで補足しながらできるだけ分かりやすく解説していきますので、是非このページで自己参照型リレーションについて学んでいっていただければと思います。
自己参照型リレーション
前述の通り、一般的なリレーションは「2つのモデルクラスの間に関連性を持たせる仕組み」になります。
それに対し、自己参照型リレーションは「自分自身のモデルクラスに対して関連性を持たせる仕組み」になります。自分自身のモデルクラスの “インスタンス” に対して関連性を持たせる仕組みであると考えた方が分かりやすいかもしれないです。
下記ページでも解説していますが、モデルクラスはデータベースのテーブルであり、モデルクラスのインスタンスはそのテーブルのレコードに該当します。
【Django入門6】モデルの基本つまり、自己参照型リレーションとは、同じテーブル内のレコード間で関連性を持たせることであると考えることができます。
このページでは、このような関係性を自己参照型リレーションと呼びますが、他にも自己結合などと呼ぶこともあるようです。
自己参照型リレーションの実現方法
続いて、自己参照型リレーションの実現方法について解説していきます。
スポンサーリンク
自分自身に対してリレーションを設定する
リレーション自体の実現は、モデルクラスに対してリレーション設定用のフィールドを持たせることで実現する事ができます。
Django においては、リレーション設定用のフィールドとして下記の3つが用意されており、これらをモデルクラスに持たせる、つまり、これらのインスタンスを右辺とするクラス変数を定義してやることで、2つのモデルクラスに対してリレーションが設定されることになります。
OneToOneField
:1対1のリレーションForeignKey
:1対多のリレーションManyToManyField
:多対多のリレーション
上記の3つのフィールドの違いや詳細に関しては下記ページでまとめていますので、詳しくは下記ページを参照していただければと思います。このページでは、まずは ForeignKey
を利用する例を用いて解説を行なっていきます。その後、ManyToManyField における自己参照型リレーション で ManyToManyField
で自己参照型リレーションを利用する場合の特記事項・注意事項について解説していきます。
さて、リレーションの設定方法についての解説に話を戻すと、2つのモデルクラスの間でリレーションを設定するためには、上記のフィールドの第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)
このように、リレーション設定用のフィールドを持つモデルクラス(Student
)とは異なるモデルクラス(Club
)を第1引数で指定した場合、2つのモデルクラスの間にリレーションが設定されることになります。
それに対し、第1引数に自分自身のモデルクラスを指定した場合、自分自身のモデルクラスに対してリレーションが設定されることになります。これこそが自己参照型リレーションであり、つまり、自己参照型リレーションを実現するためには、上記のフィールドの第1引数に自分自身のモデルクラスを指定すれば良いことになります。
したがって、下記のように定義を行えば自己参照型リレーションを実現することができることになります。
class モデルクラス名(models.Model):
フィールド名 = models.ForeignKey('モデルクラス名', on_delete=models.CASCADE)
on_delete
の部分は、アプリの特徴や機能等に応じて適当に変えてもらうので良いです。on_delete
については下記ページで詳しく解説していますので、必要に応じて参照していただければと思います。
自分自身のモデルクラスは文字列で指定する
ただし、自分自身のモデルクラスを指定する際には、その自分自身のモデルクラスを文字列として指定する必要があります。この点は自己参照型リレーション実現時のポイントになると思います。
そのため、先ほど示したモデルクラスの定義において、ForeignKey
の第1引数を モデルクラス名
ではなく 'モデルクラス名'
とシングルクォーテーションマークで囲う形で記述しています。もちろんダブルクォーテーションマークで囲っても良いです。大事なことは文字列として指定することになります。
もしくは、'モデルクラス名'
の部分を 'self'
に置き換えるのでも OK です。ただし、この場合も文字列で指定する必要がある点に注意してください。
class モデル名(models.Model):
フィールド名 = models.ForeignKey('self', on_delete=models.CASCADE)
とにかく自分自身のモデルクラスを文字列としてフィールドの第1引数として指定してやれば、自己参照型リレーションは実現することが可能です。ただし、下記のようにフィールドの第1引数への指定が文字列ではなく、モデルクラスそのものを指定した場合、マイグレーション実行時等に例外が発生するので気をつけてください。
class Student(models.Model):
loving = models.ForeignKey(Student, on_delete=models.CASCADE)
実際に発生する例外は下記のようなものになります。
name 'モデル名' is not defined
おそらく Python インタプリタはモデルクラスの中で定義しているフィールドを全て認識した後にモデルクラス自体を認識するようになっているため、自身の定義の中で自分自身を参照すると、まだ認識していないモデルクラスが使用されていると考えて例外になるのだと思います。
自己参照型リレーションの特徴
実現方法については理解していただけたと思いますので、次は自己参照型リレーションの特徴について説明していきます。
スポンサーリンク
親モデルと子モデルが同一のモデルクラスとなる
まず、自己参照型リレーションにおける特徴の1つ目として、親モデルと子モデルが同一のモデルクラスとなる点が挙げられます。
これも下記ページで解説していますが、自己参照型でないリレーションを設定した2つのモデルクラスの間には親モデルと子モデルという関係があります。
【Django入門7】リレーションの基本具体的には、フィールドの第1引数に指定したモデルクラスが親モデルとなり、そのフィールドを定義したモデルクラスが子モデルとなります。
例えば、ForeignKey
を利用した場合は1対多のリレーションが設定されることになるのですが、親モデル側が1対多における1の関係となり、子モデル側が多の関係となります。つまり、1つの親モデルのインスタンスに対し、複数の子モデルのインスタンスが関係性を持つことになります。
ですが、自己参照型リレーションの場合、自分自身へのリレーションが設定されることになるため、自己参照型リレーションを持つモデルクラスは親モデルでもあり子モデルでもあることになります。
ただし、自己参照型リレーションの場合でも、各インスタンスが親側のものであるか、子側のものであるかをしっかり意識して開発を行う必要があります。この辺りは後述で解説していきます。
データ属性が2つ追加される
自己参照型リレーションの特徴の2つ目として、このリレーションを持つモデルクラスのインスタンスには2つのデータ属性が追加される点が挙げられます。
これに関しても下記ページで解説していますが、2つのモデルクラスの間にリレーションを設定することにより、親モデル側のインスタンスと子モデル側のインスタンスにそれぞれ1つずつデータ属性が追加されることになります。
【Django入門7】リレーションの基本そして、追加されたデータ属性から、インスタンスに対するリレーションの構築相手にアクセスすることができ、リレーション構築相手を取得したり、インスタンスと他のインスタンスの間にリレーションを構築するようなことができます。
このサイトでは、モデルクラスでリレーション設定用のフィールドを持たせてモデルクラス間に関係性を持たせることを「リレーションの設定」、インスタンス同士に関係性を持たせることを「リレーションの構築」と呼んでいます
分かりやすいのが子モデル側で、子モデル側ではモデルクラスにリレーションを設定するために追加したフィールドの クラス変数名
のデータ属性が追加されることになります。
それに対し、親モデル側に追加されるデータ属性は少しややこしく、これは利用するフィールド(OneToOneField
・ForeignKey
・ManyToManyField
)によって異なるのですが、今回例に挙げている ForeignKey
の場合は、親モデル側には 子モデル(小文字)のクラス名_set
or related_name
引数で指定した名前のデータ属性が追加されることになります。
ここでのポイントは、2つのモデルクラスの間にリレーションを設定した場合、子モデル側と親モデル側に追加されるデータ属性は1つずつである点になります。
それに対し、自己参照型リレーションにおいては、親モデルと子モデルが同じモデルクラスとなります。したがって、親モデルと子モデルの区別が付きません。自己参照型リレーションを設定した場合、そのモデルクラスは親モデルであり子モデルでもあることになります。
そのため、自己参照型リレーションが設定されたモデルクラスのインスタンスには、リレーションの設定によって前述で紹介した親モデル側に追加されるデータ属性と子モデル側に追加されるデータ属性の両方が追加されることになります。
つまり、ForeignKey
で自己参照型リレーションを設定した場合、まずインスタンスには ForeignKey
のフィールドの クラス変数名
のデータ属性が追加されます。さらに、子モデル(小文字)のクラス名_set
or related_name
引数で指定した名前のデータ属性が追加されることになります。
ManyToManyField
を利用する場合、自己参照型リレーションを利用する場合でも、引数の指定によってモデルクラスに追加されるデータ属性が1つのみとなることがあります
これに関しては、後述の ManyToManyField における自己参照型リレーション で詳細を説明します
使用するデータ属性によって立場が変わる
このように、自己参照型のリレーションでは通常のリレーションに比べて親と子の区別が付きにくいという特徴があり、これが少しややこしいです…。ですが、自己参照型のリレーションにおいても、基本的にはインスタンス同士には親と子の関係があると考えられます。そして、自己参照型のリレーションを利用する場合には、特に親と子の関係を意識しながら実装すること重要となります。
例えば、リレーションの設定により追加されるデータ属性は2つとなりますが、どちらのデータ属性を利用するかによって、各インスタンスの立場、すなわち親であるか子であるかが変化することになります。
インスタンスを子として扱う
まず、2つのモデルクラスの間にリレーションを設定することで、子モデル側にはそのリレーションのフィールドの クラス変数名
のデータ属性が追加されることになりますが、このデータ属性は、あくまでも子から親にアクセスするためのデータ属性となります。したがって、このデータ属性から親の関係となるインスタンスとの間にリレーションを構築したり、リレーションを構築している親を取得したりすることができます。
そして、重要なのは、このデータ属性を利用するインスタンスは子として扱われるという点になります。
インスタンスを親として扱う
同様に、子モデル(小文字)のクラス名_set
or related_name
引数で指定した名前のデータ属性は親から子にアクセスするためのデータ属性となります。そして、このデータ属性を利用するインタスタンスは親として扱われることになります。
つまり、自己参照型リレーションを持つモデルクラスのインスタンスは、親と子の両方の側面を持ちますが、使用するデータ属性によって親の立場であるか、子の立場であるかが明確に決まることになります。そして、このデータ属性の使い分けによって、親から子にアクセスするか、子から親にアクセスするかが変化することになります。
親と子の使い分けの具体例
具体例で考えていきましょう!
今回は例として、クラス内の生徒の「恋愛事情」を管理するウェブアプリを考えていきたいと思います。このウェブアプリでは Student
モデルを定義し、この Student
モデルの各インスタンスの間でリレーションを構築していくことでクラス内の各生徒の恋愛事情、つまり、誰が誰を好きなのかを管理していくものとしたいと思います。
特定の生徒が他の生徒のことを好きになることを管理するわけですから、Student
と Student
の間にリレーションを設定することになります。つまり、Student
に自己参照型のリレーションを設定します。
まずは、制限として「各生徒が好きな人は一人のみ」としたいと思います。ですが、この場合でも、一人の生徒が複数の生徒から好かれる可能性もあります(上の図のように studentC
は studentA
と studentB
から好かれている)。
つまり、生徒と生徒の間には一対多のリレーションが存在することになりますので、ForeignKey
を利用してリレーションを設定します(各生徒の好きな人が複数である可能性がある場合は、ForeignKey
の代わりに ManyToManyField
を利用することになります。この例は後述で紹介します)。
また、1対多の関係においては、親側が1の関係となり、子側が多の関係となります。すなわち、子側が好きになる生徒は最大一人のみということになります(子が関連性を持つ親は1つのみ)。また、親側が好かれる生徒の数は複数であるという可能性があります(親が関連性を持つ子は複数)。
上記のポイントを踏まえ、次のように各生徒の関連性を管理するモデルクラス Student
を models.py
に定義したいと思います。
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'
を指定することで実現しています。
上記の ForeignKey
では null=True
を指定することで、loving
フィールドが空(None
)でもデータベースに保存可能となるようにしています
また、on_delete=models.SET_NULL
とすることで、親(好きな相手)が削除されたような場合には自動的に loving
フィールドが空に設定されるようにしています
つまり、各生徒は好きな人が無しでも良いし、好きな相手が転校等でいなくなった際には自動的に好きな人が無しに設定されるようになっています
そして、リレーションの設定により、Student
のインスタンスには、まず loving
データ属性が追加されることになります。この loving
は、子から親にアクセスするためのデータ属性となり、これを利用するインスタンスは子として扱われることになります。そして、この loving
データ属性を利用して、子から親へのリレーションの構築を行うことができます。
例えば下記関数は、子から親へのリレーションの構築を行う処理の一例となります。views.py
に実装することを想定しており、一応 HttpResponse()
を返却しているので、ビューの関数としては成立していますがレスポンスのボディは空文字列となります。
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
により、studentB
が studentA
を好きであるという関連性が生まれたことになります。
では下記の場合はどうでしょう?ちなみに、リレーションの構築を行うために studentA
と studentB
に save
メソッドを実行させているのは、各インスタンスのプライマリーキーを確定させるためです。
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
に対してリレーションを構築していることになります。先ほどと立場が逆転していますね!
このように、自己参照型リレーションを持つモデルクラスの場合、「子から親にアクセスするためのデータ属性」を利用するインスタンスが子として扱われることになります。したがって、各インスタンスは子にもなり得ますし、親にもなり得ます。
また、前述の通り、「子から親にアクセスするためのデータ属性」はリレーション設定時に設けるフィールドの名称と同じデータ属性となります。上記の例の場合は 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('')
上記の処理では studentB
が related_name
引数で指定した loved_by
という名前のデータ属性を利用しているため、studentB
は親として扱われることになります。
そして、親は1対多のリレーションにおいて1の関係にあります。この場合、リレーション構築相手が多の関係にあることになりますので、リレーション構築相手となるインスタンスは1つとは限りません。そのため、複数のインスタンスとリレーションが構築できるよう、リレーション構築相手は集合として扱い、その集合にインスタンスを加えるという意味合いで add
メソッドを実行させることでリレーションの構築が行われることになります。
また、今回のリレーションの例においては、好きになる側を子、好きになられる側を親として考えているため、上記の studentB.loved_by.add(studentA)
では studentB
が studentA
に好きになられたことを示すためのリレーションが構築されることになります。
ただ、これは逆の視点で考えれば studentA
が studentB
を好きになったことになりますので、studentB.loving = studentA
でも同じ意味合いの関連性が生まれることになります。つまり、上記の処理は studentB.loving = studentA
で代替することも可能です。
このように、これは2つのモデルクラス間でリレーションを設定している場合も同様ですが、自己参照型のリレーションにおいても親からも子からもリレーションの構築は可能となります。
ただし、add
メソッドが実行された際にはデータベースへのレコードの保存も行われるのに対し、単に =
でインスタンスを参照しただけではデータベースへのレコードが行われないという違いはあります
これも自己参照型リレーションに限った話ではなく、2つのモデルクラス間でリレーションを設定している場合も同様になります
スポンサーリンク
多段階的な関連性が簡単に実現できる
自己参照型リレーションの面白いところは、やはりインスタンスが親にも子にもなれるという点になると思います。2つのモデルクラス間でリレーションを設定した場合、必ず一方のモデルクラスが親モデルとなり、他方のモデルクラスが子モデルとなります。これは固定です。
親モデルからも小モデルからもリレーションを構築可能であるという点は自己参照型リレーションに限った話ではないですが、1つのインスタンスが親にも子にもなり得るのは自己参照型リレーションならではの特徴になります。
そして、この親にも子にもなれるという特徴を持つため、自己参照型リレーションでは多段階的なリレーションの構築が簡単に実現することができます。
例えば、下記のような処理について考えてみましょう!
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('')
上記の処理のポイントは、studentB
と studentC
が親と子の両方の立場になっているという点になります。
studentA.loving = studentB
においては、studentB
が親として扱われることになりますが、studentB.loving = studentC
においては studentB
が子として扱われることになります。studentC
も同様に親としても子としても扱われています。
親としても子としても扱うことが可能であるため、これらのインスタンスは親も子も両方持つことが可能となります。そしてこれにより、各インスタンスは親と子だけでなく、孫や祖父母のような多段階的な関係性を持たせることが可能になります。
このように、1つのリレーションによって多段階的なリレーションが構築可能なのは、各インスタンスが親にも子にもなれるからです。もし、子の立場のインスタンスが親になれないのであれば、子を持つことはできないことになります。
自己参照型リレーション以外のリレーションの場合、どちらかのインスタンス(モデル)が親 or 子で固定となるため、1つのリレーションによって多段階的なリレーションの構築はできません。これは、親は子になれないため、親に対する親のインスタンスが設定できないからになります。
それに対し、自己参照型リレーションの場合、各インスタンスは使用するデータ属性に応じて親にも子にもなれるため、特定の子に対する親は、他の親に対する子になれます。そのため、前述で紹介したように多段階的な関係を構築するのに便利です。
今回は恋愛事情管理アプリという謎なアプリで実例を示していますが、このような多段階的なリレーションの構築が必要なケースはそれなりに存在するのではないかと思っています。
例えば、掲示板に投稿されるコメントと、そのコメントへの返信について考えると、特定のコメントに対する返信コメントはコメントの一種であり、返信先のコメントの子であると考えられます。さらに、その返信コメントは他のコメントによって返信される可能性もあります。つまり、前述の返信コメントは、他の返信コメントに対する親でもあることになります。したがって、各コメントは下の図のように多段階的なリレーションによって管理されることになると考えられます。
このような関係性に関しても、自己参照型リレーションを導入すれば簡単に実現できます。実際に、下記のようなコメント管理モデルを定義してやれば、返信コメントに対して返信を行うことが可能なモデルとして扱うことができます。
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つのモデルクラスを定義し、これらの間でリレーションを設定することでも返信コメント自体の管理は可能となります。
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
について解説します。
この symmetrical
は ManyToManyField
のコンストラクタに指定可能な引数になります。
この symmetrical
は、自己参照型リレーションを構築するインスタンス間の関係性が対称であることを強制するかどうかを指定するための引数になります。symmetrical
引数を指定しなかった場合、デフォルト設定として symmetrical=True
が指定されることになります。
自己参照型リレーションを持つモデルクラスにおいて、「対称である」とは、2つのインスタンス間において互いのインスタンスの両方から他方のインスタンスに対してリレーションが構築されていることを意味します。
それに対し、一方のインスタンスからのみリレーションが構築されている場合は対称ではありません。この場合は非対称となります。
そして、symmetrical=True
が指定された場合は、この「対象である」ことが強制されるため、2つのインスタンスの一方のインスタンスから他方のインスタンスに対してリレーションを構築すれば、そのインスタンスに対して自動的に他方のインスタンスからもリレーションが構築されることになります。
スポンサーリンク
symmetrical=False
での自己参照型リレーション
この symmetrical
の意味合いに関しては、前述で示した恋愛事情の管理モデル Student
で考えると分かりやすいと思います。
前述で示した Student
では、生徒が好きになれる人数は一人のみでした。それに対し、今回は生徒が好きになれる人数は複数である可能性があるモデルを考えたいと思います。
この場合は、多対多のリレーションとなるため、下記のように Student
を定義することになります。まずは、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
を指定した場合、次の処理はどのような意味合いになるでしょうか?
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
に対してリレーションが構築されることになります。
ManyToManyField
でリレーションを設定した場合、子から親へのリレーションを構築する際にも add
メソッドを利用する必要があります
studentA
の lovings
という集合に studentB
が加えられるわけですから、上記の処理は studentA
が studentB
を好きであることを示しているということになります。ですが、studentB.lovings.add(StudentA)
は実行されていないため、studentB
は sutndentA
のことを好きではありません。
つまり、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つのインスタンスのリレーションは必ず対称となります。すなわち、片方向からのリレーションのみのリレーションが構築されている状態は存在しません。これは、片方向からのリレーションが構築される際に、逆方向からのリレーションも同時に構築されるようになるからです。
例えば、先程紹介した Student
を下記のように変更してみましょう。先ほどからの変更点は symmetrical=False
の引数指定を削除した点のみとなります。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
そして、このモデルクラスを利用した下記の処理について考えてみましょう!下記の print
関数での出力結果はどのようなものになるでしょうか?
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
の場合は2つのインスタンスの間の関係が対称であることが強制されるため、一方からのリレーションが解消された場合は他方からのリレーションも解消されることになります。
symmetrical=True
の場合に追加されるデータ属性
このように、symmetrical
引数の指定によって、自己参照型リレーションで管理できる関係性の意味合いが大きく変わることになります。
さらに、もう1つ重要な点があって、これは symmetrical=True
が指定されている or symmetrical
引数が指定されていない場合はリレーションの設定によって追加されるデータ属性が1つのみという点になります。追加されるデータ属性は「子から親にアクセスするためのデータ属性」のみとなります。具体的には、リレーション設定用のフィールドのクラス変数名のデータ属性が追加されることになります。
また、related_name
は「親から子にアクセするためのデータ属性」の名前を指定するための引数になりますが、symmetrical=False
を指定しない限りはこのデータ属性が追加されないため、related_name
引数を指定しても単に無視されることなります。
下記は前述でも示した symmetrical=True
の場合(symmetrical
を引数指定しない場合)のモデルの定義例となります。
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=False
と symmetrical=True
の一番の違いは、2つのインスタンス間の関係性において非対称であることを許可するか否かの点にあると思います。この点に注目し、どちらの引数を指定するかを決めると良いと思います。
まとめ
このページでは、Django における自己参照型リレーションについて解説を行いました。
自己参照型リレーションは、自分自身のモデルクラスに対してリレーションを設定することで実現することができます。具体的には、リレーションの設定を行うフィールドの第1引数に「自分自身のモデルクラス名」もしくは「self
」を指定してやれば良いです。ただし、どちらの場合でも文字列で指定する必要がある点に注意してください。
この自己参照型リレーションを利用することで、下記のような機能を簡単に実現することができます。
- コメント返信
- フォロー
- フレンド
特に、自己参照型リレーションの場合、各インスタンスが親にも子にもなれるため、多段階的な関係が簡単に実現することができます。実は、結構利用したくなる場面も多い仕組みとなりますので、是非このページで解説した内容に関しては頭に入れておいていただければと思います!