【Django入門7】リレーションの基本

Djangoのリレーションの解説ページアイキャッチ

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

このページでは、Django におけるリレーションについて解説していきます!

前回の連載(下記ページ)の中で解説したモデルは、データベースを管理することを役割としています。また、モデルクラスの定義によって、データベースで扱うテーブル(レコードの形式)を定義することができます。

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

また、上記ページでも簡単に説明していますが、モデルクラスのインスタンス同士を関連づけることも可能で、このモデルクラスのインスタンス同士の関連付けのことをリレーションと呼びます。

このページでは、このリレーションについて解説していきます。

リレーションの基本

では、まずはリレーションの基本について解説していきたいと思います。

リレーションとは

前述の通り、リレーションとは「モデルクラスのインスタンス同士の関連付け」のことになります。

下記ページでも解説しているとおり、モデルクラスはデータベースにおけるテーブルの形式、より具体的にはテーブルの持つフィールド(カラム)を定義するクラスとなります。また、モデルクラスのインスタンスは、そのテーブルのレコードとして扱われます。

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

単純にモデルクラスを定義した場合、基本的にはモデルクラスによって定義される各種テーブルは独立したものとなります。

リレーションを利用しない場合、各モデルクラスのテーブルが独立していることを示す図

それに対し、モデルクラスにリレーションフィールドを定義すれば、そのモデルクラスのテーブルに、特定のテーブルのレコードとの関連付けを行うためのフィールド(カラム)が追加されることになりますそして、そのフィールドを利用して、レコードと他のテーブルのレコードとの関連付けを行うことができるようになります。

MEMO

定義するリレーションフィールドの種類によっては、テーブルにフィールド(カラム)が追加されるのではなく、関連付けの情報を管理するための新たなテーブルが追加されることもあります

このあたりは後述で解説していきます

具体的には、テーブルのリレーションフィールドには、関連付けされたレコードのプライマリーキーが格納されることになります。これによって、そのフィールドを持つテーブルのレコードに関連付けされたレコードが管理できるようになります。このページでは、このプライマリーキーが id であることを前提に解説を進めていきますので、この点はご了承ください。

リレーションフィールドの定義によってフィールドが追加され、そのフィールドによって関連付けられたレコードが管理される様子

このような関連付けを行うことで、特定のレコードに関連付けられたレコードそのものや、そのレコードの各種フィールドの値を取得することが可能となります。上の図のようなテーブル構成の場合、Student のテーブルから club_id=3 のレコードを抽出すれば、Clubname=Tennis のレコードに関連付けられた Student の全レコードを取得することができることになります。これによって、例えば Tennis 部に所属する学生をリストアップする機能が実現できることになります。

リレーションを利用して特定のクラブに所属する生徒のみをピックアップする様子

もちろん、他の Club のレコードに関連付けられている Student のレコードを抽出することで、他のクラブに所属している生徒をリストアップするようなことも可能です。つまり、上の図のようにレコードの関連付けを行っておくことで、各クラブに所属している生徒をリストアップする機能が実現できることになります。

重要なのは、このような機能は異なるテーブルのレコードを関連付けさせているから実現可能であるという点になります。各テーブルが独立していると、このような機能は実現できません。例えば、下の図のようなテーブルでレコードを管理している場合、どの生徒がどのクラブに所属しているかが判断できません。

リレーションを利用しない場合、各モデルクラスのテーブルが独立していることを示す図

世の中のウェブアプリでは、このようなレコード同士の関連付け、すなわちリレーションを利用して様々な魅力的な機能が実現されています。例えば Twitter の例で考えると、このアプリではツイートとユーザーの間で関連付けが行われているため、ツイートから「ツイート元のユーザー」を特定して表示するようなことが可能となっていると予想できます。また、ユーザーから「ユーザーのツイート一覧」を表示するようなことも可能です。このような機能は、リレーションを利用しているからこそ実現可能な機能となります。

リレーションを利用してユーザーのツイート履歴を表示する様子

他のウェブアプリにおいても、リレーションによって実現されている機能が存在することは容易に想像がつくのではないかと思います。このように、リレーションによってウェブアプリで管理するデータ同士を関連付けしておくことで、様々な機能を実現することが可能となります。

ここまでの解説では「2つの異なるモデルクラスのインスタンス」に対する関連付けについて説明してきましたが、実は「同じモデルクラスの2つのインスタンス」の間で関連付けを行うことも可能です。これについては別途下記ページで解説していますので、詳しく知りたい方は、後で別途下記ページを参照していただければと思います。

Djangoにおける自己参照型リレーションの解説ページアイキャッチ 【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 が指定されているため、ClubStudent に対する 関連モデルクラス ということになります。そのため、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引数に指定するモデルクラスの定義が、リレーションフィールドを定義するモデルクラスよりも下側に定義されている場合は、モデルクラスの名称の文字列を指定する必要があります。

文字列での第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対1のリレーションはモデルクラスに OneToOneField のフィールドを定義することで実現できます。

この OneToOneField のフィールドを定義したモデルクラスのテーブルには、その 関連モデルクラス のインスタンスのプライマリーキーを格納するためのフィールド(カラム)が追加されることになります。そして、そのフィールドに格納可能なプライマリーキーは1つのみとなります。そのため、OneToOneField のフィールドを定義したモデルクラスのインスタンスと関連付け可能な 関連モデルクラス のインスタンスの個数は となります。

OneToOneFieldの特徴を示す図1

また、このテーブルに追加されたフィールドでは、各インスタンス間での値の重複が禁止されます。そのため、関連モデルクラス の各インスタンスに関連付け可能なインスタンスの個数も ということになります。

OneToOneFieldの特徴を示す図2

ClubStudent の例で考えれば、Student のインスタンスには1つの Club のインスタンスのみが関連付け可能で、さらに各 Student のインスタンスの間で関連付けられる Club のインスタンスに重複があってはいけないということになります。

つまり、このようなモデルクラスの構成においては、各学生が所属できるクラブは1つのみで、さらにクラブに所属できる学生も一人のみということになります。通常、クラブには複数の学生が所属できるはずなので、少し妙な「各クラブに所属する生徒の管理」となってしまいますね。なので、「各クラブに所属する生徒の管理」を実現するためには1対1のリレーションは不適切ということになります。

多対1のリレーション

次は、多体1のリレーションについて解説していきます。

多対1のリレーションとは、”リレーションフィールドを定義したモデルクラス” のインスタンスと関連付け可能な 関連モデルクラス のインスタンスの個数が 、かつ、関連モデルクラス のインスタンスと関連付け可能な “リレーションフィールドを定義したモデルクラス” のインスタンスの個数が となる関連付けになります。

多対1のリレーションの説明図

この場合、上の図のように、複数の Student のインスタンスが1つの Club のインスタンスに関連付け可能となるため、多対1のリレーションということになります。

前述の通り、この多対1のリレーションはモデルクラスに ForeignKey のフィールドを定義することで実現できます。

この ForeignKey のフィールドを定義したモデルクラスのテーブルには、その 関連モデルクラス のインスタンスのプライマリーキーを格納するためのフィールド(カラム)が追加されることになります。そして、そのフィールドに格納可能なプライマリーキーは1つのみとなります。そのため、ForeigneKey のフィールドを定義したモデルクラスのインスタンスと関連付け可能な 関連モデルクラス のインスタンスの個数は となります。この点に関しては OneToOneField の場合と同様となります。

ForeignKeyの特徴を示す図1

また、このテーブルに追加されたフィールドでは、各インスタンス間で値が重複することが許されています。ここが ForeigneKey と OneToOneField との違いになります。そして、このために、関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数は  ということになります。例えば下の図の場合、Clubid=2 のインスタンスは2つの Student のインスタンスと関連付けられていることになります。

ForeignKeyの特徴を示す図2

ClubStudent の例で考えると、クラブの掛け持ちが不可な「各クラブに所属する生徒の管理」が実現できることになります。

多対1のリレーションの場合、ForeigneKey のフィールドをどちらのモデルクラスに定義するのかによって関連付けの意味合いが大きく変化することに注意してください。例えば、Club 側に ForeigneKey のフィールドを定義した場合、一人の学生が複数のクラブに所属できるものの、各クラブに所属可能な生徒数は1人のみとなってしまいます。

ForeignKeyのフィールドの定義先を逆にしたときの多対1のリレーションの説明図

このように、多対1のリレーションにおいては、2つのモデルクラスのうちのどちらに ForeigneKey のフィールドを定義するのかが非常に重要となりますので、この点に注意しながらモデルクラスの定義を行うようにしましょう!ForeigneKey のフィールドを定義したモデルクラス側のインスタンスが、関連付け可能なインスタンスの個数が となります(つまり、多対1の関係における  側になります)。

多対多のリレーション

最後に多対多のリレーションについて解説します。

多対多のリレーションは、2つのモデルクラス両方のインスタンスに関連付け可能なインスタンスの個数が となるリレーションとなります。お互いのインスタンスに関連付け可能なインスタンスの数が複数となるため、多対多の関係のリレーションとなります。

多対多のリレーションの説明図

前述の通り、この多対多のリレーションはモデルクラスに ManyToManyField のフィールドを定義することで実現できます。

この多対多のリレーションは、特にテーブルの観点で考えると少し特殊で、モデルクラスに ManyToManyField のフィールドを定義することで、各インスタンス間の関連付けを管理する新たなテーブルが追加されることになります。このテーブルには、そのテーブルの id に加えて2つのフィールド(カラム)が存在し、一方のフィールドでは ManyToManyField のフィールドを定義したモデルクラスのインスタンスのプライマリーキーが、さらに他方のフィールドでは 関連モデルクラス のインスタンスのプライマリーキーが格納されることになります。

ManyToManyFieldの特徴を示す図1

そして、このテーブルの各レコードによって、2つのモデルクラスのインスタンスの関連付けが管理されることになります。例えば下の図の緑枠で囲ったレコードは、プライマリーキーが 2Student のインスタンスとプライマリーキーが 1Club のインスタンスが関連付けられていることを示しています。

ManyToManyFieldの特徴を示す図2

そして、これらの2つのフィールドに格納される値はレコード間での重複が許可されるため、両方のモデルクラスのインスタンスが関連付け可能な他方のモデルクラスのインスタンスの個数は であることになります。

ClubStudent の例で考えれば、各学生が所属できるクラブは複数かつ、クラブには複数人の学生が所属できることになります。つまり、クラブの掛け持ち可の「各クラブに所属する生徒の管理」が実現できることになります。

ここまで説明してきたように、リレーションの種類には下記の3つが存在し、それぞれで実現可能な関連性の管理が異なることになります。それぞれの特徴を理解して、適切なリレーションを利用することが重要となります。

  • 1対1のリレーション
  • 多対1のリレーション
  • 多対多のリレーション

リレーションフィールドとデータ属性

続いて、リレーションフィールドの定義によってインスタンスに追加されるデータ属性について解説していきます。インスタンスの関連付けは、このデータ属性を利用して実施することになるため、このデータ属性はリレーションを利用する上で非常に重要となります。

まず、通常のフィールド同様に、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールド名のデータ属性が追加されることになります。

そして、これはリレーションフィールド特有の話なのですが、リレーションフィールドを定義したモデルクラスのインスタンスだけでなく、リレーションフィールドの第1引数に指定した 関連モデルクラス のインスタンスにもデータ属性が追加されることになります。

これらのデータ属性を利用することで、リレーションフィールドを定義したモデルクラスのインスタンスからも、その 関連モデルクラス のインスタンスからも、他方側のモデルクラスのインスタンスに対して関連付けを行うことが可能です。これらを実施するのはビューの関数やモデルクラスのメソッド等になります。

両方のモデルクラスからインスタンスの関連付けが実施可能であることを示す図

ただ、これらの追加されるデータ属性の名称や、それらのデータ属性を利用した関連付けの手順が少し複雑なので、ここからは、このリレーションフィールドの定義によって追加されるデータ属性についての詳細を詳しく説明していきたいと思います。

の関係と追加されるデータ属性

で、このリレーションフィールドの定義によって追加されるデータ属性を理解する上で重要になるポイントは、そのインスタンスに関連付け可能なインスタンスの個数になります。このインスタンスに関連付け可能なインスタンスの個数は  or のどちらかになります。このどちらであるのかによって追加されるデータ属性の名称や使い方等が異なるので、各モデルクラスのインスタンスに関連付け可能なインスタンスの個数を意識しながら関連付けについての理解を進めることが重要となります。

そのため、ここで、各モデルクラスのインスタンスに関連付け可能なインスタンスの個数を整理しておきたいと思います。

まず、1対1のリレーションの場合は単純で、OneToOneField のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は となります。さらに、その関連付けの相手となる 関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数も となります。

同様に、多対多のリレーションの場合も単純で、ManyToManyField のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は  となります。さらに、その関連付けの相手となる 関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数も  となります。

そして、多対1のリレーションの場合は、まず ForeignKey のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は となります。それに対し、その関連付けの相手となる 関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数は  となります。

多対1のリレーションの場合は若干ややこしいのですが、テーブルを思い浮かべると分かりやすいと思います。多対1のリレーション で説明したように、ForeignKey のフィールドが定義されたモデルクラスのテーブルには 関連モデルクラス のインスタンスのプライマリーキーを格納するためのフィールドが追加されることになります。そして、このフィールドに格納できるプライマリーキーは1つだけなので、ForeignKey のフィールドが定義されたモデルクラスのインスタンスに関連付け可能なインスタンスの個数は ということになります。また、他方側の 関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数は、余った方の  ということになります。

ForeignKeyの特徴を示す図1

定義するリレーションフィールドに応じた or の関係をまとめると下図のようになります。これを理解した上で、ここからの説明を読み進めていただければと思います。

関連付け可能なインスタンスの個数のケースごとにまとめた表

データ属性の名称

前述で簡単に説明しましたが、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールドのフィールド名のデータ属性が追加されることになります。これに関しては、リレーションの種類に関わらず共通になります。

リレーションフィールドを定義したモデルクラスのインスタンスに追加されるデータ属性の名称の説明図

例えば、下記のように Student にリレーションフィールドを定義した場合、この Student のインスタンスには club というデータ属性が追加されることになります。下記では ForeignKey のフィールドを定義していますが、models.OneToOneFieldmodels.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, 略)

それに対し、関連モデルクラス のインスタンスに追加されるデータ属性の名称は、その 関連モデルクラス のインスタンスに関連付け可能なインスタンスの数( or )によって決まりす。

関連モデルクラスのインスタンスに追加されるデータ属性の名称の説明図1

具体的には、関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数が の場合、相手のモデルクラス名 という名称のデータ属性が追加されることになります。

関連モデルクラスのインスタンスに追加されるデータ属性の名称の説明図2

それに対し、関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数が の場合、相手のモデルクラス名_set という名称のデータ属性が追加されることになります。

関連モデルクラスのインスタンスに追加されるデータ属性の名称の説明図3

MEMO

少し補足しておくと、データ属性の名称の 相手のモデルクラス名 部分はすべて小文字となります

また、これらの 関連モデルクラス のインスタンスに追加されるデータ属性の名称は変更可能で、これに関しては後述の 追加されるデータ属性の名称変更(related_name) で解説します

例えば、下記のように StudentClub を定義する場合、リレーションフィールド 部分が OneToOneField であれば、Club のインスタンスと関連付けが可能なインスタンスの個数は  であるため、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, 略)

もし、上記の StudentClub の定義において、リレーションフィールド 部分が ManyToManyField or ForeignKey であれば、Club のインスタンスと関連付けが可能なインスタンスの個数は  であるため、Club のインスタンスにはデータ属性 student_set が追加されることになります。

このように、関連モデルクラス のインスタンスに追加されるデータ属性は、そのインスタンスと関連付け可能な個数が であれば 相手のモデルクラス名 という名称になりますし、 であれば 相手のモデルクラス名_set という名称になります。このように、データ属性の名称が変化するのは、そのデータ属性の役割、つまり、そのデータ属性での管理対象が異なるからになります。この点について、次の節で解説していきます。

データ属性の役割

続いて、追加されたデータ属性の役割、つまり、そのデータ属性で何を管理するのか?という点について説明します。ここを理解しておけば、インスタンス同士の関連付けを行う方法や関連付けられたインスタンスの取得方法が理解しやすくなります。

まず、関連付け可能なインスタンスの個数が であるインスタンスの場合、そのインスタンスに追加されたデータ属性の役割は、関連付け相手となるモデルクラスの1つのインスタンスを管理することとなります。このデータ属性では、その関連付け相手となるモデルクラスのインスタンスを = 演算子で直接参照することが可能です。

リレーションフィールドの定義によって追加されたデータ属性の役割を示す図1

それに対し、関連付け可能なインスタンスの個数が であるインスタンスの場合、そのインスタンスに追加されたデータ属性の役割は、集合を管理すること、もっと詳しく言えば、関連付け相手となるモデルクラスのインスタンスの集合を管理することとなります。なので、このデータ属性では、通常の集合の時と同様に、このデータ属性にメソッドを実行させることで、その集合に対する操作を行うことになります。

リレーションフィールドの定義によって追加されたデータ属性の役割を示す図2

関連付けが可能なインスタンスが なので、これらの関連付けられたインスタンスは集合(リスト等でも良いが)での管理が必要となります。そのため、この場合はリレーションフィールドを定義することで追加されるデータ属性は集合を参照するデータ属性となるというわけです。また、前述で、”関連モデルクラス のインスタンスに追加されるデータ属性は、そのインスタンスと関連付け可能なインスタンスの個数が であれば 相手のモデルクラス名_set という名称になる” と説明しましたが、わざわざデータ属性名に _set が付加されるのは、そのデータ属性が集合を管理するものであることを示すためになります。

スポンサーリンク

インスタンスの関連付け

続いて、インスタンスの関連付けについて解説していきます。

この関連付けは、リレーションフィールドの定義によって追加されるデータ属性を利用して実施することができます。

関連付け可能なインスタンスの個数が  の場合

関連付け可能なインスタンスの個数が であるインスタンスの場合は、その追加されたデータ属性に = 演算子で相手のモデルクラスのインスタンスを参照させることで、そのデータ属性を持つインスタンスと参照先のインスタンスとを関連付けることができます。

インスタンスの関連付けの仕方の説明図1

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されている場合、下記のような処理により taro に soccer を関連付けることができます。関連付けが実施されるのは taro.club = soccer の部分になります。ただ、これだけだと、その関連付けがデータベースには反映されないため、最後に taro.save() を実行して taro のインスタンスをレコードとしてデータベースに保存することで、その関連付けのデータベースへの反映を行なっています。

このように、= 演算子での関連付けをデータベースに反映させるためには、リレーションフィールドを定義したモデルクラスのインスタンスから save メソッドを実行させる必要があるという点に注意してください。

StudentからClubへの関連付け
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)

# taroにsoccerを関連付け
taro.club = soccer

# データベースに反映
taro.save()

関連付け可能なインスタンスの個数が  の場合

それに対し、関連付け可能なインスタンスの個数が であるインスタンスの場合は、その追加されたデータ属性に add メソッドを実行させることで関連付けを行うことになります。この add メソッドの引数には、関連付けしたいインスタンスを指定する必要があります。

この add メソッドの実行によって、「関連付けられているインスタンスを管理する集合」に新たなインスタンスが追加されることになり、これによってインスタンスの関連付けが実現されるようになっています。

インスタンスの関連付けの仕方の説明図2

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されている場合、下記のような処理により、soccer に関連付けられるインスタンスに taro を追加することができます。

ClubからStudentへの関連付け
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)

# soccerにtaroを関連付け
soccer.student_set.add(taro)

ちょっとややこしいようにも感じますが、上記で解説した内容は特別なものではなく、通常の変数と同じ考え方でのインスタンスの管理となります。

例えば、変数で1つのインスタンスを管理するのであれば、その変数に = 演算子で管理対象のインスタンスを参照させることになります。

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'))

これと同じようなことを、リレーションフィールドの定義によって追加されるデータ属性に対しても行う必要があるだけで、特別難しい話ではないと思います。重要なのは、操作対象のインスタンスと関連付けされるインスタンスが なのか なのかを意識し、それに応じたデータ属性の使い方を実施することになります。

関連付けのデータベースへの反映

また、= 演算子による参照で関連付けを行った場合、その関連付けをデータベースに反映するためには、リレーションフィールドを定義した方のモデルクラスのインスタンスに save メソッドを実行させる必要がありました。これは、= 演算子による参照を行ってもデータベースのレコードの更新が行われないからになります。

ですが、add メソッドでの関連付けに関しては、その add メソッドの実行が成功したタイミングでインスタンスの関連付けがデータベースに反映されることになります。ですので、上記の例のように、関連付けのみを実施するのであれば、add メソッド実行後に save メソッドを実行させる必要はありません。

=による関連付けとaddメソッドによる関連付けとのデータベースへの反映の違い

ただし、add メソッドでのレコード更新時に更新されるのは、その add メソッドの実行によって変化するリレーションフィールドのみとなります。したがって、他のフィールドを更新する場合は、別途 save メソッドメソッドを実行する必要があります。

両方のデータ属性から関連付けが可能

ここまで説明してきたように、インスタンスの関連付けは、リレーションフィールドの定義によって追加されるデータ属性によって実現可能です。また、リレーションフィールドの定義を行うことで、その定義を行ったモデルクラスだけでなく、その 関連モデルクラス に対してもデータ属性が追加されることになります。

したがって、インスタンスの関連付けは、リレーションフィールドの定義を行った方のモデルクラスのインスタンスからだけでなく、その 関連モデルクラス のインスタンスからも実施可能ということになります。そして、同じインスタンス同士を関連付けるのであれば、どちらのモデルクラスのインスタンスから関連付けを行ったとしても結果は変わりません。

両方のモデルクラスからインスタンスの関連付けが実施可能であることを示す図

つまり、特定のインスタンスから他方のインスタンスに関連付けを行えば、自動的に他方のインスタンスからその特定のインスタンスが関連付けされることになります。

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されている場合、下記のような処理により、tarosoccer を関連付けることができます。

StudentからClubへの関連付け
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)

# taroにsoccerを関連付け
taro.club = soccer

# データベースに反映
taro.save()

同時に、soccer に taro が関連付けられることにもなるため、上記の処理により、soccer.student_set の集合に taro が追加されることにもなります。

一方のモデルクラスからの関連付けにより他方のモデルクラスからの関連付けも自動的に行われることを示す図1

逆に、下記のような処理により、soccer に taro を関連付ければ、

ClubからStudentへの関連付け
# インスタンス生成
taro = Student.objects.create(略)
soccer = Club.objects.create(略)

# soccerにtaroを関連付け
soccer.student_set.add(taro)

同時に、taro に soccer が関連付けられることにもなるため、上記の処理により、soccer.club は taro を参照することにもなります。

一方のモデルクラスからの関連付けにより他方のモデルクラスからの関連付けも自動的に行われることを示す図2

このように、特定のインスタンスから他方のインスタンスを関連付ければ、逆方向の関連付けも自動的に行われるようになっています。結局、一方のインスタンスから関連付けしてしまえば、2つのインスタンスが関連しあう状態となるので、関連付けは一方のインスタンスから行えば良いだけですし、その関連付けは、リレーションフィールドの定義を行ったモデルクラスのインスタンスから行っても、その 関連モデルクラス のインスタンスから行っても問題ありません。

ただ、どちらかというとシンプルな考え方で実装可能なのは、リレーションフィールドの定義を行ったモデルクラスのインスタンスからの関連付けになると思います。なので、どちらかというと、リレーションフィールドの定義を行ったモデルクラスのインスタンスからの関連付けの実施をオススメします。

いずれにせよ、そのデータ属性を利用するインスタンスに関連付け可能なインスタンスの個数が or のどちらであるかによって関連付けの仕方やデータ属性の名称が変わることになるので、その点については注意してください。

また、特定のインスタンスに関連付けられたインスタンスの取得に関しても、前述で説明したリレーションフィールドの定義によって追加されるデータ属性を用いて実現可能となります。

関連付け可能なインスタンスの個数が  の場合

関連付け可能なインスタンスの個数が であるインスタンスの場合は単純で、追加されたデータ属性の参照先から、そのインスタンスに関連付けられたインスタンスを取得すれば良いだけです。

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されている、かつ studentStudent のインスタンスである場合、下記のような処理により student に関連付けられた Club のインスタンスを取得し、それを出力することができます。

Studentに関連付けられたClubの取得
# studentに関連付けられたClubのインスタンスを取得
club = student.club

print(club)

関連付け可能なインスタンスの個数が  の場合

それに対し、関連付け可能なインスタンスの個数が  であるインスタンスの場合は少し複雑で、追加されたデータ属性にメソッドを実行させてインスタンスを取得することが必要となります。この追加されたデータ属性に実行させられるメソッドとしては、例えば下記のようなものが挙げられます。

  • all:データ属性の集合に含まれる全インスタンスを要素とする集合を取得する
  • filter:データ属性の集合に含まれるインスタンスの内、引数で指定した条件を満たす全てのインスタンスを要素とする集合を取得する
  • get:データ属性の集合に含まれるインスタンスの内、引数で指定した条件を満たす1つのインスタンスを取得する

allfilter によって取得される集合は、より具体的に言うとクエリーセットとなります。このクエリーセットにメソッドを実行させることで、取得した集合内のインスタンスの並びを変更したり、さらに条件を絞ったインスタンスの集合を取得するようなことも可能です。また、このクエリーセットに対して for ループを実行することで、そのクエリーセットに含まれる全インスタンスに対して1つ1つ処理を実行するようなことも可能になります。

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されている、かつ clubClub のインスタンスである場合、下記のように処理を行えば club に関連付けられた全てのインスタンスを取得し、それらを1つ1つ出力することができることになります。

Clubに関連付けられたStudentの取得
# 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.pyStudentClub を定義すれば、これらのインスタンスの間には1対1の関連付けを行うことができるようになります。

OneToOneFieldの定義
from django.db import models

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

class Student(models.Model):
    name = models.CharField(max_length=32)
    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 引数の意味合いについては下記ページで解説していますので、詳細を知りたい方は下記ページを参照していただければと思います。

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

また、引数に null=True も指定していますが、これは、そのフィールドが空の状態のインスタンスをデータベースのテーブルに保存できるようにするためです。リレーションフィールドの場合、関連付けを行なっていない状態でインスタンスを保存するためには null=True の指定が必要となります。そのような状態でのインスタンスの保存を行う機会は結構あるので、リレーションフィールドに関しては、迷ったら null=True を指定しておくことをオススメします。

インスタンス同士の関連付けを行う

続いて、インスタンス同士の関連付けを行う処理をビューもしくはモデルクラスに実装していくことになります。このページでは、この関連付けはビューで実施することを前提に解説を進めます。

MEMO

どんなタイミング&どういう目的で関連付けを行うのかについては開発対象のウェブアプリによって異なりますので、ここからは、インスタンス同士の関連付けを行う例として、単純に Student のインスタンスを生成し、そのインスタンスを name='soccer' を満たす Club のインスタンスと関連付けるビューの関数の実装例を紹介していきたいと思います

ビューの関数からの返却値等はてきとうになっているので注意してください

データ属性の名称 で解説したように、リレーションフィールドを定義したモデルクラスのインスタンスには、そのフィールドのフィールド名のデータ属性が追加されます。今回の場合は Student のインスタンスにデータ属性 club が追加されることになります。そして、そのデータ属性 club にインスタンスを = で参照させることで、Student のインスタンスに Club のインスタンスが関連付けられることになります。

そのため、下記のような処理によって、Student のインスタンスに対して Club のインスタンスを関連付けることができることになります(事前に Club のテーブルに name='soccer' を満たすレコードが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))

上記では Student のインスタンスのデータ属性を利用して Club のインスタンスとの関連付けを実施していますが、逆に Club のインスタンスのデータ属性を利用して Student のインスタンスとの関連付けを実施することも可能です。

データ属性の名称 で解説したように、1対1のリレーションの場合、関連モデルクラス のインスタンスには 相手のモデルクラス名 のデータ属性が追加されることになります。今回の場合は、Club にデータ属性 student が追加されることになるため、このデータ属性に Student のインスタンスを参照させることで関連付けを行うことが可能となります。

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 = 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 メソッドを実行させたとしても、存在しないリレーションフィールドは当然更新されないので注意してください。

関連付けを反映するためにはリレーションフィールドを定義したモデルクラスのインスタンスにsaveメソッドの実行が必要となることを説明する図

もう一点補足しておくと、実は、上記と同様の関連付けは次のような関数でも実現可能です。

ClubからStudentへの関連付け
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 のインスタンスを取得し、これらの情報を表示するビューの関数の例となります。

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)

関連付けられたインスタンスの取得 で解説したように、関連付け可能なインスタンスの個数が  であるインスタンスの場合、リレーションフィールドの定義によって追加されたデータ属性が参照するオブジェクトそのものが、そのインスタンスに関連付けられたインスタンスとなります。今回の場合は、student.clubstudent に関連付けられた Club のインスタンスということになります。

また、OneToOneField を定義する で示したように、Studentclub フィールドには null=True を指定しているため、club フィールドが空の状態のレコードもデータベースに保存できることになります。そして、この空の状態のフィールドは、インスタンスのデータ属性としては値が None となるため、そのことを考慮して上記のように student.clubNone でない場合のみ、Club のインスタンスの情報を出力するようにしています。

同様に、Club のインスタンスから、そのインスタンスに関連付けられた Student のインスタンスを取得することも可能です。その例が下記で、プライマリーキーが引数 pk と一致する Club のインスタンスと、その Club のインスタンスに関連付けられた Student のインスタンスを取得し、それらの情報を表示する例となります。

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のリレーションや多対多のリレーションの場合は若干複雑になりますが、それでも基本的に実施することは同じで、重要なのは、関連付け可能なインスタンスの個数( or )を意識して、リレーションフィールドの定義によって追加されるデータ属性を利用するという点になります。

多対1のリレーションの利用

次は、多対1のリレーションの利用手順について説明していきます。

ForeignKey を定義する

多対1のリレーションを利用する場合、リレーションフィールドとしては models.ForeignKey のフィールドを定義することになります。すなわち、モデルクラスに models.ForeignKey のインスタンスを値とするクラス変数を定義することで、多対1のリレーションが利用できるようになります。

例えば下記のように models.pyStudentClub を定義すれば、これらのインスタンスの間には多対1の関連付けを行うことができるようになります。

OneToOneFieldの定義
from django.db import models

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

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

ForeignKey のコンストラクタに関しても、引数 on_delete の指定が必須となります。

また、多対1のリレーションにおいては、ForeignKey のフィールドを定義した方のモデルクラスのインスタンスが、関連付け可能なインスタンスの個数が  であるインスタンスとなります。逆に、その 関連モデルクラス のインスタンスに関連付け可能なインスタンスの個数は  となります。つまり、上記の場合、Student のインスタンスに関連付け可能なインスタンスの個数が  で、Club のインスタンスに関連付け可能なインスタンスの個数が  ということになります。この点を意識して、次に説明するインスタンスの関連付けや、関連付けされたインスタンスの取得の実装を行うことが重要となります。

インスタンス同士の関連付けを行う

ということで、次はインスタンスの関連付けを実施していきましょう!

まず、Student のインスタンスに関連付け可能な Club のインスタンスの個数は  であるため、基本的には1対1のリレーションの時と同様の手順で Student のインスタンスから Club のインスタンスの関連付けを実施することが可能です。

この関連付けを行うビューの関数の例は下記となります。

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のリレーションにおいても同様の手順で実施可能です。

また、Club のインスタンスから Student のインスタンスの関連付けを実施することも可能です。ただし、Club のインスタンスに関連付け可能な Student のインスタンスの個数は  であるため、Student にリレーションフィールドを定義することによって Club のインスタンスに追加されるデータ属性は student_set ということになります。そして、このデータ属性の名称の通り、このデータ属性では集合を扱うことになるため、関連付けは add メソッドを実行して実施する必要があります。

ということで、Club のインスタンスから Student のインスタンスの関連付けを実施するビューの関数の例は下記のようなものとなります。

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 のインスタンスを取得するビューの関数の例となります。関連付け可能なインスタンスの個数が  である場合、そのインスタンスに関連付けられたインスタンスの取得に関しても1対1のリレーションの時と同様の手順で実施可能となります。

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)

ただし、Club のインスタンスから、そのインスタンスに関連付けられた Student のインスタンスを取得する手順に関しては1対1のリレーションの時とは異なります。これは、Club のインスタンスに関連付けられる Student のインスタンスの個数が であるからになります。このため、Club のインスタンスに関連付けられたインスタンスの取得は、 Club のインスタンスに追加されたデータ属性 student_set に対してメソッドを実行させて実施する必要があります。

例えば、プライマリーキーが引数 pk と一致する Club のインスタンスに関連付けられた全 Student のインスタンスの情報を取得し、それらを出力するためには、下記のような処理を行う必要があります。

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.pyStudentClub を定義すれば、これらのインスタンスの間には多対多の関連付けを行うことができるようになります。

OneToOneFieldの定義
from django.db import models

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

class Student(models.Model):
    name = models.CharField(max_length=32)
    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 のインスタンスの関連付けを行うビューの関数の例は下記となります。

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 のインスタンスの関連付けを行うビューの関数の例は下記となります。

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')

    # soccerにtaroを関連付け
    soccer.student_set.add(taro)

    return HttpResponse(str(taro))

関連付けられたインスタンスを取得する

次は、関連付けられたインスタンスの取得について解説していきます。

この取得に関しても、関連付けのとき同様に、Student のインスタンスから Club のインスタンスを取得する時も、Club のインスタンスから Student のインスタンスを取得する時も、基本的には手順は同様になります。

まず、Student のインスタンスに関連付けられた Club のインスタンスの取得を行うビューの関数の例は下記となります。

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 のインスタンスの取得を行うビューの関数の例は下記となります。

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 引数を指定することにより、前述の通り 関連モデルクラス に追加されるデータ属性の名称を設定することが可能となります。

例えば、下記のように ClubStudent を定義した場合、related_name 引数を指定していないため、Club のインスタンスに追加されるデータ属性は student_set となります。

related_name引数の指定なし
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 を利用して実施する必要があります。

related_name引数の指定なしの場合の関連付け
# student:Studentのインスタンス
# club:Clubのインスタンス
club.student_set.add(student)

それに対し、下記のように ClubStudent を定義した場合、Club のインスタンスに追加されるデータ属性は、related_name 引数で指定された students となります。

related_name引数の指定あり
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 を利用して実施することになります。

related_name引数の指定ありの場合の関連付け
# student:Studentのインスタンス
# club:Clubのインスタンス
club.students.add(student)

同一の 関連モデルクラス に対するフィールドが複数定義可能

この related_name 引数を指定することで、まず自分自身でデータ属性の名称を決定できるというメリットが得られます。

また、メリットがあるだけではなく、この related_name の指定が必須となる場合もあります。具体的には、特定のモデルクラスから同一の 関連モデルクラス に対するリレーションフィールドを複数定義する場合、この related_name の指定が必須となります。

同一の関連モデルクラスに対するリレーションフィールドを複数定義する例を示す図

この理由について、Twitter でのツイートを例に考えていきたいと思います。

まず、ツイートはユーザーから行うもので、ユーザーはツイートを何回も行うことが可能です。つまり、ユーザーと関連付けられるツイートの個数は です。それに対し、1つのツイートの投稿者(ユーザー)は一人のみなので、ツイートに関連付けられるユーザー数は  ということになります。

UserとTweetとのリレーションの説明図1

つまり、このようなユーザーとコメントとの関連付けは、下記のような UserTweet を定義することで実現可能です。このように定義することによって 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とのリレーションの説明図2

つまり、「いいね!」機能を実現するためには、下記のような UserTweet を定義することが必要となります。

いいね!機能の実現
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, 略)

ユーザーがツイートしたツイートを管理し、さらに「いいね!」機能を実現しようとすると、上記のような UserTweet の定義が必要であることは理解していただけたのではないかと思います。ただし、このような定義を行うと、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_userfavo_user を定義することによって User のインスタンスに追加されるデータ属性の名称が共に tweet_set になってしまうからです。つまり、同じ名称のデータ属性になるため、そのデータ属性がユーザーがツイートしたツイートを管理するためのものであるか、ユーザーが「いいね!」したツイートを管理するためのものであるかを区別できなくなってしまいます。そのため、上記のような例外が発生してしまうことになります。

ここまで説明してきた通り、リレーションフィールドを定義することで、関連モデルクラス のインスタンスに関連付け可能な個数が である場合は、相手のモデルクラス名_set という名前のデータ属性が追加されることになります。したがって、1つのモデルクラスに同一の 関連モデルクラス に対するリレーションフィールドを複数定義すると、追加されるデータ属性の名称が重複してしまうことになります。

なので、このような例外を防ぐためには、related_name 引数の指定が必須となります。これにより、1つのモデルクラスに同一の 関連モデルクラス に対するリレーションフィールドを複数定義したとしても、追加されるデータ属性の名称が重複することを避けることができます。

例えば、先ほどの UserTweet の定義の例であれば、下記のように related_name 引数を指定してやれば例外が発生することなく makemigrations に成功することになります。

related_nameの指定
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のテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

このコンテキストにセットされるデータが「リレーションフィールドを定義したモデルクラスのインスタンス」or「その 関連モデルクラス のインスタンス」であれば、リレーションフィールドの定義によって追加されたデータ属性を利用して、そのインスタンスに関連付けられたインスタンスを取得し、それらの情報を出力することが可能となります。

MEMO

先ほどの 追加されるデータ属性の名称変更(related_name) で説明した related_name をフィールドの引数に指定した場合、その related_name 引数に指定した名前のデータ属性がテンプレートファイルでも利用可能となります

また、テンプレートファイルではテンプレートタグ for を利用することで、集合に含まれるインスタンスの1つ1つの情報を出力するようなことも可能です。さらに、テンプレートファイルにメソッドを参照させることで、そのメソッドを引数なしで実行し、その結果を扱うようなことも可能です。そのため、リレーションフィールドの定義によって追加されたデータ属性に all メソッドを実行させるようなことも可能です。

したがって、テンプレートファイルでも、ある程度ビューと同様の処理を実現することができ、それによってビューと同様の出力を行うことが可能となります。

例えば、Studentclub = models.ForeignKey(Club, 略) が定義されており、さらにコンテキストの 'student' キーに Student のインスタンスがセットされているのであれば、テンプレートファイルに下記のような記述を行っておくことで、このテンプレートファイルから生成される HTML に、そのインスタンスに関連付けられた Club のインスタンスの情報を出力することができることになります。

studentに関連付けられたインスタンスの出力
{{ student.club.データ属性 }}

逆に、コンテキストの 'club' キーに Club のインスタンスがセットされているのであれば、テンプレートファイルに下記のような記述を行っておくことで、このテンプレートファイルから生成される HTML に、その Club のインスタンスに関連付けられた Student の全インスタンスの情報を出力することができることになります。

clubに関連付けられた全インスタンスの出力
{% for student in club.student_set.all %}
    {{ student.データ属性 }}
{% endfor %}

こんな感じで、テンプレートファイルからも、特定のインスタンスに関連付けられたインスタンスの取得及び、そのインスタンスの情報の出力が可能であることは是非覚えておいてください。情報の出力のみを行うのであれば、基本的にはテンプレートファイルでこれらの取得を行うようにしてやれば良いと思います。

フォームからの関連付け相手の選択

また、関連付けの相手をフォームから選択できるようにする方法も知っておくと便利だと思います。

例えば、掲示板アプリでコメントを投稿するような時に、コメント投稿フォームで投稿者となるユーザー名を直接入力するのではなく、下の図のような、既に登録済みのユーザー一覧が表示されるプルダウンメニューを用意し、そこから投稿者となるユーザーが選択できれば便利ですよね!

フォームに関連付け相手を選択するプルダウンメニューが表示される様子

フォームクラスでのプルダウンメニューの実現

このようなフィールドは、下記ページで解説しているフォームクラスに対して ModelChoiceField のフィールドを定義することで実現できます。この ModelChoiceField は特定のモデルクラスのインスタンスの候補を選択するためのプルダウンメニューを実現するフィールドになります。このプルダウンメニューがクリックされた際に表示されるインスタンスの候補は、ModelChoiceFieldqueryset 引数にインスタンスの集合(クエリーセット)を指定することで設定可能です。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

掲示板アプリでのコメント投稿フォームを例に、上記のようなフォームの実現手順についてもう少し具体的に説明していきます。

まず、モデルクラスとして下記のような UserComment が定義されているとしましょう。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__ メソッドについては下記ページで解説を行なっていますので、詳しく知りたい方は下記ページをご参照ください。

モデルの__str__メソッドの説明ページアイキャッチ 【Django】モデル(Model)の__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 でフィールドを定義しなくても、リレーションフィールドを持たせたモデルクラスをベースとするフォームクラスを定義するだけで、ここで説明したことと同様のことが実現できるようになります。このモデルフォームクラスについては、次の連載の下記ページで解説していますので、リレーションについて理解した後にでも是非読んでみてください!

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

リレーション利用時の注意点

ここからは、リレーションを利用する時の注意点について解説していきます。

スポンサーリンク

プライマリーキー確定後に関連付けを行う必要がある

まず、最初の注意点が、この節の題名の通り、インスタンスの関連付けはプライマリーキーが確定してから実施する必要があるという点になります。もう少し正確に言えば、インスタンスの関連付けをデータベースに反映するときにプライマリーキーが確定している必要があります。

この点について、下記のような ClubStudent が定義されていることを前提に、例を示しながら詳細を解説していきたいと思います。 

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)

例えば下記は、StudentClub のインスタンスの関連付けを行う処理の例となります。この処理の場合は、正常にインスタンスの関連付けが実施できることになります。

StudentからClubへの関連付け
# インスタンス生成
taro = Student.objects.create(name='taro')
soccer = Club.objects.create(name='soccer')

# taroにsoccerを関連付け
taro.club = soccer

# データベースに反映
taro.save()

プライマリーキーが確定していない状態での = による関連付け

ですが、上記を次のように書き換えた場合は例外が発生することになります。

StudentからClubへの関連付け(例外発生)
# インスタンス生成
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() 時点で例外が発生することになるのでしょうか?

これに関しては、テーブルで考えると分かりやすいと思います。まず、StudentClub をテーブルとして表すと下図のようになります。ここで注目していただきたいのが、Stundentclub_id のフィールド(カラム)になります。このフィールドは、Studentclub フィールドに対応するもので、このフィールドに Club のプライマリーキーを格納することで、そのインスタンスに関連付けられた Club のインスタンスを管理できるようになっています。

StudentとClubのテーブルを図示したもの

つまり、Club のインスタンスとの関連付けを行った Stundent のインスタンスをテーブルに保存しようと思うと、club_id のフィールドに格納するための Club のインスタンスのプライマリーキーが必要ということになります。なので、Stundent のインスタンスに Club のインスタンスを関連付けるのであれば、その Club のインスタンスはプライマリーキーが確定していないといけません。プライマリーキーが確定していない状態の Club のインスタンスと関連付けた場合、club_id のフィールドに格納する値が不定となり、そのような Stundent のインスタンスをデータベースに保存しようとすると例外が発生することになります。

プライマリーキーが確定していないインスタンスと関連付けしたStudentのインスタンスの保存時に例外が発生することを説明する図

その一方で、Club のテーブルには関連付けられた Stundent のインスタンスのプライマリーキーを格納するようなフィールドは存在しないため、Stundent のインスタンスのプライマリーキーが確定していなくても、そのインスタンスに関連付けられた Club のインスタンスは問題なくテーブルに保存することが可能です。

例外の解消方法

このプライマリーキーが確定するのは、基本的にはそのインスタンスがレコードとしてデータベースに保存されたタイミングになります。

リレーションフィールドの存在しないテーブルのレコードはプライマリーキーの確定の有無に関わらず保存可能であることを示す図

また、モデルクラス名.objects.create() を実行した場合、インスタンスの生成が行われるときに、そのインスタンスがレコードとしてデータベースに保存されることになります。なので、この時点で、そのインスタンスのプライマリーキーが確定することになります。

モデルクラス.objects.create()とモデルクラス()の違いを示す図1

それに対し、モデルクラス名() を実行した場合、単にインスタンスが生成されるだけになります。なので、この時点ではプライマリーキーは確定しません。そして、前述の通り、このプライマリーキーが確定するのは、そのインスタンスをデータベースに保存したタイミングとなりますので、モデルクラス名() で生成したインスタンスに関しては、基本的には save メソッドを実行させるまではプライマリーキーが確定しないことになります。

モデルクラス.objects.create()とモデルクラス()の違いを示す図2

ここで、前述で示した例外が発生する方のスクリプトの処理の流れを確認してみると、 まず tarosoccer を生成したのち、taro.club = soccer での関連付けが行われることになります。ただし、この時点ではデータベースの更新は行われないため、この taro.club = soccer は成功することになります。ですが、その次の taro.save() を実行して taro をデータベースに保存するときに、ここまで説明した理由により例外が発生することになってしまいます。

ただ、この例外の解決方法は単純で、Stundent のインスタンスに関連付ける Club のインスタンスのプライマリーキーを先に確定させてから Student のインスタンスの保存を行うようにするだけで例外が解決できることになります。具体的には、Club のインスタンスの保存を先に行ってから、Student のインスタンスの保存を行えば良いです。

関連付けしたインスタンスの保存時の例外を防ぐ手順の説明図

つまり、下記のように taro.save() の前に soccer.save() を実行するようにすれば例外が解消します。

StudentからClubへの関連付け(成功)
# インスタンス生成
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 による関連付け

次は、先ほどと同じ定義の StudentClub を用い、今度は Club から Student の関連付けを行うときの処理について考えていきたいと思います。例えば、下記のような処理を実行した場合、先ほどと同様に例外が発生することになります。

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つのインスタンスの両方のプライマリーキーが確定していることが必要となります。

MEMO

add メソッドがこのような仕様になっているのは、おそらく多対多のリレーションでの add メソッドとの処理を共通にするためだと思います

多対多のリレーションでの add メソッド実行時の処理については、次の 多対多のリレーションでの関連付け で解説します

そのため、両方のインスタンスをデータベースに保存してから add メソッドでの関連付けを行うようにする必要があります。今回の StudentClub の定義の場合、先ほどの処理を下記のように変更すれば例外が発生しないようになります。

ClubからStudentへの関連付け(成功)
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')

# データベースに反映
soccer.save()
taro.save()

# soccerにtaroを関連付け
soccer.student_set.add(club)

また、下記のように add メソッドに bulk=False を指定すれば、関連モデルクラス 側のインスタンス、すなわち Club のインスタンスさえプライマリーキーが確定していれば add メソッドが正常終了するようになります。

ClubからStudentへの関連付け(成功)
# インスタンス生成
taro = Student(name='taro')
soccer = Club(name='soccer')

# データベースに反映
soccer.save()

# soccerにtaroを関連付け
soccer.student_set.add(club, bulk=False)

後者の方法が有効なのは、リレーションフィールドに対して null=True が指定されていない場合になります。null=True が指定されていないフィールドは必須フィールドとして扱われ、そのフィールドが空の状態でのインスタンスのデータベースへの保存が不可となります。

null=Trueを指定していないフィールドが空のインスタンスの保存が不可であることを示す図

で、そのフィールドがリレーションフィールドの場合、関連付けを行った後でなければデータベースへの保存が不可ということになります。ですが、前述の通り、その関連付けを行う前には、プライマリーキーを確定させるために、そのリレーションフィールドを持つインスタンスの保存を事前に行っておく必要があります。つまり、関連付けを行うためにインスタンスの保存が必要なのに、関連付けを行わないとインスタンスの保存が実施できないという八方ふさがりの状態になってしまいます。

リレーションフィールドにnull=Trueを指定していない場合はaddメソッド(bulk=Falseの指定なし)での関連付けが実施不可であることを示す図

その抜け道として、bulk=False を指定することで、リレーションフィールドを持つ方のインスタンスに関してはプライマリーキーが確定しない状態でも add メソッドでの関連付けが実施できるようになっています。ただ、ちょっとややこしいので、bulk=False を指定するよりかは、リレーションフィールドに対しては null=True を指定した方が楽だと思います。

多対多のリレーションでの関連付け

さて、先ほどはリレーションフィールドが ForeignKey の場合、すなわち多対1のリレーションの場合の add メソッドによる関連付けについて説明しましたが、リレーションフィールドが ManyToManyField である場合、すなわち多対多のリレーションの場合も add メソッドによる関連付けを行うことになるため、基本的には同じことに注意が必要となります。

ただ、若干注意すべき点が異なるので、その点について説明していきます。

まず、add メソッドによる関連付けを行うタイミングで、関連付けする2つのインスタンスの両方のプライマリーキーが確定している必要がある点は共通となります。ただ、リレーションの種類 で説明したように、ManyToManyField の場合は OneToOneField や ForeignKey の場合とは異なり、リレーションフィールドを定義しても、そのフィールド(カラム)は定義先のモデルクラスのテーブルには追加されません。代わりに関連付けの管理専門のテーブルが追加され、そのテーブルでインスタンス同士の関連付けが管理されることになります。

ManyToManyFieldの定義によって追加されるテーブルの説明図

このテーブルには、リレーションフィールドを定義したモデルクラスのインスタンスのプライマリーキーを管理するフィールドと、その 関連モデルクラス のインスタンスのプライマリーキーを管理するフィールドが存在します。そして、add メソッドを実行することで、関連付けられる2つのインスタンスのプライマリーキーが各フィールドに格納されたレコードが新規登録されることになります。なので、フィールドやテーブルの構成は異なるものの、結局多対多のリレーションにおいても add メソッドを実行するタイミングでは関連付けられる2つのインスタンスのプライマリーキーが確定している必要があることになります。

ManyToManyFieldを定義した場合もaddメソッドでの関連付け時に関連づける2つのインスタンスのプライマリーキーが確定している必要があることを示す図

それに対し、add メソッドへの bulk=False の指定は、多対多のリレーションにおいては基本的に不要となります。先ほども説明したように、ManyToManyField の場合はリレーションフィールドを定義しても、そのフィールド(カラム)は定義先のモデルクラスのテーブルには追加されません。したがって、null=True の指定の有無に関わらず、ManyToManyField を定義したモデルクラスのインスタンスに関しては add メソッドでの関連付けを行う前にデータベースへの保存が可能となります。

なので、多対多のリレーションの場合は、add メソッドを実行する前に、単にプライマリーキーが確定していないインスタンスに対して save メソッドを実行するようにするだけで例外の発生を防止することが可能となります。

例えば、Studentclubs = ManyToManyField(Club) が定義されている場合、下記の処理は、ManyToManyField への引数 null=True の指定の有無に関わらず成功することになります。

ClubからStudentへの関連付け(成功)
# インスタンス生成
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におけるN+1問題の解説ページアイキャッチ 【Django入門14】N+1問題とselect_related・prefetch_relatedでの解決

掲示板アプリでリレーションを利用してみる

では、ここまで説明してきた内容を踏まえて、実際にリレーションの利用例を示していきたいと思います。

この Django 入門 に関しては連載形式となっており、ここでは前回下記ページの 掲示板アプリでモデルを利用してみる で作成したウェブアプリに対してリレーションを導入する形で、リレーションの利用例を示していきたいと思います。

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

ここまで開発してきた掲示板アプリでは、モデルクラスとして UserComment の2つを定義しています。ですが、まだリレーションを利用していないため、各モデルクラスのインスタンスは独立したものとなっています。今回、リレーションを利用することで、各モデルクラスの間に関係性を持たせます。

具体的には、Comment にリレーションフィールドを定義し、Comment のインスタンスの作成者となる User のインスタンス、すなわちコメントの投稿者であるユーザーを管理できるようにしていきます。1つのコメントの投稿者は一人のみとし、さらに一人のユーザーは複数のコメントを投稿できるようにするため、今回は Comment に「関連モデルクラス を User とする ForeignKey のリレーションフィールド」を定義していきます。

Userに関連付け可能なCommentの個数が多で、Commentに関連付け可能なUserの個数が1であることを示す図

スポンサーリンク

掲示板アプリのプロジェクト一式の公開先

この 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 を定義します。

models.py
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 のインスタンスの個数は であるため、このデータ属性 user からは = 演算子での参照によって User のインスタンスとの関連付けを行うことができます。

Commentのインスタンスからの関連付けの実施手順の説明図

また、この user フィールドの定義により、User のインスタンスからは、related_name 引数で指定した名前のデータ属性、すなわち comments を利用して Comment のインスタンスとの関連付け等を行うことが可能となります。User のインスタンスに関連付け可能な Comment のインスタンスの個数は であるため、このデータ属性 comments からは add メソッドによって Comment のインスタンスとの関連付けを行うことができます。

Userのインスタンスからの関連付けの実施手順の説明図

フォームの変更

リレーションフィールドの定義が完了したため、次は models.py 以外の変更を行なっていきます。

まずはフォームの変更を行います。

先ほどのリレーションフィールドの定義によって、各コメントの投稿者が管理できるようになったことになります。そのため、下の図のように、コメント投稿フォームにプルダウンメニューを追加し、コメント投稿時に投稿者をフォームから指定できるようにしたいと思います。

コメント投稿フォームで投稿者を指定できるようにした様子

この実現方法は フォームからの関連付け相手の選択 で説明した通りで、コメント投稿フォームを実現するフォームクラスに ModelChoiceField を定義してやれば良いことになります。具体的には、コメント投稿フォームは PostForm によって実現しているため、下記のように forms.py を変更して PostForm に user フィールドを定義してやれば良いです。

forms.py
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 に指定した集合に含まれるインスタンスがリストアップされることになります。上記では querysetUser.objects.all() を指定しているため、User のテーブルに登録されている全インスタンスの出力結果がリストアップされることになります。

また、models.pyUser__str__ メソッドを定義し、この __str__ メソッドでは username を返却するようにしているため、インスタンスの出力結果は各インスタンスの username の値、すなわち各ユーザーの名前となります。なので、プルダウンメニューにも各ユーザーの名前がリストアップされることになります。 

プルダウンメニューにユーザー名が選択肢として表示される様子

スポンサーリンク

ビューの変更

次は views.py の変更を行なっていきたいと思います。

この views.py では、Comment のインスタンスの新規登録時に、User のインスタンスとの関連付けを実施する処理を追加していきます。

まず、Comment のインスタンスの新規登録を行うのは views.pypost_view 関数になります。そして、この post_view 関数では、PostForm からデータを受信したときに、PostForm の各種フィールドに入力されたデータから Comment のインスタンスを生成し、それをデータベースに新規登録するようになっています。

また、先ほどの PostForm の変更によって、user フィールドで投稿者となる User のインスタンスが選択できるようになっており、このフォームから送信されてくるデータには、その選択された User のインスタンス(のプライマリーキー)も送信されてくるようになっています。

なので、post_view 関数でフォームからデータを受信したときに、Comment のインスタンスを生成し、そのインスタンスのデータ属性 user でフォームから送信されてきた User のインスタンスを参照させれば、Comment のインスタンスと User のインスタンスとの関連付けが実現できることになります。

具体的には、views.py における post_view を下記のように変更することで、Comment のインスタンスの新規登録時に、User のインスタンスとの関連付けが実施されるようになります。

post_view
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 との関連付けを実施するように変更しているだけです。

post_viewの差分1
user = form.cleaned_data.get('user')
post_viewの差分2
comment.user = user

これで、Comment のインスタンスと User のインスタンスとが関連付けられるようになり、一方のインスタンスから、そのインスタンスに関連付けられたインスタンスを取得することができるようになります。そして、これにより、User のインスタンスの情報を出力する時に、そのインスタンスに関連付けられた Comment のインスタンスの情報を合わせて出力したり、逆に Comment のインスタンスの情報を出力する時に、そのインスタンスに関連付けられた User のインスタンスの情報を合わせて出力したりすることが可能となります。

テンプレートの変更

次は、そのような出力が実現できるようにウェブアプリを変更していきたいと思います。今回は、関連付けられたインスタンスの取得、さらには、それらのインスタンスの出力は全て、テンプレートから行うようにしたいと思います。そのため、テンプレートファイルの変更を行なっていきます。

comments.html の変更

コメント関連のページの基となる comments.htmlcomment.html では、リレーションを利用してコメントの投稿者の情報を表示するように変更を行っていきます。

前述の通り、Comment のインスタンスと User のインスタンスが関連付けられたことにより、Comment のインスタンスから投稿者の User を取得することができるようになります。そのため、 コメント一覧ページの基となる comments.html では、各 Comment のインスタンスから投稿者を取得し、その名前を HTML に出力するようにしていきます。

具体的には、下記のように comments.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、すなわちコメントの投稿者を表示する部分となります。

ただ、commentUser のインスタンスと関連付けられていない場合、comment.userNone となるため、その場合は 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 を変更します。

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.htmluser.html の変更を行っていきます。

先ほどとは逆に、User のインスタンスから Comment のインスタンスの情報を取得し、それを HTML に出力するようにしていきます。 

まず、ユーザー一覧ページでは、ユーザー名の横に、そのユーザーのコメント回数を表示するようにしたいと思います。これを実現するため、ユーザー一覧ページの基になる users.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 を変更します。

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.alluser が投稿した全 Comment のインスタンスの集合となりますので、テンプレートタグ {% for comment in user.comments.all %} により user が投稿した各 Comment のインスタンス(comment)に対して for ループが行われることになります。そして、その中で各 comment の本文を HTML に出力することで、ユーザーのコメント履歴一覧の表示を実現しています。userComment のインスタンスが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 を変更して Commentuser フィールドを追加したため、マイグレーションの実行により、下の図のように 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:多対多のリレーション

また、これらのリレーションフィールドを定義することで各インスタンスにデータ属性が追加されることになり、そのデータ属性を利用してインスタンス同士の関連付け等を実施することになります。ポイントは、追加されるデータ属性やデータ属性の利用の仕方が、上記のリレーションフィールドの種類や、リレーションフィールドを定義した方のモデルクラスであるか、その 関連モデルクラス であるかによって異なるという点になります。そして、これらは、そのデータ属性を利用するインスタンスに関連付け可能なインスタンスの個数が or のどちらであるかによって決まるため、これを意識して実装するという点がリレーションを上手く利用する上で重要になります。

最初はリレーションの利用が難しく感じるかもしれませんが、慣れれば楽々と利用できるようになると思いますので、とにかくいろんなウェブアプリを開発してみて少しずつリレーションに慣れていきましょう!

今回は異なるモデルクラスのインスタンスの関連付けに焦点を当ててリレーションについて説明を行いましたが、同じモデルクラスの2つのインスタンス同士を関連付けるようなことも可能です。これに関しては、下記ページで解説していますので、興味があれば読んでみてください。

Djangoにおける自己参照型リレーションの解説ページアイキャッチ 【Django】自己参照型リレーションについて分かりやすく解説(同じモデルクラスのインスタンスの関連付け)

また、Django 入門 の次の連載(下記ページ)では、モデルフォームについて解説しています。モデルフォームを利用することで、ウェブアプリの開発の効率化を図ることができます!是非下記ページも読んでみてください!

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

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