このページでは、Django におけるリレーションについて解説していきます!
前回の連載(下記ページ)の中で解説したモデルは、データベースを管理することを役割としています。また、モデルクラスの定義によって、データベースで扱うテーブル(レコードの形式)を定義することができます。
【Django入門6】モデルの基本また、上記ページでも簡単に説明していますが、モデルクラスのインスタンス同士を関連づけることも可能で、このモデルクラスのインスタンス同士の関連付けのことをリレーションと呼びます。
このページでは、このリレーションについて解説していきます。
リレーションの基本
では、まずはリレーションの基本について解説していきたいと思います。
リレーションとは
前述の通り、リレーションとは「モデルクラスのインスタンス同士の関連付け」のことになります。
下記ページでも解説しているとおり、モデルクラスはデータベースにおけるテーブルの形式、より具体的にはテーブルの持つフィールド(カラム)を定義するクラスとなります。また、モデルクラスのインスタンスは、そのテーブルのレコードとして扱われます。
【Django入門6】モデルの基本単純にモデルクラスを定義した場合、基本的にはモデルクラスによって定義される各種テーブルは独立したものとなります。
それに対し、モデルクラスにリレーションフィールドを定義すれば、そのモデルクラスのテーブルに、特定のテーブルのレコードとの関連付けを行うためのフィールド(カラム)が追加されることになります。そして、そのフィールドを利用して、レコードと他のテーブルのレコードとの関連付けを行うことができるようになります。
定義するリレーションフィールドの種類によっては、テーブルにフィールド(カラム)が追加されるのではなく、関連付けの情報を管理するための新たなテーブルが追加されることもあります
このあたりは後述で解説していきます
具体的には、テーブルのリレーションフィールドには、関連付けされたレコードのプライマリーキーが格納されることになります。これによって、そのフィールドを持つテーブルのレコードに関連付けされたレコードが管理できるようになります。このページでは、このプライマリーキーが id
であることを前提に解説を進めていきますので、この点はご了承ください。
このような関連付けを行うことで、特定のレコードに関連付けられたレコードそのものや、そのレコードの各種フィールドの値を取得することが可能となります。上の図のようなテーブル構成の場合、Student
のテーブルから club_id=3
のレコードを抽出すれば、Club
の name=Tennis
のレコードに関連付けられた Student
の全レコードを取得することができることになります。これによって、例えば Tennis
部に所属する学生をリストアップする機能が実現できることになります。
もちろん、他の Club
のレコードに関連付けられている Student
のレコードを抽出することで、他のクラブに所属している生徒をリストアップするようなことも可能です。つまり、上の図のようにレコードの関連付けを行っておくことで、各クラブに所属している生徒をリストアップする機能が実現できることになります。
重要なのは、このような機能は異なるテーブルのレコードを関連付けさせているから実現可能であるという点になります。各テーブルが独立していると、このような機能は実現できません。例えば、下の図のようなテーブルでレコードを管理している場合、どの生徒がどのクラブに所属しているかが判断できません。
世の中のウェブアプリでは、このようなレコード同士の関連付け、すなわちリレーションを利用して様々な魅力的な機能が実現されています。例えば Twitter の例で考えると、このアプリではツイートとユーザーの間で関連付けが行われているため、ツイートから「ツイート元のユーザー」を特定して表示するようなことが可能となっていると予想できます。また、ユーザーから「ユーザーのツイート一覧」を表示するようなことも可能です。このような機能は、リレーションを利用しているからこそ実現可能な機能となります。
他のウェブアプリにおいても、リレーションによって実現されている機能が存在することは容易に想像がつくのではないかと思います。このように、リレーションによってウェブアプリで管理するデータ同士を関連付けしておくことで、様々な機能を実現することが可能となります。
ここまでの解説では「2つの異なるモデルクラスのインスタンス」に対する関連付けについて説明してきましたが、実は「同じモデルクラスの2つのインスタンス」の間で関連付けを行うことも可能です。これについては別途下記ページで解説していますので、詳しく知りたい方は、後で別途下記ページを参照していただければと思います。
【Django】自己参照型リレーションについて分かりやすく解説(同じモデルクラスのインスタンスの関連付け)また、このページでのリレーションについては「2つの異なるモデルクラスのインスタンス」に対する関連付けであることを前提に解説を進めていきますので、この点についてはご理解していただければと思います。
スポンサーリンク
リレーションフィールド
前述の通り、リレーションとは、基本的には2つの異なるモデルクラス間のインスタンス同士を関連付けることを言います。そして、この関連付けを行う上でポイントになるのがリレーションフィールドになります。このリレーションフィールドを定義したモデルクラスのインスタンスは他のモデルクラスのインスタンスとの関連付けが可能となります。
このリレーションフィールドは、他のモデルクラスとの間に関連性を持たせるための少し特殊なフィールドではあるのですが、このリレーションフィールドに関しても他のフィールド同様に models.Field
のサブクラスとなります。ですので、基本的には他のフィールドを定義する時と同様の手順で定義可能です。つまり、リレーションフィールドのインスタンスを値とするクラス変数をモデルクラスに定義してやれば良いことになります。
また、このリレーションフィールドには下記の3つの種類のものが存在し、どのリレーションフィールドを定義するのかによって実現可能なリレーションの種類、すなわち実現可能なインスタンスの関連付けが異なることになります。このリレーションの種類については、次の リレーションの種類 で詳細を解説します。
- 1対1のリレーション:
models.OneToOneField
- 多対1のリレーション:
models.ForeignKey
- 多対多のリレーション:
models.ManyToManyField
さらに、これらのリレーションフィールドには、第1引数に関連性を持たせたいモデルクラスを指定する必要があります。具体的には、この第1引数には、モデルクラスのクラスオブジェクト or モデルクラスの名称の文字列を指定する必要があります。
以降では、このリレーションフィールドの第1引数で指定されたモデルクラスのことを 関連モデルクラス
と記します。リレーションフィールドを定義することで、そのリレーションフィールドを定義したモデルクラスのインスタンスは、この 関連モデルクラス
のインスタンスと関連付け可能となります。
例えば、下記のように models.py
を作成した場合、Stundent
にはリレーションフィールドの1つである models.ForeignKey
のフィールドが定義されており、さらに、このフィールドの第1引数に Club
が指定されているため、Club
は Student
に対する 関連モデルクラス
ということになります。そのため、Stundent
のインスタンスと Club
のインスタンスとは関連付け可能となります。そして、この場合、リレーションフィールドは models.ForeignKey
であるため、Club
のインスタンスと Student
のインスタンスとの間では多対1の関連付けが可能ということになります。
from django.db import models
class Club(models.Model):
name = models.CharField(max_length=32)
class Stundent(models.Model):
name = models.CharField(max_length=32)
club = models.ForeignKey(Club, 略)
また、前述でも少し触れたとおり、第1引数は文字列で指定することも可能です。基本的には、第1引数にはモデルクラスのオブジェクトを指定すればよいのですが、第1引数に指定するモデルクラスが自分自身の場合や、下記のように第1引数に指定するモデルクラスの定義が、リレーションフィールドを定義するモデルクラスよりも下側に定義されている場合は、モデルクラスの名称の文字列を指定する必要があります。
from django.db import models
class Stundent(models.Model):
name = models.CharField(max_length=32)
club = models.ForeignKey('Club', 略)
class Club(models.Model):
name = models.CharField(max_length=32)
リレーションの種類
先ほど簡単に説明したように、リレーションには下記の3つの種類が存在します。Django でも、これら3つの種類のリレーションを利用することが可能です。そして、この利用するリレーションによって、実現可能なインスタンスの関連付けが異なることになります。
- 1対1のリレーション
- 多対1のリレーション
- 多対多のリレーション
ここからは、これらの3つのリレーションについて簡単に解説していきます。また、これらの解説の中では、クラブ(部活動)を管理するモデルクラス Club
と生徒を管理するモデルクラス Student
を定義し、Student
側に 関連モデルクラス
を Club
とするリレーションフィールドを定義することで、「各クラブに所属する生徒の管理」を行う例を用いて説明を行っていきます。上記のリレーションの種類によって、実現可能な管理にどのような違いがあるのか?という点に注目しながら解説を読み進めていただければと思います。
1対1のリレーション
1対1のリレーションは、2つのモデルクラス両方のインスタンスに関連付け可能なインスタンスの個数が 1
となるリレーションとなります。お互いのインスタンスに関連付け可能なインスタンスの数は1つのみとなるため、1対1の関係のリレーションとなります。
前述の通り、この1対1のリレーションはモデルクラスに OneToOneField
のフィールドを定義することで実現できます。
この OneToOneField
のフィールドを定義したモデルクラスのテーブルには、その 関連モデルクラス
のインスタンスのプライマリーキーを格納するためのフィールド(カラム)が追加されることになります。そして、そのフィールドに格納可能なプライマリーキーは1つのみとなります。そのため、OneToOneField
のフィールドを定義したモデルクラスのインスタンスと関連付け可能な 関連モデルクラス
のインスタンスの個数は 1
となります。
また、このテーブルに追加されたフィールドでは、各インスタンス間での値の重複が禁止されます。そのため、関連モデルクラス
の各インスタンスに関連付け可能なインスタンスの個数も 1
ということになります。
Club
と Student
の例で考えれば、Student
のインスタンスには1つの Club
のインスタンスのみが関連付け可能で、さらに各 Student
のインスタンスの間で関連付けられる Club
のインスタンスに重複があってはいけないということになります。
つまり、このようなモデルクラスの構成においては、各学生が所属できるクラブは1つのみで、さらにクラブに所属できる学生も一人のみということになります。通常、クラブには複数の学生が所属できるはずなので、少し妙な「各クラブに所属する生徒の管理」となってしまいますね。なので、「各クラブに所属する生徒の管理」を実現するためには1対1のリレーションは不適切ということになります。
多対1のリレーション
次は、多体1のリレーションについて解説していきます。
多対1のリレーションとは、”リレーションフィールドを定義したモデルクラス” のインスタンスと関連付け可能な 関連モデルクラス
のインスタンスの個数が 1
、かつ、関連モデルクラス
のインスタンスと関連付け可能な “リレーションフィールドを定義したモデルクラス” のインスタンスの個数が 多
となる関連付けになります。
この場合、上の図のように、複数の Student
のインスタンスが1つの Club
のインスタンスに関連付け可能となるため、多対1のリレーションということになります。
前述の通り、この多対1のリレーションはモデルクラスに ForeignKey
のフィールドを定義することで実現できます。
この ForeignKey
のフィールドを定義したモデルクラスのテーブルには、その 関連モデルクラス
のインスタンスのプライマリーキーを格納するためのフィールド(カラム)が追加されることになります。そして、そのフィールドに格納可能なプライマリーキーは1つのみとなります。そのため、ForeigneKey
のフィールドを定義したモデルクラスのインスタンスと関連付け可能な 関連モデルクラス
のインスタンスの個数は 1
となります。この点に関しては OneToOneField
の場合と同様となります。
また、このテーブルに追加されたフィールドでは、各インスタンス間で値が重複することが許されています。ここが ForeigneKey
と OneToOneField
との違いになります。そして、このために、関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数は 多
ということになります。例えば下の図の場合、Club
の id=2
のインスタンスは2つの Student
のインスタンスと関連付けられていることになります。
Club
と Student
の例で考えると、クラブの掛け持ちが不可な「各クラブに所属する生徒の管理」が実現できることになります。
多対1のリレーションの場合、ForeigneKey
のフィールドをどちらのモデルクラスに定義するのかによって関連付けの意味合いが大きく変化することに注意してください。例えば、Club
側に ForeigneKey
のフィールドを定義した場合、一人の学生が複数のクラブに所属できるものの、各クラブに所属可能な生徒数は1人のみとなってしまいます。
このように、多対1のリレーションにおいては、2つのモデルクラスのうちのどちらに ForeigneKey
のフィールドを定義するのかが非常に重要となりますので、この点に注意しながらモデルクラスの定義を行うようにしましょう!ForeigneKey
のフィールドを定義したモデルクラス側のインスタンスが、関連付け可能なインスタンスの個数が 1
となります(つまり、多対1の関係における 多
側になります)。
多対多のリレーション
最後に多対多のリレーションについて解説します。
多対多のリレーションは、2つのモデルクラス両方のインスタンスに関連付け可能なインスタンスの個数が 多
となるリレーションとなります。お互いのインスタンスに関連付け可能なインスタンスの数が複数となるため、多対多の関係のリレーションとなります。
前述の通り、この多対多のリレーションはモデルクラスに ManyToManyField
のフィールドを定義することで実現できます。
この多対多のリレーションは、特にテーブルの観点で考えると少し特殊で、モデルクラスに ManyToManyField
のフィールドを定義することで、各インスタンス間の関連付けを管理する新たなテーブルが追加されることになります。このテーブルには、そのテーブルの id
に加えて2つのフィールド(カラム)が存在し、一方のフィールドでは ManyToManyField
のフィールドを定義したモデルクラスのインスタンスのプライマリーキーが、さらに他方のフィールドでは 関連モデルクラス
のインスタンスのプライマリーキーが格納されることになります。
そして、このテーブルの各レコードによって、2つのモデルクラスのインスタンスの関連付けが管理されることになります。例えば下の図の緑枠で囲ったレコードは、プライマリーキーが 2
の Student
のインスタンスとプライマリーキーが 1
の Club
のインスタンスが関連付けられていることを示しています。
そして、これらの2つのフィールドに格納される値はレコード間での重複が許可されるため、両方のモデルクラスのインスタンスが関連付け可能な他方のモデルクラスのインスタンスの個数は 多
であることになります。
Club
と Student
の例で考えれば、各学生が所属できるクラブは複数かつ、クラブには複数人の学生が所属できることになります。つまり、クラブの掛け持ち可の「各クラブに所属する生徒の管理」が実現できることになります。
ここまで説明してきたように、リレーションの種類には下記の3つが存在し、それぞれで実現可能な関連性の管理が異なることになります。それぞれの特徴を理解して、適切なリレーションを利用することが重要となります。
- 1対1のリレーション
- 多対1のリレーション
- 多対多のリレーション
リレーションフィールドとデータ属性
続いて、リレーションフィールドの定義によってインスタンスに追加されるデータ属性について解説していきます。インスタンスの関連付けは、このデータ属性を利用して実施することになるため、このデータ属性はリレーションを利用する上で非常に重要となります。
まず、通常のフィールド同様に、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールド名のデータ属性が追加されることになります。
そして、これはリレーションフィールド特有の話なのですが、リレーションフィールドを定義したモデルクラスのインスタンスだけでなく、リレーションフィールドの第1引数に指定した 関連モデルクラス
のインスタンスにもデータ属性が追加されることになります。
これらのデータ属性を利用することで、リレーションフィールドを定義したモデルクラスのインスタンスからも、その 関連モデルクラス
のインスタンスからも、他方側のモデルクラスのインスタンスに対して関連付けを行うことが可能です。これらを実施するのはビューの関数やモデルクラスのメソッド等になります。
ただ、これらの追加されるデータ属性の名称や、それらのデータ属性を利用した関連付けの手順が少し複雑なので、ここからは、このリレーションフィールドの定義によって追加されるデータ属性についての詳細を詳しく説明していきたいと思います。
1
と 多
の関係と追加されるデータ属性
で、このリレーションフィールドの定義によって追加されるデータ属性を理解する上で重要になるポイントは、そのインスタンスに関連付け可能なインスタンスの個数になります。このインスタンスに関連付け可能なインスタンスの個数は 1
or 多
のどちらかになります。このどちらであるのかによって追加されるデータ属性の名称や使い方等が異なるので、各モデルクラスのインスタンスに関連付け可能なインスタンスの個数を意識しながら関連付けについての理解を進めることが重要となります。
そのため、ここで、各モデルクラスのインスタンスに関連付け可能なインスタンスの個数を整理しておきたいと思います。
まず、1対1のリレーションの場合は単純で、OneToOneField
のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は 1
となります。さらに、その関連付けの相手となる 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数も 1
となります。
同様に、多対多のリレーションの場合も単純で、ManyToManyField
のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は 多
となります。さらに、その関連付けの相手となる 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数も 多
となります。
そして、多対1のリレーションの場合は、まず ForeignKey
のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は 1
となります。それに対し、その関連付けの相手となる 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数は 多
となります。
多対1のリレーションの場合は若干ややこしいのですが、テーブルを思い浮かべると分かりやすいと思います。多対1のリレーション で説明したように、ForeignKey
のフィールドが定義されたモデルクラスのテーブルには 関連モデルクラス
のインスタンスのプライマリーキーを格納するためのフィールドが追加されることになります。そして、このフィールドに格納できるプライマリーキーは1つだけなので、ForeignKey
のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は 1
ということになります。また、他方側の 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数は、余った方の 多
ということになります。
定義するリレーションフィールドに応じた 1
or 多
の関係をまとめると下図のようになります。これを理解した上で、ここからの説明を読み進めていただければと思います。
データ属性の名称
前述で簡単に説明しましたが、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールドのフィールド名のデータ属性が追加されることになります。これに関しては、リレーションの種類に関わらず共通になります。
例えば、下記のように Student
にリレーションフィールドを定義した場合、この Student
のインスタンスには club
というデータ属性が追加されることになります。下記では ForeignKey
のフィールドを定義していますが、models.OneToOneField
や models.ManyToManyField
のフィールドを定義した場合も、同様に Student
のインスタンスには club
という名称のデータ属性が追加されることになります。
from django.db import models
class Club(models.Model):
name = models.CharField(max_length=32)
class Stundent(models.Model):
name = models.CharField(max_length=32)
club = models.ForeignKey(Club, 略)
それに対し、関連モデルクラス
のインスタンスに追加されるデータ属性の名称は、その 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの数(1
or 多
)によって決まりす。
具体的には、関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数が 1
の場合、相手のモデルクラス名
という名称のデータ属性が追加されることになります。
それに対し、関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数が 多
の場合、相手のモデルクラス名_set
という名称のデータ属性が追加されることになります。
少し補足しておくと、データ属性の名称の 相手のモデルクラス名
部分はすべて小文字となります
また、これらの 関連モデルクラス
のインスタンスに追加されるデータ属性の名称は変更可能で、これに関しては後述の 追加されるデータ属性の名称変更(related_name) で解説します
例えば、下記のように Student
と Club
を定義する場合、リレーションフィールド
部分が OneToOneField
であれば、Club
のインスタンスと関連付けが可能なインスタンスの個数は 1
であるため、Club
のインスタンスにはデータ属性 student
が追加されることになります。
from django.db import models
class Club(models.Model):
name = models.CharField(max_length=32)
class Stundent(models.Model):
name = models.CharField(max_length=32)
club = models.リレーションフィールド(Club, 略)
もし、上記の Student
と Club
の定義において、リレーションフィールド
部分が ManyToManyField
or ForeignKey
であれば、Club
のインスタンスと関連付けが可能なインスタンスの個数は 多
であるため、Club
のインスタンスにはデータ属性 student_set
が追加されることになります。
このように、関連モデルクラス
のインスタンスに追加されるデータ属性は、そのインスタンスと関連付け可能な個数が 1
であれば 相手のモデルクラス名
という名称になりますし、多
であれば 相手のモデルクラス名_set
という名称になります。このように、データ属性の名称が変化するのは、そのデータ属性の役割、つまり、そのデータ属性での管理対象が異なるからになります。この点について、次の節で解説していきます。
データ属性の役割
続いて、追加されたデータ属性の役割、つまり、そのデータ属性で何を管理するのか?という点について説明します。ここを理解しておけば、インスタンス同士の関連付けを行う方法や関連付けられたインスタンスの取得方法が理解しやすくなります。
まず、関連付け可能なインスタンスの個数が 1
であるインスタンスの場合、そのインスタンスに追加されたデータ属性の役割は、関連付け相手となるモデルクラスの1つのインスタンスを管理することとなります。このデータ属性では、その関連付け相手となるモデルクラスのインスタンスを =
演算子で直接参照することが可能です。
それに対し、関連付け可能なインスタンスの個数が 多
であるインスタンスの場合、そのインスタンスに追加されたデータ属性の役割は、集合を管理すること、もっと詳しく言えば、関連付け相手となるモデルクラスのインスタンスの集合を管理することとなります。なので、このデータ属性では、通常の集合の時と同様に、このデータ属性にメソッドを実行させることで、その集合に対する操作を行うことになります。
関連付けが可能なインスタンスが 多
なので、これらの関連付けられたインスタンスは集合(リスト等でも良いが)での管理が必要となります。そのため、この場合はリレーションフィールドを定義することで追加されるデータ属性は集合を参照するデータ属性となるというわけです。また、前述で、”関連モデルクラス
のインスタンスに追加されるデータ属性は、そのインスタンスと関連付け可能なインスタンスの個数が 多
であれば 相手のモデルクラス名_set
という名称になる” と説明しましたが、わざわざデータ属性名に _set
が付加されるのは、そのデータ属性が集合を管理するものであることを示すためになります。
スポンサーリンク
インスタンスの関連付け
続いて、インスタンスの関連付けについて解説していきます。
この関連付けは、リレーションフィールドの定義によって追加されるデータ属性を利用して実施することができます。
関連付け可能なインスタンスの個数が 1
の場合
関連付け可能なインスタンスの個数が 1
であるインスタンスの場合は、その追加されたデータ属性に =
演算子で相手のモデルクラスのインスタンスを参照させることで、そのデータ属性を持つインスタンスと参照先のインスタンスとを関連付けることができます。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されている場合、下記のような処理により taro
に soccer
を関連付けることができます。関連付けが実施されるのは taro.club = soccer
の部分になります。ただ、これだけだと、その関連付けがデータベースには反映されないため、最後に taro.save()
を実行して taro
のインスタンスをレコードとしてデータベースに保存することで、その関連付けのデータベースへの反映を行なっています。
このように、=
演算子での関連付けをデータベースに反映させるためには、リレーションフィールドを定義したモデルクラスのインスタンスから save
メソッドを実行させる必要があるという点に注意してください。
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save()
関連付け可能なインスタンスの個数が 多
の場合
それに対し、関連付け可能なインスタンスの個数が 多
であるインスタンスの場合は、その追加されたデータ属性に add
メソッドを実行させることで関連付けを行うことになります。この add
メソッドの引数には、関連付けしたいインスタンスを指定する必要があります。
この add
メソッドの実行によって、「関連付けられているインスタンスを管理する集合」に新たなインスタンスが追加されることになり、これによってインスタンスの関連付けが実現されるようになっています。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されている場合、下記のような処理により、soccer
に関連付けられるインスタンスに taro
を追加することができます。
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)
# soccerにtaroを関連付け
soccer.student_set.add(taro)
ちょっとややこしいようにも感じますが、上記で解説した内容は特別なものではなく、通常の変数と同じ考え方でのインスタンスの管理となります。
例えば、変数で1つのインスタンスを管理するのであれば、その変数に =
演算子で管理対象のインスタンスを参照させることになります。
class Person:
def __init__(self, name):
self.name = name
# 1つのインスタンスを管理するために変数で参照
x = Person('Taro')
それに対し、変数で複数のインスタンスを管理するのであれば、その変数からはリストや集合等を参照させ、さらに、メソッドを用いて、それらのリストや集合への操作(追加・取得・削除等)を行うことになります。例えば、その変数が集合を参照しており、その集合に要素を追加するのであれば add
メソッドを実行することになると思います。
class Person:
def __init__(self, name):
self.name = name
# 複数のインスタンスを管理するためにaddメソッドで集合に追加
x = set()
x.add(Person('Taro'))
x.add(Person('Hanako'))
x.add(Person('Saburo'))
これと同じようなことを、リレーションフィールドの定義によって追加されるデータ属性に対しても行う必要があるだけで、特別難しい話ではないと思います。重要なのは、操作対象のインスタンスと関連付けされるインスタンスが 1
なのか 多
なのかを意識し、それに応じたデータ属性の使い方を実施することになります。
関連付けのデータベースへの反映
また、=
演算子による参照で関連付けを行った場合、その関連付けをデータベースに反映するためには、リレーションフィールドを定義した方のモデルクラスのインスタンスに save
メソッドを実行させる必要がありました。これは、=
演算子による参照を行ってもデータベースのレコードの更新が行われないからになります。
ですが、add
メソッドでの関連付けに関しては、その add
メソッドの実行が成功したタイミングでインスタンスの関連付けがデータベースに反映されることになります。ですので、上記の例のように、関連付けのみを実施するのであれば、add
メソッド実行後に save
メソッドを実行させる必要はありません。
ただし、add
メソッドでのレコード更新時に更新されるのは、その add
メソッドの実行によって変化するリレーションフィールドのみとなります。したがって、他のフィールドを更新する場合は、別途 save
メソッドメソッドを実行する必要があります。
両方のデータ属性から関連付けが可能
ここまで説明してきたように、インスタンスの関連付けは、リレーションフィールドの定義によって追加されるデータ属性によって実現可能です。また、リレーションフィールドの定義を行うことで、その定義を行ったモデルクラスだけでなく、その 関連モデルクラス
に対してもデータ属性が追加されることになります。
したがって、インスタンスの関連付けは、リレーションフィールドの定義を行った方のモデルクラスのインスタンスからだけでなく、その 関連モデルクラス
のインスタンスからも実施可能ということになります。そして、同じインスタンス同士を関連付けるのであれば、どちらのモデルクラスのインスタンスから関連付けを行ったとしても結果は変わりません。
つまり、特定のインスタンスから他方のインスタンスに関連付けを行えば、自動的に他方のインスタンスからその特定のインスタンスが関連付けされることになります。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されている場合、下記のような処理により、taro
に soccer
を関連付けることができます。
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save()
同時に、soccer
に taro
が関連付けられることにもなるため、上記の処理により、soccer.student_set
の集合に taro
が追加されることにもなります。
逆に、下記のような処理により、soccer
に taro
を関連付ければ、
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)
# soccerにtaroを関連付け
soccer.student_set.add(taro)
同時に、taro
に soccer
が関連付けられることにもなるため、上記の処理により、soccer.club
は taro
を参照することにもなります。
このように、特定のインスタンスから他方のインスタンスを関連付ければ、逆方向の関連付けも自動的に行われるようになっています。結局、一方のインスタンスから関連付けしてしまえば、2つのインスタンスが関連しあう状態となるので、関連付けは一方のインスタンスから行えば良いだけですし、その関連付けは、リレーションフィールドの定義を行ったモデルクラスのインスタンスから行っても、その 関連モデルクラス
のインスタンスから行っても問題ありません。
ただ、どちらかというとシンプルな考え方で実装可能なのは、リレーションフィールドの定義を行ったモデルクラスのインスタンスからの関連付けになると思います。なので、どちらかというと、リレーションフィールドの定義を行ったモデルクラスのインスタンスからの関連付けの実施をオススメします。
いずれにせよ、そのデータ属性を利用するインスタンスに関連付け可能なインスタンスの個数が 1
or 多
のどちらであるかによって関連付けの仕方やデータ属性の名称が変わることになるので、その点については注意してください。
関連付けられたインスタンスの取得
また、特定のインスタンスに関連付けられたインスタンスの取得に関しても、前述で説明したリレーションフィールドの定義によって追加されるデータ属性を用いて実現可能となります。
関連付け可能なインスタンスの個数が 1
の場合
関連付け可能なインスタンスの個数が 1
であるインスタンスの場合は単純で、追加されたデータ属性の参照先から、そのインスタンスに関連付けられたインスタンスを取得すれば良いだけです。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されている、かつ student
が Student
のインスタンスである場合、下記のような処理により student
に関連付けられた Club
のインスタンスを取得し、それを出力することができます。
# studentに関連付けられたClubのインスタンスを取得
club = student.club
print(club)
関連付け可能なインスタンスの個数が 多
の場合
それに対し、関連付け可能なインスタンスの個数が 多
であるインスタンスの場合は少し複雑で、追加されたデータ属性にメソッドを実行させてインスタンスを取得することが必要となります。この追加されたデータ属性に実行させられるメソッドとしては、例えば下記のようなものが挙げられます。
all
:データ属性の集合に含まれる全インスタンスを要素とする集合を取得するfilter
:データ属性の集合に含まれるインスタンスの内、引数で指定した条件を満たす全てのインスタンスを要素とする集合を取得するget
:データ属性の集合に含まれるインスタンスの内、引数で指定した条件を満たす1つのインスタンスを取得する
all
や filter
によって取得される集合は、より具体的に言うとクエリーセットとなります。このクエリーセットにメソッドを実行させることで、取得した集合内のインスタンスの並びを変更したり、さらに条件を絞ったインスタンスの集合を取得するようなことも可能です。また、このクエリーセットに対して for
ループを実行することで、そのクエリーセットに含まれる全インスタンスに対して1つ1つ処理を実行するようなことも可能になります。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されている、かつ club
が Club
のインスタンスである場合、下記のように処理を行えば club
に関連付けられた全てのインスタンスを取得し、それらを1つ1つ出力することができることになります。
# clubに関連付けられたStudentの全インスタンスの集合を取得
students = club.student_set.all()
for student in students:
print(student)
リレーションを利用する目的は、単にインスタンス同士を関連付けることではなく、その関連付けられたインスタンスの取得によって新たな機能を実現することにあります。なので、関連付けだけでなく、関連付けられたインスタンスの取得方法についてもしっかり理解しておきましょう!
追加されるデータ属性のまとめ
ここまで説明してきた内容を下図の表にまとめておきます。リレーションの種類、さらにはリレーションフィールドを定義したモデルクラス or その 関連モデルクラス
のどちらであるのかによって、追加されるデータ属性の名称・追加されるデータ属性の管理対象オブジェクトが異なることになります。また、追加されるデータ属性の管理対象オブジェクトの違いによって、インスタンスの関連付けやインスタンスの取得の仕方が異なることになります。
このあたりを理解した上で、インスタンス同士の関連付けを行うウェブアプリを開発するようにしましょう!
スポンサーリンク
リレーションの利用
ここからは、ここまで説明してきたリレーションの利用手順、すなわちインスタンス同士の関連付けの手順について、リレーションの種類ごとに解説をしていきたいと思います。
基本的には、ここでの説明は、ここまでの解説内容をリレーションの種類ごとにまとめた内容となります。なので、ここまでの解説内容を振り返りながら、解説を読み進めていただければと思います。
1対1のリレーションの利用
まずは、1対1のリレーションの利用手順について説明していきます。
OneToOneField
を定義する
リレーションを利用する際に最初に行うことは models.py
で定義するモデルクラスへのリレーションフィールドの定義となります。1対1のリレーションを利用するのであれば、models.OneToOneField
のフィールドを定義することになります。
例えば下記のように models.py
で Student
と Club
を定義すれば、これらのインスタンスの間には1対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)
height = models.FloatField()
weight = models.FloatField()
club = models.OneToOneField(Club, on_delete=models.CASCADE, null=True)
モデルクラスの定義を変更した際には makemigrations
コマンドと migrate
コマンドを実施する必要があるので、この点にはご注意ください。」
上記では OneToOneField
コンストラクタの引数に on_delete
を指定していますが、OneToOneField
コンストラクタでは on_delete
引数の指定が必須となります。この on_delete
引数の意味合いについては下記ページで解説していますので、詳細を知りたい方は下記ページを参照していただければと思います。
また、引数に null=True
も指定していますが、これは、そのフィールドが空の状態のインスタンスをデータベースのテーブルに保存できるようにするためです。リレーションフィールドの場合、関連付けを行なっていない状態でインスタンスを保存するためには null=True
の指定が必要となります。そのような状態でのインスタンスの保存を行う機会は結構あるので、リレーションフィールドに関しては、迷ったら null=True
を指定しておくことをオススメします。
インスタンス同士の関連付けを行う
続いて、インスタンス同士の関連付けを行う処理をビューもしくはモデルクラスに実装していくことになります。このページでは、この関連付けはビューで実施することを前提に解説を進めます。
どんなタイミング&どういう目的で関連付けを行うのかについては開発対象のウェブアプリによって異なりますので、ここからは、インスタンス同士の関連付けを行う例として、単純に Student
のインスタンスを生成し、そのインスタンスを name='soccer'
を満たす Club
のインスタンスと関連付けるビューの関数の実装例を紹介していきたいと思います
ビューの関数からの返却値等はてきとうになっているので注意してください
データ属性の名称 で解説したように、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールドのフィールド名のデータ属性が追加されます。今回の場合は Student
のインスタンスにデータ属性 club
が追加されることになります。そして、そのデータ属性 club
にインスタンスを =
で参照させることで、Student
のインスタンスに Club
のインスタンスが関連付けられることになります。
そのため、下記のような処理によって、Student
のインスタンスに対して Club
のインスタンスを関連付けることができることになります(事前に Club
のテーブルに name='soccer'
を満たすレコードが1つのみ保存されていることを前提としたビューになります。以降の例も同様です)。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save()
return HttpResponse(str(taro))
上記では Student
のインスタンスのデータ属性を利用して Club
のインスタンスとの関連付けを実施していますが、逆に Club
のインスタンスのデータ属性を利用して Student
のインスタンスとの関連付けを実施することも可能です。
データ属性の名称 で解説したように、1対1のリレーションの場合、関連モデルクラス
のインスタンスには 相手のモデルクラス名
のデータ属性が追加されることになります。今回の場合は、Club
にデータ属性 student
が追加されることになるため、このデータ属性に Student
のインスタンスを参照させることで関連付けを行うことが可能となります。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# soccerにtaroを関連付け
soccer.student = taro
# データベースに反映
taro.save()
return HttpResponse(str(taro))
あくまでも、関連付けによって実際にレコードが変化するのはリレーションフィールドを定義したモデルクラスのものであるという点に注意してください。そのため、リレーションフィールドを定義していないモデルクラスのデータ属性を利用して関連付けを行ったとしても、それをデータベースに反映するためには、リレーションフィールドを定義した方のモデルクラスのインスタンスに save
メソッドを実行させる必要があります。
例えば、上記の例において、soccer.student = taro
を実行すると、Club
のインスタンスである soccer
のレコードが変化するように感じると思います。なので、それをデータベースに反映するためには、taro.save()
ではなく soccer.save()
を実行するのが正しいように思えるかもしれません。
ですが、今回リレーションフィールドを定義したのは Student
側なので、レコードとして考えた場合に soccer.student = taro
によって変化するのは Student
のインスタンスである taro
側となります。そのため、soccer.student = taro
による関連付けをデータベースに反映するためには taro.save()
を実行する必要があります。リレーションフィールドの存在しない soccer
に対して save
メソッドを実行させたとしても、存在しないリレーションフィールドは当然更新されないので注意してください。
もう一点補足しておくと、実は、上記と同様の関連付けは次のような関数でも実現可能です。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1,
club=soccer # soccerを関連付け
)
return HttpResponse(str(taro))
Student.objects.create
を実行することで、キーワード引数で指定した通りに各種フィールドに値が格納された Student
のレコードがデータベースに新規登録されることになります。したがって、この時に club=Clubのインスタンス
を指定してやれば、club
フィールド(リレーションフィールド)に、その Clubのインスタンス
のプライマリーキーが格納された Student
のレコードがデータベースに新規登録されることになります。Student.objects.create
の場合はデータベースへの新規登録まで行われるため、save
メソッドの実行も不要になります。結局、関連付けはレコードのリレーションフィールドにプライマリーキーを格納することで実現されることになるため、上記のような処理によってもインスタンスの関連付けが可能となります。
さて、ここまで3つの関数を紹介し、それぞれ異なる方法でインスタンスの関連付けを行いましたが、これらの関数で実施される関連付けの結果は全て同じとなります。このことより、インスタンスの関連付けは様々な方法で実施可能であることを理解していただけると思います。
関連付けられたインスタンスを取得する
インスタンス同士の関連付けを行えば、一方のインスタンスから他方のインスタンスを取得し、その取得したインスタンスの情報を出力することができるようになります。それを、ビューで実施する例を示していきます。
下記は、Student
のインスタンスから、そのインスタンスに関連付けられた Club
のインスタンスを取得するビューの関数の例となります。より具体的には、プライマリーキーが引数 pk
と一致する Student
のインスタンスと、その Student
のインスタンスに関連付けられた Club
のインスタンスを取得し、これらの情報を表示するビューの関数の例となります。
from django.http import HttpResponse
from .models import Student
def student_view(request, pk):
# Studentのインスタンスの取得
student = Student.objects.get(pk=pk)
# studentの所属するClubのインスタンスの取得
club = student.club
body = ''
body += '名前:' + student.name + '<br>'
body += '身長:' + str(student.height) + '<br>'
body += '体重:' + str(student.weight) + '<br>'
if club is not None:
body += '所属:' + club.name + '<br>'
return HttpResponse(body)
関連付けられたインスタンスの取得 で解説したように、関連付け可能なインスタンスの個数が 1
であるインスタンスの場合、リレーションフィールドの定義によって追加されたデータ属性が参照するオブジェクトそのものが、そのインスタンスに関連付けられたインスタンスとなります。今回の場合は、student.club
が student
に関連付けられた Club
のインスタンスということになります。
また、OneToOneField を定義する で示したように、Student
の club
フィールドには null=True
を指定しているため、club
フィールドが空の状態のレコードもデータベースに保存できることになります。そして、この空の状態のフィールドは、インスタンスのデータ属性としては値が None
となるため、そのことを考慮して上記のように student.club
が None
でない場合のみ、Club
のインスタンスの情報を出力するようにしています。
同様に、Club
のインスタンスから、そのインスタンスに関連付けられた Student
のインスタンスを取得することも可能です。その例が下記で、プライマリーキーが引数 pk
と一致する Club
のインスタンスと、その Club
のインスタンスに関連付けられた Student
のインスタンスを取得し、それらの情報を表示する例となります。
from django.http import HttpResponse
from .models import Club
def club_view(request, pk):
# Clubのインスタンスの取得
club = Club.objects.get(pk=pk)
# clubに所属するStudentのインスタンスの取得
student = club.student
body = ''
body += '名前:' + club.name + '<br>'
body += '部員:<br>'
if student is not None:
body += '<ul>'
body += '<li>' + student.name + '</li>'
body += '</ul>'
return HttpResponse(body)
ここまで説明してきたように、1対1のリレーションの場合、インスタンス同士の関連付けはリレーションフィールドの定義によって追加されたデータ属性に関連付けたいインスタンスを参照させるだけで実現可能です。また、関連付けられたインスタンスの取得に関しても、そのデータ属性を単に参照するだけで実現可能です。なので、1対1のリレーションの利用は結構簡単です。
ここから説明する多対1のリレーションや多対多のリレーションの場合は若干複雑になりますが、それでも基本的に実施することは同じで、重要なのは、関連付け可能なインスタンスの個数(1
or 多
)を意識して、リレーションフィールドの定義によって追加されるデータ属性を利用するという点になります。
多対1のリレーションの利用
次は、多対1のリレーションの利用手順について説明していきます。
ForeignKey
を定義する
多対1のリレーションを利用する場合、リレーションフィールドとしては models.ForeignKey
のフィールドを定義することになります。すなわち、モデルクラスに models.ForeignKey
のインスタンスを値とするクラス変数を定義することで、多対1のリレーションが利用できるようになります。
例えば下記のように models.py
で 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)
height = models.FloatField()
weight = models.FloatField()
club = models.ForeignKey(Club, on_delete=models.CASCADE, null=True)
ForeignKey
のコンストラクタに関しても、引数 on_delete
の指定が必須となります。
また、多対1のリレーションにおいては、ForeignKey
のフィールドを定義した方のモデルクラスのインスタンスが、関連付け可能なインスタンスの個数が 1
であるインスタンスとなります。逆に、その 関連モデルクラス
のインスタンスに関連付け可能なインスタンスの個数は 多
となります。つまり、上記の場合、Student
のインスタンスに関連付け可能なインスタンスの個数が 1
で、Club
のインスタンスに関連付け可能なインスタンスの個数が 多
ということになります。この点を意識して、次に説明するインスタンスの関連付けや、関連付けされたインスタンスの取得の実装を行うことが重要となります。
インスタンス同士の関連付けを行う
ということで、次はインスタンスの関連付けを実施していきましょう!
まず、Student
のインスタンスに関連付け可能な Club
のインスタンスの個数は 1
であるため、基本的には1対1のリレーションの時と同様の手順で Student
のインスタンスから Club
のインスタンスの関連付けを実施することが可能です。
この関連付けを行うビューの関数の例は下記となります。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save()
return HttpResponse(str(taro))
上記は、1対1のリレーションの利用 の インスタンス同士の関連付けを行う で示した “Student
のインスタンスから Club
のインスタンスの関連付けを行う関数” と全く同じものになります。つまり、フィールド名やクラス名に関してはモデルクラスの定義によって変更する必要がありますが、関連付け可能なインスタンスの個数が 1
となるインスタンスからの関連付けに関しては、1対1のリレーションにおいても多対1のリレーションにおいても同様の手順で実施可能です。
また、Club
のインスタンスから Student
のインスタンスの関連付けを実施することも可能です。ただし、Club
のインスタンスに関連付け可能な Student
のインスタンスの個数は 多
であるため、Student
にリレーションフィールドを定義することによって Club
のインスタンスに追加されるデータ属性は student_set
ということになります。そして、このデータ属性の名称の通り、このデータ属性では集合を扱うことになるため、関連付けは add
メソッドを実行して実施する必要があります。
ということで、Club
のインスタンスから Student
のインスタンスの関連付けを実施するビューの関数の例は下記のようなものとなります。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# soccerにtaroを関連付け
soccer.student_set.add(taro)
return HttpResponse(str(taro))
ここまで紹介してきた例とは利用するデータ属性の名称や関連付けの行い方が異なりますが、基本的にやってることは同じですし、やってることも難易度は高くないと感じられたのではないかと思います。
重要なのは、ここまで説明してきた通り、関連付け可能なインスタンスの個数が 1
であるか 多
であるかを意識しながら実装することだと思います。これが 多
であれば、複数のインスタンスを管理することが必要であるため、リレーションフィールドの定義によって追加されるデータ属性は集合を扱うものであることになります。そして、これが理解できれば、追加されたデータ属性の名称も、そのデータ属性を利用した関連付けのやり方も自然と自身で導けるようになると思います。
関連付けられたインスタンスを取得する
次は、関連付けられたインスタンスの取得について解説していきます。
下記は、Student
のインスタンスから、そのインスタンスに関連付けられた Club
のインスタンスを取得するビューの関数の例となります。関連付け可能なインスタンスの個数が 1
である場合、そのインスタンスに関連付けられたインスタンスの取得に関しても1対1のリレーションの時と同様の手順で実施可能となります。
from django.http import HttpResponse
from .models import Student
def student_view(request, pk):
# Studentのインスタンスの取得
student = Student.objects.get(pk=pk)
# studentの所属するClubのインスタンスの取得
club = student.club
body = ''
body += '名前:' + student.name + '<br>'
body += '身長:' + str(student.height) + '<br>'
body += '体重:' + str(student.weight) + '<br>'
if club is not None:
body += '所属:' + club.name + '<br>'
return HttpResponse(body)
ただし、Club
のインスタンスから、そのインスタンスに関連付けられた Student
のインスタンスを取得する手順に関しては1対1のリレーションの時とは異なります。これは、Club
のインスタンスに関連付けられる Student
のインスタンスの個数が 多
であるからになります。このため、Club
のインスタンスに関連付けられたインスタンスの取得は、 Club
のインスタンスに追加されたデータ属性 student_set
に対してメソッドを実行させて実施する必要があります。
例えば、プライマリーキーが引数 pk
と一致する Club
のインスタンスに関連付けられた全 Student
のインスタンスの情報を取得し、それらを出力するためには、下記のような処理を行う必要があります。
from django.http import HttpResponse
from .models import Club
def club_view(request, pk):
# Clubのインスタンスの取得
club = Club.objects.get(pk=pk)
body = ''
body += '名前:' + club.name + '<br>'
body += '部員:<br>'
# clubに所属する全Studentのインスタンスの取得
students = club.student_set.all()
# 各Studentのインスタンスの情報を1つ1つ出力
for student in students:
body += '<ul>'
body += '<li>' + student.name + '</li>'
body += '</ul>'
return HttpResponse(body)
ここまで説明してきたように、多対1のリレーションの場合、リレーションフィールドを定義した方のモデルクラスのインスタンスに関しては、1対1のリレーションにおける各モデルクラスのインスタンスと同様にして扱うことが可能です。それに対し、その 関連モデルクラス
のインスタンスに関しては、扱うデータ属性が 相手のモデルクラス名_set
であり、インスタンスの関連付けに関しても、関連付けられたインスタンスの取得に関しても、そのデータ属性にメソッドを実行させて実施する必要があります。
スポンサーリンク
多対多のリレーションの利用
最後に、多対多のリレーションの利用手順について説明していきます。
ManyToManyField
を定義する
多対多のリレーションを利用する場合、リレーションフィールドとしては models.ManyToManyField
のフィールドを定義することになります。すなわち、モデルクラスに models.ManyToManyField
のインスタンスを値とするクラス変数を定義することで、多対多のリレーションが利用できるようになります。
例えば下記のように models.py
で Student
と Club
を定義すれば、これらのインスタンスの間には多対多の関連付けを行うことができるようになります。
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)
height = models.FloatField()
weight = models.FloatField()
clubs = models.ManyToManyField(Club, null=True)
ManyToManyField
のコンストラクタに関しては、引数 on_delete
の指定が不要です。
また、多対多のリレーションにおいては、関連付け可能なインスタンスの個数は両方のモデルクラスのインスタンスともに 多
となります。なので、リレーションフィールドを定義することで追加されるデータ属性は、両方のモデルクラスのインスタンスにおいて集合を扱うものとなります。
関連モデルクラス
側に関しては、自動的に 相手のモデルクラス名_set
という集合と分かりやすいデータ属性が追加されますが、リレーションフィールドを定義した方のモデルクラスにおいては、定義したフィールド名そのままのデータ属性が追加されることになるため、そのフィールド名には「集合を扱うものであること」or「複数のインスタンスを扱うものであること」が分かるような名前を採用するとよいと思います。上記においては、そのフィールド名には clubs
という複数形の単語の名前を付けており、複数のインスタンスを扱うことが分かりやすいようにしています。
インスタンス同士の関連付けを行う
次はインスタンスの関連付けを実施していきましょう!
Student
のインスタンス、Club
のインスタンスともに、関連付け可能なインスタンスの個数が 多
であるため、add
メソッドを利用して関連付けを行うことになります。なので、利用するデータ属性名等は異なりますが、Student
のインスタンスから Club
のインスタンスを関連付ける時も、Club
のインスタンスから Student
のインスタンスを関連付ける時も、基本的には手順は同様になります。
まず、Student
のインスタンスから Club
のインスタンスの関連付けを行うビューの関数の例は下記となります。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンス生成
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# taroにsoccerを関連付け
taro.clubs.add(soccer)
return HttpResponse(str(taro))
また、Club
のインスタンスから Student
のインスタンスの関連付けを行うビューの関数の例は下記となります。
from django.http import HttpResponse
from .models import Student, Club
def create_view(request):
# Studentのインスタンスの新規登録
taro = Student.objects.create(
name='taro',
height=172.6,
weight=72.1,
)
# Clubのインスタンスの取得
soccer = Club.objects.get(name='soccer')
# soccerにtaroを関連付け
soccer.student_set.add(taro)
return HttpResponse(str(taro))
関連付けられたインスタンスを取得する
次は、関連付けられたインスタンスの取得について解説していきます。
この取得に関しても、関連付けのとき同様に、Student
のインスタンスから Club
のインスタンスを取得する時も、Club
のインスタンスから Student
のインスタンスを取得する時も、基本的には手順は同様になります。
まず、Student
のインスタンスに関連付けられた Club
のインスタンスの取得を行うビューの関数の例は下記となります。
from django.http import HttpResponse
from .models import Student
def student_view(request, pk):
# Studentのインスタンスの取得
student = Student.objects.get(pk=pk)
body = ''
body += '名前:' + student.name + '<br>'
body += '身長:' + str(student.height) + '<br>'
body += '体重:' + str(student.weight) + '<br>'
# studentの所属するClubのインスタンスの取得
clubs = student.clubs.all()
# 各Clubのインスタンスの情報を1つ1つ出力
for club in clubs:
body += '<ul>'
body += '<li>' + club.name + '</li>'
body += '</ul>'
return HttpResponse(body)
また、Club
のインスタンスに関連付けられた Student
のインスタンスの取得を行うビューの関数の例は下記となります。
from django.http import HttpResponse
from .models import Club
def club_view(request, pk):
# Clubのインスタンスの取得
club = Club.objects.get(pk=pk)
body = ''
body += '名前:' + club.name + '<br>'
body += '部員:<br>'
# clubに所属する全Studentのインスタンスの取得
students = club.student_set.all()
# 各Studentのインスタンスの情報を1つ1つ出力
for student in students:
body += '<ul>'
body += '<li>' + student.name + '</li>'
body += '</ul>'
return HttpResponse(body)
ここまで説明してきたように、多対多のリレーションの場合、リレーションフィールドを定義した方のモデルクラスのインスタンスに関しても、その 関連モデルクラス
においても、追加されるデータ属性は集合を扱うものであり、メソッドの実行によってインスタンスの関連付けや、関連付けられたインスタンスの取得を行うことになります。
集合を扱うという難しさはあるかもしれませんが、両方のモデルクラスで共通のやり方でインスタンスの関連付けも関連付けられたインスタンスの取得も実施できるため、実装の難易度は、多対1のリレーションに比べれば低いと思います。
リレーションの詳細
ここまで、リレーションの基本的な事柄について説明してきました。
実例なども通して、各リレーションの種類や、インスタンス同士の関連付けの手順、さらには関連付けられたインスタンスの取得の手順についても、理解していただけたのではないかと思います。
次は、リレーションについて、もう少し詳細な部分の解説を行っていきます。このあたりを理解すれば、ウェブアプリでリレーションを上手く使いこなすことができるようになると思います。
前述で、リレーションフィールドを定義することで、その定義先のモデルクラス、さらには、そのフィールドの第1引数に指定したモデルクラス(関連モデルクラス
)にデータ属性が追加されると説明しました。
その追加されるデータ属性の名称は データ属性の名称 で解説した通りで、前者に追加されるデータ属性は、定義したフィールドのフィールド名と同じものになります。なので、このデータ属性は、開発者自身が決定できることになります。
それに対し、後者、すなわち 関連モデルクラス
に追加されるデータ属性の名称は、関連付けの相手となるモデルクラスの名称に基づいて自動的に決定されることになります。具体的には、1対1のリレーションの場合は 相手のモデルクラス名
となり、それ以外の場合は 相手のモデルクラス名_set
となります(相手のモデルクラス名
は全て小文字となります)。
今までは、これらのデータ属性としては自動的に決定された名称のものを利用してきましたが、実は、これらの 関連モデルクラス
に追加されるデータ属性の名称は変更可能です。この 関連モデルクラス
に追加されるデータ属性の名称変更は related_name
を利用することで実現可能です。
related_name
とは
この related_name
は、リレーションフィールド、より具体的には、下記の3つのフィールドのコンストラクタに指定可能な引数となります。
OneToOneField
(1対1のリレーション)ForeignKey
(多対1のリレーション)ManyToManyField
(多対多のリレーション)
この related_name
引数を指定することにより、前述の通り 関連モデルクラス
に追加されるデータ属性の名称を設定することが可能となります。
例えば、下記のように Club
と Student
を定義した場合、related_name
引数を指定していないため、Club
のインスタンスに追加されるデータ属性は 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)
clubs = models.ManyToManyField(Club)
なので、Club
からのインスタンスの関連付けや、関連付けされたインスタンスの取得は、このデータ属性 student_set
を利用して実施する必要があります。
# student:Studentのインスタンス
# club:Clubのインスタンス
club.student_set.add(student)
それに対し、下記のように Club
と Student
を定義した場合、Club
のインスタンスに追加されるデータ属性は、related_name
引数で指定された students
となります。
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)
clubs = models.ManyToManyField(Club, related_name='students')
そのため、Club
からのインスタンスの関連付けや、関連付けされたインスタンスの取得は、このデータ属性 students
を利用して実施することになります。
# student:Studentのインスタンス
# club:Clubのインスタンス
club.students.add(student)
同一の 関連モデルクラス
に対するフィールドが複数定義可能
この related_name
引数を指定することで、まず自分自身でデータ属性の名称を決定できるというメリットが得られます。
また、メリットがあるだけではなく、この related_name
の指定が必須となる場合もあります。具体的には、特定のモデルクラスから同一の 関連モデルクラス
に対するリレーションフィールドを複数定義する場合、この related_name
の指定が必須となります。
この理由について、Twitter でのツイートを例に考えていきたいと思います。
まず、ツイートはユーザーから行うもので、ユーザーはツイートを何回も行うことが可能です。つまり、ユーザーと関連付けられるツイートの個数は 多
です。それに対し、1つのツイートの投稿者(ユーザー)は一人のみなので、ツイートに関連付けられるユーザー数は 1
ということになります。
つまり、このようなユーザーとコメントとの関連付けは、下記のような User
と Tweet
を定義することで実現可能です。このように定義することによって User
のインスタンスには tweet_set
というデータ属性が追加され、このデータ属性で Tweet
のインスタンスとの関連付けを行うことができるようになります。そして、これによりユーザーがツイートしたツイートを管理することができるようになります。
from django.db import models
class User(models.Model):
略
class Tweet(models.Model):
text = models.CharField(max_length=256)
tweet_user = models.ForeignKey(User, 略)
次は、これらのモデルクラスを利用して「いいね!」機能を実現することを考えていきたいと思います。
まず、ユーザーは、複数のツイートに対して「いいね!」することが可能です。つまり、ユーザーに関連付け可能なツイートの個数は 多
です。また、1つのツイートは複数のユーザーから「いいね!」される可能性があります。つまり、ツイートに関連付け可能なユーザーの個数も 多
です。
つまり、「いいね!」機能を実現するためには、下記のような User
と Tweet
を定義することが必要となります。
from django.db import models
class User(models.Model):
略
class Tweet(models.Model):
text = models.CharField(max_length=256)
tweet_user = models.ForeignKey(User, 略)
favo_users models.ManyToManyField(User, 略)
ユーザーがツイートしたツイートを管理し、さらに「いいね!」機能を実現しようとすると、上記のような User
と Tweet
の定義が必要であることは理解していただけたのではないかと思います。ただし、このような定義を行うと、makemigrations
を実行した時点で次のような例外が発生することになります。
sns.Tweet.favo_users: (fields.E304) Reverse accessor for 'sns.Tweet.favo_users' clashes with reverse accessor for 'sns.Tweet.tweet_user'. HINT: Add or change a related_name argument to the definition for 'sns.Tweet.favo_users' or 'sns.Tweet.tweet_user'. sns.Tweet.tweet_user: (fields.E304) Reverse accessor for 'sns.Tweet.tweet_user' clashes with reverse accessor for 'sns.Tweet.favo_users'. HINT: Add or change a related_name argument to the definition for 'sns.Tweet.tweet_user' or 'sns.Tweet.favo_users'.
なぜ、このような例外が発生するかというと、それは、上記のように Tweet
の tweet_user
と favo_user
を定義することによって User
のインスタンスに追加されるデータ属性の名称が共に tweet_set
になってしまうからです。つまり、同じ名称のデータ属性になるため、そのデータ属性がユーザーがツイートしたツイートを管理するためのものであるか、ユーザーが「いいね!」したツイートを管理するためのものであるかを区別できなくなってしまいます。そのため、上記のような例外が発生してしまうことになります。
ここまで説明してきた通り、リレーションフィールドを定義することで、関連モデルクラス
のインスタンスに関連付け可能な個数が 多
である場合は、相手のモデルクラス名_set
という名前のデータ属性が追加されることになります。したがって、1つのモデルクラスに同一の 関連モデルクラス
に対するリレーションフィールドを複数定義すると、追加されるデータ属性の名称が重複してしまうことになります。
なので、このような例外を防ぐためには、related_name
引数の指定が必須となります。これにより、1つのモデルクラスに同一の 関連モデルクラス
に対するリレーションフィールドを複数定義したとしても、追加されるデータ属性の名称が重複することを避けることができます。
例えば、先ほどの User
と Tweet
の定義の例であれば、下記のように related_name
引数を指定してやれば例外が発生することなく makemigrations
に成功することになります。
from django.db import models
class User(models.Model):
略
class Tweet(models.Model):
text = models.CharField(max_length=256)
tweet_user = models.ForeignKey(User, related_name='tweets', 略)
favo_users models.ManyToManyField(User, related_name='favos', 略)
ウェブアプリを本格的に開発しだすと、上記のような例外を目にする機会は結構多いと思います。このエラーは related_name
の指定によって解決できることを覚えておくと、例外が出ても焦ることなく解決することができると思いますので、この related_name
に関しては是非覚えておきましょう!
スポンサーリンク
テンプレートからの関連付けられたインスタンスの取得
さて、ここまで「関連付けられたインスタンスの取得」の仕方や、その実例をいくつか示してきましたが、これらは主にビューから実施することを前提として解説してきました。また、同様の処理を実装することで、モデルクラスのメソッドの中でも関連付けられたインスタンスの取得を行うことが可能となります。
さらに、この「関連付けられたインスタンスの取得」はテンプレートファイルで実施することも可能です。下記ページで解説しているように、テンプレートファイルではコンテキストにセットされたデータを参照することが可能です。
【Django入門4】テンプレート(Template)の基本このコンテキストにセットされるデータが「リレーションフィールドを定義したモデルクラスのインスタンス」or「その 関連モデルクラス
のインスタンス」であれば、リレーションフィールドの定義によって追加されたデータ属性を利用して、そのインスタンスに関連付けられたインスタンスを取得し、それらの情報を出力することが可能となります。
先ほどの 追加されるデータ属性の名称変更(related_name) で説明した related_name
をフィールドの引数に指定した場合、その related_name
引数に指定した名前のデータ属性がテンプレートファイルでも利用可能となります
また、テンプレートファイルではテンプレートタグ for
を利用することで、集合に含まれるインスタンスの1つ1つの情報を出力するようなことも可能です。さらに、テンプレートファイルにメソッドを参照させることで、そのメソッドを引数なしで実行し、その結果を扱うようなことも可能です。そのため、リレーションフィールドの定義によって追加されたデータ属性に all
メソッドを実行させるようなことも可能です。
したがって、テンプレートファイルでも、ある程度ビューと同様の処理を実現することができ、それによってビューと同様の出力を行うことが可能となります。
例えば、Student
に club = models.ForeignKey(Club, 略)
が定義されており、さらにコンテキストの 'student'
キーに Student
のインスタンスがセットされているのであれば、テンプレートファイルに下記のような記述を行っておくことで、このテンプレートファイルから生成される HTML に、そのインスタンスに関連付けられた Club
のインスタンスの情報を出力することができることになります。
{{ student.club.データ属性 }}
逆に、コンテキストの 'club'
キーに Club
のインスタンスがセットされているのであれば、テンプレートファイルに下記のような記述を行っておくことで、このテンプレートファイルから生成される HTML に、その Club
のインスタンスに関連付けられた Student
の全インスタンスの情報を出力することができることになります。
{% for student in club.student_set.all %}
{{ student.データ属性 }}
{% endfor %}
こんな感じで、テンプレートファイルからも、特定のインスタンスに関連付けられたインスタンスの取得及び、そのインスタンスの情報の出力が可能であることは是非覚えておいてください。情報の出力のみを行うのであれば、基本的にはテンプレートファイルでこれらの取得を行うようにしてやれば良いと思います。
フォームからの関連付け相手の選択
また、関連付けの相手をフォームから選択できるようにする方法も知っておくと便利だと思います。
例えば、掲示板アプリでコメントを投稿するような時に、コメント投稿フォームで投稿者となるユーザー名を直接入力するのではなく、下の図のような、既に登録済みのユーザー一覧が表示されるプルダウンメニューを用意し、そこから投稿者となるユーザーが選択できれば便利ですよね!
フォームクラスでのプルダウンメニューの実現
このようなフィールドは、下記ページで解説しているフォームクラスに対して ModelChoiceField
のフィールドを定義することで実現できます。この ModelChoiceField
は特定のモデルクラスのインスタンスの候補を選択するためのプルダウンメニューを実現するフィールドになります。このプルダウンメニューがクリックされた際に表示されるインスタンスの候補は、ModelChoiceField
の queryset
引数にインスタンスの集合(クエリーセット)を指定することで設定可能です。
掲示板アプリでのコメント投稿フォームを例に、上記のようなフォームの実現手順についてもう少し具体的に説明していきます。
まず、モデルクラスとして下記のような User
と Comment
が定義されているとしましょう。user = models.ForeignKey(User,略)
が定義されているので、これらのインスタンス同士は関連付けが可能ということになります。
from django.db import models
class User(models.Model):
name = models.CharField(max_length=32)
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
text = models.TextField(max_length=256)
このようなモデルクラスの定義において、Comment
のインスタンスの新規登録用のフォームで投稿者を登録済みの User
の全インスタンスの中からプルダウンメニューで選択できるようにしたいのであれば、下記のような CommentForm
を定義してやれば良いことになります。
from django import forms
from .models import User
class CommentForm(forms.Form):
user = forms.ModelChoiceField(
queryset=User.objects.all()
)
text = forms.CharField()
この CommentForm
のインスタンスをページに表示してやれば、text
フィールドは単なる文字列入力用のフィールドとなりますが、user
フィールドは ModelChoiceField
なのでプルダウンメニューとして表示されることになります。そして、そのメニューに表示されるのは queryset
で指定された集合に含まれるインスタンスとなり、上記の場合は User.objects.all()
を指定しているため、User
のテーブルに存在する全インスタンスとなります。all()
ではなく filter()
を利用すれば、特定の条件を満たすレコードのみをプルダウンメニューに表示することも可能です。
また、このプルダウンメニューに表示されるのは、queryset
で指定された集合に含まれる各インスタンスの出力結果となります。この出力結果は、デフォルトでは下記のような形式の文字列となります。
モデルクラス名 object (プライマリーキーの値)
ハッキリ言ってこれだと分かりにくいので、モデルクラスに __str__
メソッドを実行してインスタンスの出力結果を変更してやった方が良いです。例えば上記の例であれば、User
に __str__
メソッドを定義して name
フィールドを出力するようにしてやれば、プルダウンメニューにユーザー名の一覧が表示されるようになって使いやすくなると思います。__str__
メソッドについては下記ページで解説を行なっていますので、詳しく知りたい方は下記ページをご参照ください。
あとは、フォームからデータが送信されてきた際に、そのデータから user
フィールドの値、つまり利用者が選択した User
のインスタンスを受け取り、それを新規に登録する Comment
のインスタンスに関連付けてやれば、その登録した Comment
のインスタンスの投稿者がデータベースで管理できることになります。
例えば、フォームからデータを受信するビューの関数として下記のような関数を用意しておけば、コメント投稿フォームからデータを受信したときに、user
フィールドの値と新規に登録する Comment
のインスタンスとの関連付けが行われることになります。
def post(request):
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = Comment()
comment.user = form.cleaned_data.get('user')
comment.text = form.cleaned_data.get('text')
comment.save()
return redirect('comments')
else:
form = CommentForm()
context = {
'form': form
}
return render(request, 'relation/post.html', context)
このように、リレーションを利用する場合、フォームは関連付け相手となるインスタンスをユーザーに選択してもらうためのインターフェースとしても使えて便利です。
また、単なるフォームクラスではなく、モデルフォームクラスを利用すれば、実は上記で紹介したような ModelChoiceField
でフィールドを定義しなくても、リレーションフィールドを持たせたモデルクラスをベースとするフォームクラスを定義するだけで、ここで説明したことと同様のことが実現できるようになります。このモデルフォームクラスについては、次の連載の下記ページで解説していますので、リレーションについて理解した後にでも是非読んでみてください!
リレーション利用時の注意点
ここからは、リレーションを利用する時の注意点について解説していきます。
スポンサーリンク
プライマリーキー確定後に関連付けを行う必要がある
まず、最初の注意点が、この節の題名の通り、インスタンスの関連付けはプライマリーキーが確定してから実施する必要があるという点になります。もう少し正確に言えば、インスタンスの関連付けをデータベースに反映するときにプライマリーキーが確定している必要があります。
この点について、下記のような Club
と Student
が定義されていることを前提に、例を示しながら詳細を解説していきたいと思います。
from django.db import models
class Club(models.Model):
name = models.CharField(max_length=32)
class Stundent(models.Model):
name = models.CharField(max_length=32)
club = models.ForeignKey(Club, on_delete=models.CASCADE, null=True)
例えば下記は、Student
と Club
のインスタンスの関連付けを行う処理の例となります。この処理の場合は、正常にインスタンスの関連付けが実施できることになります。
# インスタンス生成
taro = Student.objects.create(name='taro')
soccer = Club.objects.create(name='soccer')
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save()
プライマリーキーが確定していない状態での =
による関連付け
ですが、上記を次のように書き換えた場合は例外が発生することになります。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
taro.save() # 例外発生!
soccer.save()
例外が発生するのは taro.save()
の箇所で、発生する例外は下記のようなものになります。
ValueError: save() prohibited to prevent data loss due to unsaved related object 'club'.
上記の2つの例の大きな違いは、前者が モデルクラス名.objects.create()
でインスタンスを生成しているのに対し、後者が モデルクラス名()
でインスタンスを生成しているという点になります。どちらに関してもインスタンスの成功には成功するのですが、後者の場合のみ taro.save()
実行時に例外が発生することになります。なぜ、後者の場合のみ例外が発生してしまうのでしょうか?
この答えは、後者の場合、taro.save()
が実行されるタイミングで Student
に関連付けられようとしている Club
のインスタンスのプライマリーキーが確定していないからになります。逆に、前者の場合、taro.save()
が実行されるタイミングで Club
のインスタンスのプライマリーキーが確定しているため、例外が発生しません。
じゃあ、なぜプライマリーキーが確定していないと taro.save()
時点で例外が発生することになるのでしょうか?
これに関しては、テーブルで考えると分かりやすいと思います。まず、Student
と Club
をテーブルとして表すと下図のようになります。ここで注目していただきたいのが、Stundent
の club_id
のフィールド(カラム)になります。このフィールドは、Student
の club
フィールドに対応するもので、このフィールドに Club
のプライマリーキーを格納することで、そのインスタンスに関連付けられた Club
のインスタンスを管理できるようになっています。
つまり、Club
のインスタンスとの関連付けを行った Stundent
のインスタンスをテーブルに保存しようと思うと、club_id
のフィールドに格納するための Club
のインスタンスのプライマリーキーが必要ということになります。なので、Stundent
のインスタンスに Club
のインスタンスを関連付けるのであれば、その Club
のインスタンスはプライマリーキーが確定していないといけません。プライマリーキーが確定していない状態の Club
のインスタンスと関連付けた場合、club_id
のフィールドに格納する値が不定となり、そのような Stundent
のインスタンスをデータベースに保存しようとすると例外が発生することになります。
その一方で、Club
のテーブルには関連付けられた Stundent
のインスタンスのプライマリーキーを格納するようなフィールドは存在しないため、Stundent
のインスタンスのプライマリーキーが確定していなくても、そのインスタンスに関連付けられた Club
のインスタンスは問題なくテーブルに保存することが可能です。
例外の解消方法
このプライマリーキーが確定するのは、基本的にはそのインスタンスがレコードとしてデータベースに保存されたタイミングになります。
また、モデルクラス名.objects.create()
を実行した場合、インスタンスの生成が行われるときに、そのインスタンスがレコードとしてデータベースに保存されることになります。なので、この時点で、そのインスタンスのプライマリーキーが確定することになります。
それに対し、モデルクラス名()
を実行した場合、単にインスタンスが生成されるだけになります。なので、この時点ではプライマリーキーは確定しません。そして、前述の通り、このプライマリーキーが確定するのは、そのインスタンスをデータベースに保存したタイミングとなりますので、モデルクラス名()
で生成したインスタンスに関しては、基本的には save
メソッドを実行させるまではプライマリーキーが確定しないことになります。
ここで、前述で示した例外が発生する方のスクリプトの処理の流れを確認してみると、 まず taro
と soccer
を生成したのち、taro.club = soccer
での関連付けが行われることになります。ただし、この時点ではデータベースの更新は行われないため、この taro.club = soccer
は成功することになります。ですが、その次の taro.save()
を実行して taro
をデータベースに保存するときに、ここまで説明した理由により例外が発生することになってしまいます。
ただ、この例外の解決方法は単純で、Stundent
のインスタンスに関連付ける Club
のインスタンスのプライマリーキーを先に確定させてから Student
のインスタンスの保存を行うようにするだけで例外が解決できることになります。具体的には、Club
のインスタンスの保存を先に行ってから、Student
のインスタンスの保存を行えば良いです。
つまり、下記のように taro.save()
の前に soccer.save()
を実行するようにすれば例外が解消します。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# taroにsoccerを関連付け
taro.club = soccer
# データベースに反映
soccer.save()
taro.save()
もちろん、taro.club = soccer
の前に soccer.save()
を実行しても良いですし、最初に見せた例のように モデルクラス名.objects.create()
を実行してインスタンス生成時にデータベースへの保存も一緒に行うようにするのでもオーケーです。
また、ここではリレーションフィールドが ForeignKey
である場合、すなわち多対1のリレーションの場合の =
演算子での参照による関連付けの注意点を説明しましたが、リレーションフィールドが OneToOneField
である場合も同様に =
演算子での参照による関連付けを行うことになるため、同じことに注意が必要となります。
プライマリーキーが確定していない状態での add
による関連付け
次は、先ほどと同じ定義の Student
と Club
を用い、今度は Club
から Student
の関連付けを行うときの処理について考えていきたいと思います。例えば、下記のような処理を実行した場合、先ほどと同様に例外が発生することになります。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# soccerにtaroを関連付け
soccer.student_set.add(club) # 例外発生!
# データベースに反映
taro.save()
soccer.save()
発生する例外は下記のようなものになります。例外のメッセージは異なりますが、原因は先ほどと同様で、結局はインスタンスのプライマリーキーが確定していないのに、関連付けを行っていることが原因となります。
ValueError: "<Club: Club object (None)>" needs to have a value for field "id" before this relationship can be used.
で、この add
メソッドでの関連付けの場合は、先ほどの =
演算子での関連付けの時とは異なり、関連付けしたタイミングで例外が発生することになります。これは、インスタンスの関連付け で説明したように、add
メソッドによる関連付けを実行した場合、その add
メソッドが実行されたタイミングで、インスタンスのデータベースへの保存が行われることになるからです(リレーションフィールドのみが更新される)。
さらに、この add
メソッドでの関連付けの場合は、基本的には関連付けする2つのインスタンスの両方のプライマリーキーが確定していることが必要となります。
add
メソッドがこのような仕様になっているのは、おそらく多対多のリレーションでの add
メソッドとの処理を共通にするためだと思います
多対多のリレーションでの add
メソッド実行時の処理については、次の 多対多のリレーションでの関連付け で解説します
そのため、両方のインスタンスをデータベースに保存してから add
メソッドでの関連付けを行うようにする必要があります。今回の Student
と Club
の定義の場合、先ほどの処理を下記のように変更すれば例外が発生しないようになります。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# データベースに反映
soccer.save()
taro.save()
# soccerにtaroを関連付け
soccer.student_set.add(club)
また、下記のように add
メソッドに bulk=False
を指定すれば、関連モデルクラス
側のインスタンス、すなわち Club
のインスタンスさえプライマリーキーが確定していれば add
メソッドが正常終了するようになります。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# データベースに反映
soccer.save()
# soccerにtaroを関連付け
soccer.student_set.add(club, bulk=False)
後者の方法が有効なのは、リレーションフィールドに対して null=True
が指定されていない場合になります。null=True
が指定されていないフィールドは必須フィールドとして扱われ、そのフィールドが空の状態でのインスタンスのデータベースへの保存が不可となります。
で、そのフィールドがリレーションフィールドの場合、関連付けを行った後でなければデータベースへの保存が不可ということになります。ですが、前述の通り、その関連付けを行う前には、プライマリーキーを確定させるために、そのリレーションフィールドを持つインスタンスの保存を事前に行っておく必要があります。つまり、関連付けを行うためにインスタンスの保存が必要なのに、関連付けを行わないとインスタンスの保存が実施できないという八方ふさがりの状態になってしまいます。
その抜け道として、bulk=False
を指定することで、リレーションフィールドを持つ方のインスタンスに関してはプライマリーキーが確定しない状態でも add
メソッドでの関連付けが実施できるようになっています。ただ、ちょっとややこしいので、bulk=False
を指定するよりかは、リレーションフィールドに対しては null=True
を指定した方が楽だと思います。
多対多のリレーションでの関連付け
さて、先ほどはリレーションフィールドが ForeignKey
の場合、すなわち多対1のリレーションの場合の add
メソッドによる関連付けについて説明しましたが、リレーションフィールドが ManyToManyField
である場合、すなわち多対多のリレーションの場合も add
メソッドによる関連付けを行うことになるため、基本的には同じことに注意が必要となります。
ただ、若干注意すべき点が異なるので、その点について説明していきます。
まず、add
メソッドによる関連付けを行うタイミングで、関連付けする2つのインスタンスの両方のプライマリーキーが確定している必要がある点は共通となります。ただ、リレーションの種類 で説明したように、ManyToManyField
の場合は OneToOneField
や ForeignKey
の場合とは異なり、リレーションフィールドを定義しても、そのフィールド(カラム)は定義先のモデルクラスのテーブルには追加されません。代わりに関連付けの管理専門のテーブルが追加され、そのテーブルでインスタンス同士の関連付けが管理されることになります。
このテーブルには、リレーションフィールドを定義したモデルクラスのインスタンスのプライマリーキーを管理するフィールドと、その 関連モデルクラス
のインスタンスのプライマリーキーを管理するフィールドが存在します。そして、add
メソッドを実行することで、関連付けられる2つのインスタンスのプライマリーキーが各フィールドに格納されたレコードが新規登録されることになります。なので、フィールドやテーブルの構成は異なるものの、結局多対多のリレーションにおいても add
メソッドを実行するタイミングでは関連付けられる2つのインスタンスのプライマリーキーが確定している必要があることになります。
それに対し、add
メソッドへの bulk=False
の指定は、多対多のリレーションにおいては基本的に不要となります。先ほども説明したように、ManyToManyField
の場合はリレーションフィールドを定義しても、そのフィールド(カラム)は定義先のモデルクラスのテーブルには追加されません。したがって、null=True
の指定の有無に関わらず、ManyToManyField
を定義したモデルクラスのインスタンスに関しては add
メソッドでの関連付けを行う前にデータベースへの保存が可能となります。
なので、多対多のリレーションの場合は、add
メソッドを実行する前に、単にプライマリーキーが確定していないインスタンスに対して save
メソッドを実行するようにするだけで例外の発生を防止することが可能となります。
例えば、Student
に clubs = ManyToManyField(Club)
が定義されている場合、下記の処理は、ManyToManyField
への引数 null=True
の指定の有無に関わらず成功することになります。
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')
# データベースに反映
soccer.save()
taro.save()
# soccerにtaroを関連付け
soccer.student_set.add(club)
1対多のリレーションの場合は、リレーションフィールドへの null=True
の指定の有無によって add
メソッドへの引数を変更する必要がありましたが、多対多のリレーションの場合は、それが不要になるため、多対多のリレーションの方が実装が楽になります。
ちなみに、一点補足しておくと、ManyToManyField
の場合、引数 null=True
を指定すると、ウェブアプリ起動時等に下記のような警告が発生することになります。
WARNINGS: アプリ名.Student.club: (fields.W340) null has no effect on ManyToManyField.
引数 null=True
を指定することで、そのフィールドが空の状態のインスタンスをデータベースに保存することができるようになるのですが、ここまで説明してきたように ManyToManyField
の場合は、そのフィールドを定義したモデルクラスのテーブルにはフィールドが追加されないため、そのフィールドが空であろうがなかろうが関係なくデータベースに保存可能です。そのため、引数 null=True
を指定しても意味がなく、上記のような警告が発生することになります。
別に上記の警告が発生しても正常にウェブアプリは動作するのですが、ManyToManyField
の場合は引数 null=True
の指定が無意味であることは覚えておくとよいと思います。
N + 1
問題の発生に注意
リレーション利用時の2つ目の注意点は「N + 1
問題の発生」になります。
N + 1
問題とは、簡単に言うとウェブアプリのパフォーマンスが低下する問題で、この問題はリレーションを利用した場合に発生する可能性があります。実は、ここまで説明の中で示してきた例の中でも N + 1
問題が発生しているものがあります。
この N + 1
問題と、N + 1
問題の解決方法については Django 入門 の連載における下記ページで解説していきますので、この辺りについては下記ページを読む際に理解していただければと思います!
掲示板アプリでリレーションを利用してみる
では、ここまで説明してきた内容を踏まえて、実際にリレーションの利用例を示していきたいと思います。
この Django 入門 に関しては連載形式となっており、ここでは前回下記ページの 掲示板アプリでモデルを利用してみる で作成したウェブアプリに対してリレーションを導入する形で、リレーションの利用例を示していきたいと思います。
【Django入門6】モデルの基本ここまで開発してきた掲示板アプリでは、モデルクラスとして User
と Comment
の2つを定義しています。ですが、まだリレーションを利用していないため、各モデルクラスのインスタンスは独立したものとなっています。今回、リレーションを利用することで、各モデルクラスの間に関係性を持たせます。
具体的には、Comment
にリレーションフィールドを定義し、Comment
のインスタンスの作成者となる User
のインスタンス、すなわちコメントの投稿者であるユーザーを管理できるようにしていきます。1つのコメントの投稿者は一人のみとし、さらに一人のユーザーは複数のコメントを投稿できるようにするため、今回は Comment
に「関連モデルクラス
を User
とする ForeignKey
のリレーションフィールド」を定義していきます。
スポンサーリンク
掲示板アプリのプロジェクト一式の公開先
この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。
https://github.com/da-eu/django-introduction
また、前述のとおり、ここでは前回の連載の 掲示板アプリでモデルを利用してみる で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。
https://github.com/da-eu/django-introduction/releases/tag/django-model
さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。ソースコードの変更等を行うのが面倒な場合など、必要に応じて下記からプロジェクト一式を取得してください。
https://github.com/da-eu/django-introduction/releases/tag/django-relation
リレーションフィールドの定義
ということで、まずはリレーションフィールドの定義を行なっていきたいと思います。
先ほど説明したように、models.py
を変更し、Comment
に関連モデルクラス
を User
とする models.ForeignKey
のフィールド user
を定義します。
from django.db import models
class User(models.Model):
username = models.CharField(max_length=32)
email = models.EmailField()
age = models.IntegerField()
def __str__(self):
return self.username
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='comments')
text = models.CharField(max_length=256)
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.text[:10]
この user
フィールドの定義により、Comment
のインスタンスからはデータ属性 user
を利用して User
のインスタンスとの関連付け等を行うことが可能となります。Comment
のインスタンスに関連付け可能な User
のインスタンスの個数は 1
であるため、このデータ属性 user
からは =
演算子での参照によって User
のインスタンスとの関連付けを行うことができます。
また、この user
フィールドの定義により、User
のインスタンスからは、related_name
引数で指定した名前のデータ属性、すなわち comments
を利用して Comment
のインスタンスとの関連付け等を行うことが可能となります。User
のインスタンスに関連付け可能な Comment
のインスタンスの個数は 多
であるため、このデータ属性 comments
からは add
メソッドによって Comment
のインスタンスとの関連付けを行うことができます。
フォームの変更
リレーションフィールドの定義が完了したため、次は models.py
以外の変更を行なっていきます。
まずはフォームの変更を行います。
先ほどのリレーションフィールドの定義によって、各コメントの投稿者が管理できるようになったことになります。そのため、下の図のように、コメント投稿フォームにプルダウンメニューを追加し、コメント投稿時に投稿者をフォームから指定できるようにしたいと思います。
この実現方法は フォームからの関連付け相手の選択 で説明した通りで、コメント投稿フォームを実現するフォームクラスに ModelChoiceField
を定義してやれば良いことになります。具体的には、コメント投稿フォームは PostForm
によって実現しているため、下記のように forms.py
を変更して PostForm
に user
フィールドを定義してやれば良いです。
from django import forms
from .models import User
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def check_username(username):
if not username.isalnum() or not username.isascii():
raise ValidationError(_('usernameにはアルファベットと数字のみ入力可能です'))
if not username[0].isalpha():
raise ValidationError(_('usernameの最初の文字はアルファベットにしてください'))
class RegisterForm(forms.Form):
username = forms.CharField(validators=[check_username])
email = forms.EmailField()
age = forms.IntegerField(min_value=0, max_value=200)
class PostForm(forms.Form):
user = forms.ModelChoiceField(queryset=User.objects.all())
text = forms.CharField()
フォームからの関連付け相手の選択 で説明した通り、user
フィールドはフォーム表示時にプルダウンメニューとして表示され、このプルダウンメニューには、ModelChoiceField()
の queryset
に指定した集合に含まれるインスタンスがリストアップされることになります。上記では queryset
に User.objects.all()
を指定しているため、User
のテーブルに登録されている全インスタンスの出力結果がリストアップされることになります。
また、models.py
で User
に __str__
メソッドを定義し、この __str__
メソッドでは username
を返却するようにしているため、インスタンスの出力結果は各インスタンスの username
の値、すなわち各ユーザーの名前となります。なので、プルダウンメニューにも各ユーザーの名前がリストアップされることになります。
スポンサーリンク
ビューの変更
次は views.py
の変更を行なっていきたいと思います。
この views.py
では、Comment
のインスタンスの新規登録時に、User
のインスタンスとの関連付けを実施する処理を追加していきます。
まず、Comment
のインスタンスの新規登録を行うのは views.py
の post_view
関数になります。そして、この post_view
関数では、PostForm
からデータを受信したときに、PostForm
の各種フィールドに入力されたデータから Comment
のインスタンスを生成し、それをデータベースに新規登録するようになっています。
また、先ほどの PostForm
の変更によって、user
フィールドで投稿者となる User
のインスタンスが選択できるようになっており、このフォームから送信されてくるデータには、その選択された User
のインスタンス(のプライマリーキー)も送信されてくるようになっています。
なので、post_view
関数でフォームからデータを受信したときに、Comment
のインスタンスを生成し、そのインスタンスのデータ属性 user
でフォームから送信されてきた User
のインスタンスを参照させれば、Comment
のインスタンスと User
のインスタンスとの関連付けが実現できることになります。
具体的には、views.py
における post_view
を下記のように変更することで、Comment
のインスタンスの新規登録時に、User
のインスタンスとの関連付けが実施されるようになります。
def post_view(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
text = form.cleaned_data.get('text')
user = form.cleaned_data.get('user')
comment = Comment(text=text)
comment.user = user
comment.save()
return redirect('comments')
else:
form = PostForm()
context = {
'form': form,
}
return render(request, 'forum/post.html', context)
掲示板アプリでモデルを利用してみる で示した post_view
との違いは、下記の2行を追加した点のみとなります。つまり、フォームの user
フィールドで選択された User
のインスタンスを受け取り、それと comment
との関連付けを実施するように変更しているだけです。
user = form.cleaned_data.get('user')
comment.user = user
これで、Comment
のインスタンスと User
のインスタンスとが関連付けられるようになり、一方のインスタンスから、そのインスタンスに関連付けられたインスタンスを取得することができるようになります。そして、これにより、User
のインスタンスの情報を出力する時に、そのインスタンスに関連付けられた Comment
のインスタンスの情報を合わせて出力したり、逆に Comment
のインスタンスの情報を出力する時に、そのインスタンスに関連付けられた User
のインスタンスの情報を合わせて出力したりすることが可能となります。
テンプレートの変更
次は、そのような出力が実現できるようにウェブアプリを変更していきたいと思います。今回は、関連付けられたインスタンスの取得、さらには、それらのインスタンスの出力は全て、テンプレートから行うようにしたいと思います。そのため、テンプレートファイルの変更を行なっていきます。
comments.html
の変更
コメント関連のページの基となる comments.html
と comment.html
では、リレーションを利用してコメントの投稿者の情報を表示するように変更を行っていきます。
前述の通り、Comment
のインスタンスと User
のインスタンスが関連付けられたことにより、Comment
のインスタンスから投稿者の User
を取得することができるようになります。そのため、 コメント一覧ページの基となる comments.html
では、各 Comment
のインスタンスから投稿者を取得し、その名前を HTML に出力するようにしていきます。
具体的には、下記のように comments.html
を変更します。
{% extends "forum/base.html" %}
{% block title %}
コメント一覧
{% endblock %}
{% block main %}
<h1>コメント一覧(全{{ comments|length }}件)</h1>
<table class="table table-hover">
<thead>
<tr>
<th>本文</th><th>投稿者</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
<tr>
<td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
{% if comment.user is not None %}
<td>{{ comment.user.username }}</td>
{% else %}
<td>不明</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
ポイントは下記部分で、{{ comment.user.username }}
が comment
と関連付けられた User
のインスタンスのデータ属性 username
、すなわちコメントの投稿者を表示する部分となります。
ただ、comment
が User
のインスタンスと関連付けられていない場合、comment.user
が None
となるため、その場合は comment.user
を使用せずに、単に 不明
と出力するようにテンプレートタグ {% if comment.user is not None %}
を使って条件分岐を行っています。
{% if comment.user is not None %}
<td>{{ comment.user.username }}</td>
{% else %}
<td>不明</td>
{% endif %}
こんな感じで、if
タグを利用すれば、テンプレートファイルでも条件分岐できることは覚えておきましょう!
comment.html
の変更
コメントの詳細ページの基となる comment.html
でも、その Comment
のインスタンスから投稿者を取得し、その名前を HTML に出力するようにしていきます。
具体的には、下記のように comment.html
を変更します。
{% extends "forum/base.html" %}
{% block title %}
コメント
{% endblock %}
{% block main %}
<h1>コメント({{ comment.id }})</h1>
<table class="table table-hover">
{% if comment.user is not None %}
<tr><td>投稿者</td><td><a href="{% url 'user' comment.user.id %}">{{ comment.user.username }}</a></td></tr>
{% else %}
<tr><td>投稿者</td><td>不明</td></tr>
{% endif %}
<tr><td>本文</td><td>{{ comment.text }}</td></tr>
<tr><td>投稿日</td><td>{{ comment.date|date }}</td></tr>
</table>
{% endblock %}
変更点のポイントは comments.html
とほぼ同様ですが、上記の comment.html
では、投稿者の名前に url
タグを利用してそのユーザーの詳細ページへのリンクを貼るようにしています。つまり、このコメントの詳細ページはユーザーの詳細ページにつながっていることになります。このように、リレーションの利用により、今まで独立していたコメントとユーザーの間に繋がりが発生し、ページとページの間にも関連性を持たせやすくなります。
users.html
の変更
今度は、ユーザー関連の情報を表示するページの基となる users.html
と user.html
の変更を行っていきます。
先ほどとは逆に、User
のインスタンスから Comment
のインスタンスの情報を取得し、それを HTML に出力するようにしていきます。
まず、ユーザー一覧ページでは、ユーザー名の横に、そのユーザーのコメント回数を表示するようにしたいと思います。これを実現するため、ユーザー一覧ページの基になる users.html
を下記のように変更します。
{% extends "forum/base.html" %}
{% block title %}
ユーザー一覧
{% endblock %}
{% block main %}
<h1>ユーザー一覧(全{{ users|length }}人)</h1>
<table class="table table-hover">
<thead>
<tr>
<th>ユーザー</th><th>コメント数</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td><a href="{% url 'user' user.id %}">{{ user.username }}</a></td>
<td>{{ user.comments.all|length }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
今までの users.html
に対し、ユーザー一覧の表にコメント数の列を追加しています。コメント数の列の各要素は {{ user.comments.all|length }}
としており、これにより、user
に関連付けられた Comment
の総数、すなわち user
のコメント総数が HTML に出力されることになります(|length
は Django テンプレート言語におけるフィルターとなります)。
また、今回 Comment
に定義したリレーションフィールドには引数 related_name='comments'
を指定しているため、この名前のデータ属性からメソッド(今回の場合は all
)を実行させて関連付けられたインスタンスを取得する必要がある点もポイントとなります。
user.html
の変更
最後にユーザー詳細ページを変更していきます。ユーザー詳細ページでは、ユーザーのコメント履歴一覧、すなわち、User
のインスタンスに関連付けられた全 Comment
のインスタンスの情報を出力するようにしていきたいと思います。そのために、下記のように user.html
を変更します。
{% extends "forum/base.html" %}
{% block title %}
{{ user.username }}
{% endblock %}
{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
<tbody>
<tr><th>名前</th><td>{{ user.username }}</td></tr>
<tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
<tr><th>年齢</th><td>{{ user.age }}</td></tr>
</tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
<tbody>
{% for comment in user.comments.all %}
<tr>
<td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
今までの user.html
に対し、”コメント履歴” の節を追加し、そこにコメント履歴表を表示するように変更しています。
user.comments.all
は user
が投稿した全 Comment
のインスタンスの集合となりますので、テンプレートタグ {% for comment in user.comments.all %}
により user
が投稿した各 Comment
のインスタンス(comment
)に対して for
ループが行われることになります。そして、その中で各 comment
の本文を HTML に出力することで、ユーザーのコメント履歴一覧の表示を実現しています。user
に Comment
のインスタンスが1つも関連付けられていない場合は、user.comments.all
が空となるので、コメント履歴としては何も表示されないことになります。
また、ここでも各コメントの本文にリンクを貼っており、ユーザー詳細ページからコメント詳細ページに遷移することができるようになっています。
動作確認
ソースコードの変更の解説は以上となります。
最後に動作確認を行なっていきましょう!
DB 関連の初期化
今回は、前回から models.py
を変更しているため、そのままマイグレーションを実行するとエラーになる可能性が高いです。そのため、マイグレーションを実行する前に、データベース関連のファイルの削除を行いたいと思います。
ここから紹介する手順でデータベースの初期化を行うとデータベースやマイグレーション用の設定ファイルを完全に初期化することができます。ただ、今まで作成してきたレコードも無くなってしまいますので、この点は注意してください。ただ、一番確実&楽にマイグレーションのエラーを解決する手順ではあるので、練習でウェブアプリを開発していてマイグレーションのエラーがどうしても解決できない場合は、この手順で初期化するのが良いと思います。
まず、ターミナルやコマンドプロンプト等のコマンド実行可能アプリを開きます。そして、プロジェクトフォルダ(testproject
フォルダ)の直下に移動してください。このフォルダには manage.py
が存在するはずで、いつもマイグレーション等のコマンドを実行しているフォルダになります。
さらに、移動後に下記コマンドを実行してデータベースファイルを削除します。
% rm db.sqlite3
続いて、下記コマンドを実行してマイグレーション設定ファイルを削除します。
% rm forum/migrations/00*
以上で、データベース関連のファイルが全て削除されたことになります。
マイグレーションの実行
続いて、マイグレーションを実施していきます。このマイグレーションは、プロジェクトフォルダの直下、つまり、manage.py
が存在するフォルダで下記コマンドを実行することで行うことができます。
% python manage.py makemigrations
% python manage.py migrate
今回は models.py
を変更して Comment
に user
フィールドを追加したため、マイグレーションの実行により、下の図のように Comment
のテーブルに新たなフィールド(カラム)が追加されることになります。そして、この追加されたフィールドには、その Comment
のインスタンスに関連付けられた User
のインスタンスのプライマリーキーが格納されることになります。
開発用ウェブサーバーの起動
マイグレーションが完了した後は、いつも通り Django 開発用ウェブサーバーの起動を行います。マイグレーションを実行した時と同じフォルダで下記コマンドを実行すれば、Django 開発用ウェブサーバーが起動します。
% python manage.py runserver
リレーションの効果の確認
ここからは、今回新たに導入したリレーションの効果を確認していきたいと思います。
まずウェブブラウザを開き、アドレスバーに下記 URL を指定してください。
http://localhost:8000/forum/register/
そうすると、ユーザー登録フォームのページが表示されるはずです。以前の掲示板アプリの動作確認時にユーザーを登録した方がおられるかもしれませんが、今回データベースの初期化を行ったため、再度ユーザー登録が必要となりますので、この登録フォームから複数人のユーザーの登録をしておいてください。
ユーザーの登録が完了したら、次はナビゲーションバーの コメント投稿
リンクをクリックしてください。すると、下の図のようなコメント投稿フォームが表示されるはずです。
今まではコメント投稿フォームに text
フィールドしか表示されていなかったのですが、今回フォームを変更したため、user
フィールドが表示されるようになっているはずです。そして、この user
フィールドはプルダウンメニューになっており、クリックすれば、先ほど登録したユーザーの一覧が表示されます。
まずは、user
フィールドから投稿者となるユーザーを選択してください。その後、text
フィールドに適当なコメントを入力して 送信
ボタンを押してコメントの投稿を行なってみてください。
送信
ボタンを押すと自動的にコメント一覧ページに遷移するはずです。注目していただきたいのが、投稿したコメントの右側に表示されている投稿者欄です。ここには、先ほどコメント投稿フォームで選択した投稿者の名前が表示されているはずです。既に皆さん理解してくださっている通り、ここの投稿者欄には、その Comment
のインスタンスに関連付けられた User
のインスタンスの username
が出力されています。
ポイントは、このページを表示するテンプレートファイル comments.html
から HTML を生成する際に用意したコンテキストには User
のインスタンスがセットされていないにも関わらず、User
のインスタンスの情報が表示されているという点になります。このことは、views.py
の comments_view
関数での下記部分で確認できると思います。
context = {
'comments' : comments
}
コンテキストにセットしているのは Comment
のインスタンスの集合(クエリーセット)のみなのですが、その集合内の Comment
のインスタンスから User
のインスタンスの情報が表示できています。これが可能なのは、リレーションを利用しているからになります。
続いて、先ほど投稿したコメントの本文部分のリンクをクリックしてみてください。そうすると、そのコメントの詳細ページに遷移し、そこに投稿者の名前が表示されているはずです。
次は、この投稿者の名前部分のリンクをクリックしてみてください。これにより、その投稿者の詳細ページが表示されるはずです。さらに、その投稿者の詳細ページには投稿履歴が表示されており、そこに先ほど投稿したコメントが表示されているはずです。
最後にナビゲーションバーの ユーザー一覧
リンクをクリックしてユーザー一覧ページを表示してみましょう!ユーザー一覧の右側にコメント数の列が追加されており、そこに各ユーザーのコメント数が表示されているはずです。
ここまで説明した通りに動作確認をしてくださった方であれば、最初のコメント投稿時に選択したユーザーのコメント数が 1
になっていることが確認できると思います。
コメント投稿を繰り返し行えば、コメント投稿時に選択したユーザーのコメント数が順次増えていくことも確認できると思いますし、そのユーザーの詳細ページでコメント履歴が増えていくことも確認できると思います。
この動作確認でポイントになるのは、コメントから投稿者、さらに投稿者からコメントをリンクで辿れるようになったという点と、投稿者とコメントの情報を一緒に表示できるようになったという点になります。実は、こういった動作は下記ページで開発したような、テーブル同士が独立したウェブアプリでは実現できません。
【Django入門6】モデルの基本こういった動作は、モデルクラスにリレーションフィールドを定義することで、つまりリレーションを導入したことで実現できています。
今回はコメントと投稿者の間で関連性を持たせる例を示しましたが、このリレーションを導入することで実現可能になる機能は非常に多く存在します。例えばツイートとユーザーの間で関連性を持たせることで、Twitter の多くの機能も実現できることはイメージできるのではないかと思います。
愛用しているアプリや、以前に使ったことのあるアプリなどを思い浮かべ、そのアプリでどういったリレーションが利用されているのかを妄想してみると面白いと思いますし、リレーションの理解を深めることもできると思いますので、こういったことにも是非取り組んでみてください!
スポンサーリンク
まとめ
このページでは、Django のリレーションについて解説を行いました!
基本的に、モデルクラスは互いに独立した状態となっています。ですが、このリレーションを導入することで、モデルクラスの間に関連性を持たせることができ、各モデルクラスのインスタンス同士を関連付けることが可能となります。
そして、この関連付けにより、一方のモデルクラスのインスタンスから他方のモデルクラスのインスタンスの情報を取得するようなことが可能となり、これにより様々な機能を実現することが可能となります。
リレーションを利用するためには、モデルクラスに下記のいずれかのリレーションフィールドを定義する必要があります。実現したいウェブアプリに合わせて定義するリレーションフィールドを適切に選択する必要があります。
OneToOneField
:1対1のリレーションForeignKey
:多対1のリレーションManyToManyField
:多対多のリレーション
また、これらのリレーションフィールドを定義することで各インスタンスにデータ属性が追加されることになり、そのデータ属性を利用してインスタンス同士の関連付け等を実施することになります。ポイントは、追加されるデータ属性やデータ属性の利用の仕方が、上記のリレーションフィールドの種類や、リレーションフィールドを定義した方のモデルクラスであるか、その 関連モデルクラス
であるかによって異なるという点になります。そして、これらは、そのデータ属性を利用するインスタンスに関連付け可能なインスタンスの個数が 1
or 多
のどちらであるかによって決まるため、これを意識して実装するという点がリレーションを上手く利用する上で重要になります。
最初はリレーションの利用が難しく感じるかもしれませんが、慣れれば楽々と利用できるようになると思いますので、とにかくいろんなウェブアプリを開発してみて少しずつリレーションに慣れていきましょう!
今回は異なるモデルクラスのインスタンスの関連付けに焦点を当ててリレーションについて説明を行いましたが、同じモデルクラスの2つのインスタンス同士を関連付けるようなことも可能です。これに関しては、下記ページで解説していますので、興味があれば読んでみてください。
【Django】自己参照型リレーションについて分かりやすく解説(同じモデルクラスのインスタンスの関連付け)また、Django 入門 の次の連載(下記ページ)では、モデルフォームについて解説しています。モデルフォームを利用することで、ウェブアプリの開発の効率化を図ることができます!是非下記ページも読んでみてください!
【Django入門8】モデルフォームの基本