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

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

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

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

前回の連載で、モデルについて解説を行いました。

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

モデルはデータベースを管理することを役割としており、このモデルのクラス定義によってウェブアプリからデータベースで扱うデータ(レコードの形式)が決まります。

また、上記ページでも少し触れていますが、モデルクラス同士を関連づけることも可能となります。このモデルクラス同士の関連付けをリレーションと呼びます。

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

リレーションの基本

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

リレーションとは

前述の通り、リレーションとはモデルクラス同士に関連付けを行うことを言います。下記ページでも解説しているとおり、モデルクラスはデータベースにおけるテーブルの形式、より具体的にはテーブルの持つフィールド(カラム)を定義するクラスとなります。

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

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

各テーブル(モデル)が独立して存在する様子

それに対し、モデルクラスにリレーションを設定すれば、特定のテーブルと他のテーブルの間に関連性を持たせることができます。イメージとしては、下の図のように、リレーションの設定によって他のテーブルのレコードを参照するフィールドが追加され、これによってお互いのテーブルのレコード間に関連性を持たせることが可能となります。

モデルにリレーションを設定する様子

このようなリレーションを利用することで、一方のテーブルのレコードに関連性のある他方のテーブルのレコードを取得するようなことも可能になります。例えば上の図のテーブルあれば、Tennis に関連付けられている Student のレコードを全て取得して表示してやれば、Tennis 部に所属する全生徒をリストアップすることもできます。

特定のレコードに関連付けられているレコードのみを抽出する様子

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

重要なのは、このような機能は異なるテーブルのレコード間に関連性を持たせているから実現可能であるという点になります。各テーブルが独立していると、このような機能は実現できません。

世の中のウェブアプリでは、このようなリレーションを利用して様々な魅力的な機能が実現されています。例えば Twitter の例で想像すると、このアプリではツイートとユーザーの間にリレーションが存在するため、ツイートから “ツイート元のユーザー” を特定して表示するようなことが可能となっていると予想できます。

また、ユーザーから “ユーザーのツイート一覧” を表示するようなことも可能です。このような機能は、リレーションを利用しているからこそ実現可能な機能となります。

リレーションの活用例

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

スポンサーリンク

リレーションの種類

また、このリレーションには複数の種類が存在し、Django では下記の3種類のリレーションが利用することが可能です。

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

次は、この各種リレーションについて説明していきたいと思います。

親モデルと子モデル

リレーションを理解する上で、特に実装面では、関連性を持たせる2種類のモデルクラスを親と子とに分類して考えるのが良いと思います。以降では、親側のモデルクラスのことを親モデル、子側のモデルクラスのことを子モデルと記して説明していきます。

まず、リレーションは子モデル側に対して行う設定となります。具体的には、子モデルに対してリレーションを設定するための特殊なフィールドを持たせることで、リレーションを実現することになります。さらにそのフィールドでリレーションの設定先の親モデルを指定することで、親モデルと子モデルとの間にリレーションが設定されます。

リレーションの設定の仕方の説明図

そして、この特殊なフィールドには上記の3種類のものが存在し、子モデルに持たせるフィールドの種類に応じて上記の3つのリレーションのいずれかが2つのモデルクラスの間に設定されることになります。

リレーションは通常のフィールドとは異なり、リレーション設定用のフィールドを持たせたモデルクラスだけでなく、リレーション設定相手となるモデルクラスにもデータ属性が追加されるところがポイントとなります。つまり、フィールドを持たせた子モデルだけでなく、親モデル側にもデータ属性が追加されることになります。どのようなデータ属性が追加されるのかについては、後述の リレーションの構築 で解説します。

1対1のリレーション

では、各種リレーションの詳細について説明していきたいと思います。

まず、1対1のリレーションとは、親モデルのインスタンスが「最大 “1つのみ” の子モデルのインスタンス」と関連性を持つこと、および、子モデルのインスタンスが「最大 “1つのみ” の親モデルのインスタンス」と関連性を持つことを示すリレーションとなります。この場合、子モデルの各インスタンスそれぞれが関連性を持つ親モデルのインスタンスの間に重複はありません。

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

前述の通り、リレーションの設定は子モデル側で行います。つまり、データベース観点で考えれば、子モデルに対応するテーブルにフィールドを追加することになります。具体的には、下の図のように、子モデルに対応するテーブルのフィールドに親モデルのインスタンスを特定するフィールド(カラム)を追加し、このフィールドによって関連するレコードを管理します。そして、1対1のリレーションの場合、このフィールドの各要素の間では同じデータが重複することは許されません。

テーブル視点での1対1のリレーション

「モデルのインスタンスを特定するフィールド」とは、具体的にはモデルクラスにおけるプライマリーキーのフィールドとなります。上の図においては親モデルのインスタンスを特定するフィールドとして id を例に挙げていますが、プライマリーキーの設定によっては他の値となる場合もあります。この点については、1対1のリレーションだけでなく、他のリレーションにおいても同様の話になります。

1対1のリレーションの例

次は、1対1のリレーションについて具体例で考えていきましょう。

例えば、生徒とその生徒の1枚の顔写真を管理するウェブアプリを想定し、生徒を管理するモデルクラス Student と顔写真を管理するモデルクラス Image との間のリレーションについて考えていきたいと思います。

生徒と生徒の顔写真を管理するためのクラスの説明図

まず、生徒の顔写真を管理するのですから、これらの2つのモデルクラスの間には関連性があるはずです。さらに、各生徒に対して管理する顔写真は1枚のみであり、さらに顔写真は生徒間で重複がないはずです。

つまり、Image を子モデルとし、Student を親モデルと考えれば、親モデルのインスタンスは1つのみの子モデルのインスタンスと関連性を持ち、子モデルのインスタンスは1つのみの親モデルのインスタンスと関連性を持つことになります。

したがって、Student クラスと Image クラスとの間に1対1のリレーションを設定することで、上記のようなウェブアプリを実現することができることになります。

StudentとImageのリレーションの説明図

1対1のリレーションでは、関連性を持つインスタンスが1つずつになるため、一方のインスタンスから、関連付けられた他方のインスタンスを一意に特定することができます。

1対多のリレーション

1対1のリレーションに対し、1対多のリレーションとは、親モデルのインスタンスが「複数の子モデルのインスタンス」と関連性を持つこと、および、子モデルのインスタンスが「最大 “1つのみ” の親モデルのインスタンス」と関連性を持つことを示すリレーションを示すリレーションとなります。この場合、子モデルの各インスタンスそれぞれが関連性を持つ親モデルのインスタンスの間に重複が発生する可能性があります。

つまり、”1” の親モデルのインスタンスに対し “多” の子モデルのインスタンスが関連づけられることになります。また、1対多のリレーションの場合、2つのモデルクラスのうちのどちらを親とするかで意味合いが大きく変わることになります。

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

1対1のリレーションの時同様、1対多のリレーションに関しても、子モデルに親モデルのインスタンスを特定するためのフィールドを設けることで実現することができます。テーブルで考えれば、下の図のように、子モデルに対応するテーブルのフィールドに親モデルのインスタンスを特定するフィールド(カラム)が存在することになります。ただし、このフィールドの各要素の間には重複が存在する可能性があります。

テーブル視点での1対多のリレーション

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

例えば、部活に所属する生徒を管理するウェブアプリを想定し、生徒を管理するモデルクラス Student と部活を管理するモデルクラス Club の間のリレーションについて考えていきたいと思います。生徒は部活の掛け持ちは不可であるとします。

生徒と部活を管理するためのクラスの説明図

まず、生徒は部活に所属することになるため、これらの2つのモデルクラスの間には関連性があるはずです。さらに、部活には複数人の生徒が所属することができるため、1つの部活に対して複数の生徒が関連していることになります。ただし、部活は掛け持ち不可であるとしていますので、一人の生徒が複数の部活に関連することはありません。

つまり、Student を子モデルとし、Club クラスを親モデルとすれば、親モデルのインスタンスは複数の子モデルのインスタンスと関連性を持ち、子モデルのインスタンスは最大1つのみの親モデルのインスタンスと関連性を持つことになります

したがって、Club クラスと Student クラスとの間に1対多のリレーションを設定することで、上記のようなウェブアプリを実現することができることになります。

ClubとStudentのリレーションの説明図

1対多のリレーションでは、子モデルのインスタンスから、そのインスタンスに関連付けられた親モデルのインスタンスを一意に特定することが可能です。しかし、親モデルのインスタンスからは、関連付けられている子モデルのインスタンスが複数存在する可能性があるため、一意に特定できない場合があります。

当然のことと言えば当然なのですが、この辺りの違いがリレーションを扱う上で面倒だったりするため、この点については頭に入れておいてください。

また、この例において、親モデルと子モデルを逆にするとリレーションの意味合いが大きく変化することになるので注意してください。親モデルを Student、子モデルを Club とした場合、Club には一人の Student しか所属することができないことになります。

多対多のリレーション

多対多のリレーションとは、親モデルのインスタンスが複数の子モデルのインスタンスと関連性を持ち、さらに子モデルのインスタンスも複数の親モデルのインスタンスと関連性を持つことを示すリレーションとなります。

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

多対多のリレーションを実現するためのテーブル構成は、1対1のリレーションや1対多のリレーションの時に比べると複雑です。多対多のリレーションは、下の図のように親モデルと子モデルとは別のテーブルを用意し、このテーブルの2つのフィールドで、関連付く2つのモデルクラスのインスタンスを特定できるようになっています。多対多の関係ですので、それぞれのフィールドにおける各要素感には重複が存在する可能性があります。

テーブル視点での多対多のリレーション

多対多のリレーションの例

例えば、部活に所属する生徒を管理するウェブアプリを想定し、生徒を管理する Student クラスと部活を管理する Club クラスの間のリレーションについて考えていきたいと思います。ここまでは1対多のリレーションの説明で用いた例と同じになりますが、今回は部活の掛け持ちは可能であるとします。

まず、生徒は部活に所属することになるため、これらの2つのモデルクラスの間には関連性があるはずです。さらに、部活には複数人の生徒が所属することができるため、1つの部活に対して複数の生徒が関連していることになります。さらに、生徒は複数の部活に所属することができるため、一人の生徒に対して複数の部活が関連することになります。

つまり、Student を子モデルとし、Club クラスを親モデルとすれば、親モデルのインスタンスが複数の子モデルのインスタンスと関連性を持ち、さらに子モデルのインスタンスも複数の親モデルのインスタンスと関連性を持つことになります。

したがって、Club クラスと Student クラスとの間に多対多のリレーションを設定することで、上記のようなウェブアプリを実現することができることになります。

ClubとStudentのリレーションの説明図

また、多対多のリレーションでは、子モデルのインスタンスからも、親モデルのインスタンスからも、そのインスタンスに関連付けられた他方のモデルのインスタンスを一意に特定することができません。

リレーションの設定

先程、各リレーションの違いについて解説を行ってきましたが、ここからは各種リレーションの設定・および構築を行う具体的手順を示していきたいと思います。

リレーションの設定とは

まず、リレーションを利用するためには下記の2つのことを行う必要があります。

  • 2つのモデルクラスに対してリレーションを設定する
  • 2つのインスタンスに対してリレーションを構築する

モデルクラスへのリレーションの設定

ここまでの説明でも感じ取っていただけたかもしれませんが、リレーションの目的は、あくまでも2つのモデルクラスの “インスタンスの間” に関連性を持たせることになります。テーブル観点で考えれば、2つのテーブルのレコードの間に関連性を持たせることとも考えられます。

インスタンス同士に対するリレーションの説明図

ですが、インスタンス同士に関連性を持たせるためには、事前に2つのモデルクラスの間にリレーションを設定しておく必要があります。この設定を行っておかないと、2つのインスタンスの間に関連性を持たせることができません。

クラス同士に対するリレーションの説明図

つまり、インスタンス同士に関連性を持たせるためには、それらのインスタンスのモデルクラスに対して事前に設定を行う必要があります。そして、その設定のことを、このサイトでは「リレーションの設定」と呼んでいます。さらに、それと区別を行うことを目的に、インスタンス同士に関連性を持たせることを「リレーションの構築」と呼んでいます。

スポンサーリンク

リレーションの設定方法

このモデルクラスに対するリレーションの設定は、子モデル側のクラスに対して下記のいずれかのフィールドをクラス変数として持たせることで実現することができます。

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

どのフィールドを持たせるにしても、フィールドのコンストラタを実行する際に引数としてモデルクラスを指定する必要があります。そして、その指定したクラスが、フィールドを持たせたクラスに対する親モデルとなります。

リレーションの設定

例えば、1対多のリレーションの例 ので示した ClubStudent の例の場合、下記のように定義を行うことで Club を親モデル、Student を子モデルとした1対多のリレーションの設定を行うことができます。

リレーションの設定例
from django.db import models

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

class Student(models.Model):
    name = models.CharField(max_length=256)
    club = models.ForeignKey(Club, on_delete=models.CASCADE)

ポイントは、上記のようなリレーション設定用のフィールドを持たせる必要があるのは一方のモデルクラスに対してのみである点であり、フィールドを持たせた側のモデルクラスが子モデルとなります。

また、モデルクラスのインスタンスはモデルクラスの持つフィールドをデータ属性として利用可能となります。従って、上記のようにリレーションの設定を行うことで、子モデルのインスタンスにはデータ属性が1つ追加されることになります。

そして、後述で解説しますが、そのデータ属性を利用することでインスタンス同士のリレーションの構築を行うことが可能となります。

また、リレーション設定用のフィールドを子モデルに持たせた場合、子モデル側だけでなく親モデル側にもデータ属性が追加されることになります。そして、そのデータ属性を利用することで、インスタンス同士のリレーションの構築を行うことが可能となります。つまり、リレーションの設定は子モデル側で必ず行う必要がありますが、リレーションの構築に関してはどちらのモデル側からでも実施可能となります。

MEMO

ここでは詳細な説明は行いませんが、OneToOneFieldForeignKey の場合はコンストラクタに引数として on_delete を指定する必要があります

この on_delete は、親モデルのインスタンスが削除されていなくなった際の子モデルのインスタンスに対する処理を指定する引数となります

詳しくは下記ページで解説していますので、こちらを参照してください

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

文字列での親モデルの指定

前述の通り、これらのフィールドでは引数で親モデルを指定する必要があります。この親モデルの指定に関しては、基本的に親モデルとなるモデルクラスをそのまま指定してやれば良いです。

ただし、親モデルの定義が子モデル側の定義よりも下側で行われるような場合は、モデルクラスをそのまま指定すると例外が発生することになります。これを解決するためには、モデルクラスを文字列で指定してやれば良いです。

例えば、下記のようにモデルクラスの定義を行うと、User の定義がインタプリタに認識されるよりも前に ForeignKey のコンストラクタが実行されることになり、第1引数の User が認識されていないために例外が発生します。

親モデルをクラスで指定
class Comment(models.Model)
    user = models.ForeignKey(User, 〜略〜)

class User(models.Model):
    〜略〜

これは、下記のように User をクラスではなく文字列 'User' で指定してやることで解決できます。もちろん User の定義を Comment よりも上側に持っていくことでも解決可能です。

親モデルを文字列で指定
class Comment(models.Model)
    user = models.ForeignKey('User', 〜略〜)

class User(models.Model):
    〜略〜

また、自分自身に対してリレーションを設定することも可能であり、この場合は、必ず親モデルは自分自身のモデル名を文字列で指定する必要があります。

自分自身に対するリレーションの設定1
class Comment(models.Model)
    reply = models.ForeignKey('Comment', 〜略〜)

もしくは、親モデルとして 'self' を指定します。この場合も、単に self で自分自身を参照するのではなく、文字列 'self' を指定する必要があります。

自分自身に対するリレーションの設定2
class Comment(models.Model)
    reply = models.ForeignKey('self', 〜略〜)

このような、自分自身に対してリレーションの設定を行うことを自己参照型リレーションと呼びます。これを利用することで、コメントの返信機能やフォロー機能などを簡単に実現できます。

自己参照型リレーションについては下記ページで解説していますので、興味があれば、このページを読んだ後にでも是非読んでみてください。

Djangoにおける自己参照型リレーションの解説ページアイキャッチ 【Django】自己参照型リレーションについて分かりやすく解説

リレーションの構築

2つのモデルクラス間にリレーションを設定すれば、それらのインスタンス間に関連性を持たせることが可能となります。モデルクラス間に関連性を持たせることを「リレーションを設定する」と呼ぶのに対し、インスタンス間に関連性を持たせることを「リレーションを構築する」と呼ばせていただきます。

前述の通り、リレーションの設定に関しては必ず子モデル側で行う必要がありましたが、リレーションの構築に関しては子モデル側からでも親モデル側からでも行うことが可能となります。ですが、子モデル側 or 親モデル側のどちらであるかや、リレーションの種類によってリレーションの構築の仕方が若干異なります。そのため、リレーションの種類ごとにリレーションの構築手順を解説していきたいと思います。

1対1のリレーションの構築

リレーションの構築手順として一番分かりやすいのが1対1のリレーションになります。

子モデル側からの1対1のリレーションの構築

リレーションの設定 でも解説したように、リレーション構築用のフィールドを子モデルに持たすことで、子モデル側と親モデル側の両方にデータ属性が追加されることになります。1対1のリレーションの場合、その追加されたデータ属性からリレーションを構築したい相手のインスタンスを参照してやれば、リレーションの構築を実現することができます。

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

例えば下記のように models.pyStudentImage を定義したとします。ImageOneToOneField を持っており、リレーションの設定先に Student を指定しているため、これらは1体1のリレーションが設定されていることになります。そして、この場合、Image が子モデルとなります。

1対1リレーションの設定例
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=256)

class Image(models.Model):
    img = models.ImageField(upload_to='img/', null=True)
    student = models.OneToOneField(Student, on_delete=models.CASCADE)

ちなみに、ImageField はモデルで画像を扱ったり画像のアップロード機能を実現するために利用するフィールドです。詳細は下記ページで解説していますので、興味のある方は読んでみてください。今回はリレーションの構築に焦点を当てているため、画像のアップロードをしなくても良いよう null=True を指定しています。

Djangoでの画像のアップロードの実現方法解説ページアイキャッチ 【Django】画像をアップロードする

このようなモデルクラスを定義した場合、OneToOneField を持つクラスのインスタンスは、その OneToOneField のフィールド名、上記の例で言えば studentのデータ属性を持つことになります。そして、このデータ属性からリレーションが設定されたクラスのインスタンスを= で参照させることで、お互いのインスタンスの間にリレーションが構築されることになります。

OneToOneFieldで親モデルのインスタンスとリレーションを構築する様子

例えば下記は、前述の Student のインスタンスと Image のインスタンスの間にリレーションを構築する例となります。両方のクラスのインスタンスを生成した後、image.student = student の部分でリレーションの構築を行なっています。

子モデルからの1対1リレーションの構築例
student = Student(name='YamadaTaro')
student.save()

image = Image()
image.student = student # リレーション構築
image.save()

後述でも説明しますが、リレーションの構築を行う場合、子モデル側のインスタンスの保存を行う前に、親モデル側のインスタンスを先にテーブルに保存しておく必要がある点に注意してください。先に子モデル側のインスタンスの保存を行なった場合、上記の例で言えば、student.save() の実行が image.save() よりも後ろにある場合、次のような例外が発生することになります。

save() prohibited to prevent data loss due to unsaved related object 'student'.

親モデル側からの1対1のリレーションの構築

また、上記のようにクラスを定義した場合、OneToOneField でリレーションの設定先として指定した親モデル側にも自動的にデータ属性が追加されることになります。このデータ属性の名称は子モデルのクラス名を小文字で表したものになります。

したがって、上記の場合では Studentimage のデータ属性が追加されることになります。そして、このデータ属性からリレーションが設定された子モデルのインスタンスを = で参照させることで、お互いのインスタンスの間にリレーションを構築することができます。

OneToOneFieldで親モデルのインスタンスから子モデルのインスタンスとリレーションを構築する様子

つまり、下記のような処理でも Student のインスタンスと Image のインスタンスの間にリレーションを構築することができます。

親モデルからの1対1リレーションの構築例
student = Student(name='YamadaHanako')

image = Image()

student.image = image # リレーション構築
student.save()

image.save()

前述の通り、子モデル側のインスタンスの保存を行う前に、親モデル側のインスタンスを先に保存しておく必要がある点に注意してください。

こういった注意点はあるものの、1対1のリレーションの場合、親モデルであっても子モデルであってもリレーションを設定することで追加されるデータ属性にインスタンスを参照させれば良いだけなので、簡単にリレーションの構築を行うことが可能です。

スポンサーリンク

1対多のリレーションの構築

それに対し、1対多のリレーションの場合は、親モデルのインスタンスからリレーションを構築する場合と、子モデルのインスタンスからリレーションを構築する場合とで手順が異なるので注意が必要です。

子モデル側からの1対多のリレーションの構築

簡単なのは子モデル側になります。1対多のリレーションにおいて、子モデルは “多” 側となります。逆に言えば、子モデルのリレーション構築相手は1対多のリレーションにおいて “1” 側となります。

この場合、1対1のリレーションの時と同様、リレーションの設定によって追加されたデータ属性にインスタンスを参照させることでリレーションを構築することができます。

例えば下記のように models.pyClubStudent を定義したとします。StudentForeignKey を持っており、リレーションの設定先に Club を指定しているため、これらは1体多のリレーションが設定されていることになります。

1対多リレーションの設定例
from django.db import models

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

class Student(models.Model):
    name = models.CharField(max_length=256)
    club = models.ForeignKey(Club, on_delete=models.CASCADE)

この設定により、Student のインスタンスにデータ属性 club が追加されることになります。そのため、下記のような処理を行うことで、Club のインスタンスと Student のインスタンスの間にリレーションを構築することができます。この場合も、先に親モデルのインスタンスを保存する必要がある点に注意してください。

子モデルからの1対多リレーションの構築例
club = Club(name='Soccer')
club.save()
student = Student(name='YamadaTaro')

student.club = club # リレーション構築
student.save()

1対1のリレーションの場合や、1対多の子モデルの場合、リレーションの設定により追加されるデータ属性からインスタンスを = で参照するだけでリレーションが構築できるので楽です。

このように、= で参照することができるのは、それぞれのクラスから見た他方側のクラス、つまりリレーションの設定先のクラスが1対1や1対多のリレーションにおいて “1の関係” 側のものであるからです。これらは関連づけられるインスタンスが1つのみなので、単純に = で参照させるだけでリレーションが構築できます。

1対多のリレーションにおける子モデルでは関係性を持つインスタンスが1つのみであることを示す図

親モデル側からの1対多のリレーションの構築

それに対し、リレーションの設定先のクラスが “多の関係” 側の場合である場合はリレーションの構築がちょっとややこしいです。まさに、1対多のリレーションにおいては親モデルから見たリレーション設定先のクラスは “多の関係” 側の子モデルとなり、親モデルのインスタンスからは、ここまでの説明とは異なる手順でリレーションの構築を行う必要があります。

1対多のリレーションにおける親モデルでは関係性を持つインスタンスが複数であることを示す図

まず、リレーションが設定された親モデル側のインスタンスには、1対1のリレーションの場合とは異なり、子モデル側のクラス名ではなく 子モデルのクラス名_set というデータ属性が追加されることになります(クラス名は小文字となります)。上記の StudentClub の例であれば、student_set というデータ属性が追加されることになります。_set なので、要は関連性を持つインスタンスの集合を管理するデータ属性と考えて良いと思います。

リレーションの設定によって子モデルのモデル_setというデータ属性が追加される様子

さらに、リレーションの構築は、このデータ属性に単に = でインスタンスを参照させるのではなく、子モデルのクラス名_set の集合に対して子モデルのインスタンスを追加するような処理を行う必要があります。そして、その追加を行う処理は、子モデルのクラス名_set に対して add メソッドを実行させることで実現できます。さらに、この add メソッドの引数に、リレーションを構築したいインスタンスを指定してやれば、お互いのインスタンスの間にリレーションを構築することができます。

addメソッドによりリレーションの構築を行う様子

ただし、基本的には 子モデルのクラス名_set.add の引数には、既にデータベースに保存されているレコードに対応するインスタンスしか指定できません。子モデルのクラス名_set.add の引数に bulk=False を指定した時のみ、未保存のインスタンスを指定することができるようになります。

つまり、親モデルのインスタンスから1対多のリレーションを子モデルのインスタンスと構築するためには、子モデルのクラス名_set.add の引数に指定するインスタンスは事前に save メソッドで保存しておく、もしくは 子モデルのクラス名_set.add の引数に bulk=False を指定する必要があります。

例えば下記は、Club のインスタンスから Student のインスタンスに対してリレーションを構築する処理の例となります。

親モデルからの1対多リレーションの構築例
student = Student(name='YamadaTaro')
club = Club(name='Soccer')
club.save()

club.student_set.add(student, bulk=False)

student.save()

add メソッド実行時に bulk=False を指定している点に注意してください。bulk=False を指定しないと、add メソッドの引数に未保存のインスタンスを指定すると例外が発生することになります。この理由については後述で解説を行います。

上記の処理だけ見れば複雑な処理でもないのですが、実際に実装していると、リレーションが設定された相手側が “多の関係” にあるモデルであるか、”1の関係” にあるモデルであるかによって扱い方が異なる点がややこしいので注意してください。また、後述でも説明しますが、リレーションが設定された相手側が “多の関係” 側のモデルであるか、”1の関係” 側のモデルであるかによってリレーションが構築されているインスタンスの取得方法も異なることになります。

多対多のリレーションの構築

続いて、多対多のリレーションにおけるインスタンス間のリレーションの構築手順について説明していきます。

基本は1対多のリレーション構築時と同様の考え方になります。

ただし、多対多のリレーションの場合、親モデルから見たリレーションの設定相手は “多の関係” にあり、子モデルから見たリレーションの設定相手も “多の関係” にあります。したがって、リレーションを構築する際には、子モデルのインスタンスからリレーションを構築する場合も、親モデルのインスタンスからリレーションを構築する場合も、add メソッドを利用して集合にインスタンスを追加するような処理を行う必要があります。

また、リレーションの設定を行うことで、子モデル側には ManyToManyField のフィールド名のデータ属性が追加されますが、親モデル側には 子モデルのクラス名_set のデータ属性が追加されることになります。これらの違いも踏まえて実装する必要があるので注意してください。

さらに、多対多のリレーションを構築する際には、リレーションを構築する2つのインスタンス両方を事前にデータベースに保存しておく必要があるので注意してください。1対1のリレーションの時とは異なり、インスタンスは親モデル側のものを先に保存しても子モデル側のものを先に保存しても良いです。

例えば下記のように models.pyClubStudent を定義したとします。StudentManyToManyField を持っており、リレーションの設定先に Club を指定しているため、これらは多体多のリレーションが設定されていることになります。

多対多リレーションの設定例
from django.db import models

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

class Student(models.Model):
    name = models.CharField(max_length=256)
    clubs = models.ManyToManyField(Club)

そして、下記のような処理を行うことで、Student のインスタンスから Club のインスタンスの間にリレーションを構築することができます。前述の通り、リレーションを構築する前(add メソッドを実行する前)に各インスタンスを保存しておく必要がある点に注意してください。

子モデルからの多対多リレーションの構築例
student = Student(name='YamadaTaro')

club1 = Club(name='Soccer')
club2 = Club(name='BaseBall')

club1.save()
club2.save()
student.save()

student.clubs.add(club1)
student.clubs.add(club2)

また、下記のような処理を行うことで、Club のインスタンスから Student のインスタンスにリレーションを構築することができます。

親モデルからの多対多リレーションの構築例
student1 = Student(name='YamadaTaro')
student2 = Student(name='SatoHanako')

club = Club(name='Soccer')

club.save()
student1.save()
student2.save()

club.student_set.add(student1)
club.student_set.add(student2)

リレーションの構築手順まとめ

ここで、リレーションの構築手順についてまとめておきます。

まず、インスタンス間でのリレーションを構築するためには、モデルクラスに対してリレーションを設定しておく必要があります。このリレーションは、子モデルに対してリレーション設定用のフィールドを持たせることで実現できます。

子モデルにフィールドを追加することでリレーションを設定する様子

さらに、このリレーションをモデルクラスに設定しておくことで、子モデル側からはリレーション設定用のフィールドの フィールド名 と同じ名前のデータ属性から親モデル側のインスタンスとのリレーションの構築が可能となります。

子モデルのインスタンスからリレーションを構築する様子

それに対し、親モデル側からは、リレーションが設定された子モデルの クラス名 or クラス名_set のデータ属性からリレーションの構築が可能となります(このデータ属性におけるクラス名は自動的に小文字となります)。クラス名 or クラス名_set のどちらのデータ属性が利用できるかは、リレーション設定相手の子モデルが “1の関係” であるか “多の関係” であるかによって決まります。前者の場合は クラス名、後者の場合は クラス名_set のデータ属性が利用できるようになります。

リレーションの種類によって親モデル側に追加されるデータ属性が異なることを説明する図

また、リレーションの設定相手のモデルクラスが “1の関係” であるか “多の関係” であるかによって、リレーションの構築方法が異なります。要は、リレーションの構築相手が1つのみか、複数の可能性があるのかによって構築方法が異なります。

前者の場合は、前述のデータ属性から = でインスタンスを参照することでリレーションを構築することになり、後者の場合は、前述のデータ属性から add メソッドを実行することでリレーションを構築することになります。add メソッドの引数にはリレーションを構築したいインスタンスを指定する必要があります。

リレーションの構築の仕方が相手側が"1の関係"であるか"多の関係"であるかによって異なることを示す図

このように、リレーションの設定相手となるモデルクラスが “1の関係” であるか “多の関係” にあるかによって利用可能なデータ属性やリレーションの構築方法が異なる点がポイントとなります。

後述で説明する取得に関しても同様の点がポイントとなるため、これらを頭の中に入れた上で、続きを読んでいただければと思います。

スポンサーリンク

リレーション構築時は保存の順番に注意

続いて、リレーション構築時の注意事項について解説したいと思います。

基本的に親モデル側を先に保存する

その注意事項とは、基本的にリレーションが構築できるのは “既にレコードとしてデータベースに保存されているインスタンス” のみとなる点になります。

詳細を説明すると、まずテーブル観点で考えると、親モデルと子モデルの間に1対1のリレーション・1対多のリレーションを設定した場合、子モデル側のテーブルにリレーション構築相手のプライマリーキーを格納するためのフィールドが追加されることになります。このフィールドのプライマリーキーからリレーション構築相手のインスタンスが特定できるようになります。

リレーションの設定により親モデルのインスタンスのプライマリーキーを格納するフィールドが追加される様子

また、デフォルトでは、各モデルの持つ全フィールドは必須項目として設定されているため、子モデルのインスタンスは全フィールドに値を設定しないと保存できないようになっています。したがって、子モデルのインスタンスを保存するためには、リレーション構築相手となる親モデルのインスタンスのプライマリーキーを設定しておく必要があり、さらに、そのためには事前に親モデル側のプライマリーキーを確定しておく必要があります。

フィールドに未設定の項目があるためテーブルへの保存に失敗する様子

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

したがって、下記のように親モデル側のインスタンスを保存せずにリレーションを構築してから子モデル側のインスタンスを保存してしまうと、子モデル側の club フィールドに格納する値が未決定であるため、例外が発生することになります。

インスタンスの保存に失敗する例
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

student.club = club # clubはまだ保存されていない

student.save() # ここで例外発生
club.save()

発生する例外は具体的には下記のようなものになります。

save() prohibited to prevent data loss due to unsaved related object 'club'.

前述の通り、このプライマリーキーはデータベースにインスタンスが保存される際に自動的に採番されたり確定したりするので、上記の処理で発生する例外は、親モデル側のインスタンスを保存してから子モデル側のインスタンスを保存することで解決できます。

保存失敗の解決策1
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

student.club = club

club.save() # clubを保存
student.save() # 正常に保存完了

また、親モデル側のインスタンスをコンストラクタで作成するのではなく、マネージャーの create メソッドを利用して作成するようにすることでも解決することができます。

保存失敗の解決策2
student = Student(name='YamadaTaro')
club = Club.objects.create(name='BaseBall')

student.club = club

student.save()

単純にモデルクラスのコンストラクタを利用してインスタンスを生成しただけでは、そのインスタンスはレコードとしてデータベースに作成されません。save メソッド実行時に初めてレコードがデータベースに保存されることになります。

それに対し、create メソッドを実行した場合、まずデータベースにレコードが作成され、そのレコードに基づいたインスタンスを取得して利用することになるため、子モデル側のインスタンス保存前に親モデル側のインスタンスが必ずレコードとして保存された状態になります。したがって、前述のように save メソッドの実行順序を間違って例外が発生してしまうような問題を防ぐことができます。

create実行時とコンストラクタ実行時のレコードの扱いの違い

add メソッドでのリレーション構築前にインスタンスの保存を行う

また、同様の話にはなるのですが、add メソッドでリレーションを構築する場合、基本的には、add メソッドを実行するインスタンスと add メソッドの引数に指定するインスタンスは事前にレコードとしてデータベースに保存を行なっておく必要があります(”基本的には” と言っているくらいなので例外のケースもあります。これに関しては後述で解説します)。

例えば下の図のように親モデル側のインスタンスがデータベースに保存されており、この保存されているインスタンスから add メソッドの引数に子モデルのインスタンスを指定して実行した場合、子モデルのインスタンスがデータベースに保存されている場合は add メソッドに成功しますが、まだデータベースに保存されていない場合は add メソッドに失敗することになります。

つまり、親モデルのインスタンスと子モデルのインスタンスの両方をデータベースにレコードとして保存してから add メソッドを実行する必要があるということになります。

addメソッド実行時に親モデル側と子モデル側の両方のインスタンスをレコードとして保存しておく必要があることを示す図

これは、特に1対多のリレーションにおいて、親モデル側からリレーションを構築する際に厄介な制約となります。1対多のリレーションの場合、親モデル側からリレーションを構築する際には add メソッドの引数に子モデルのインスタンスを指定することになります。したがって、事前に子モデルのインスタンスをデータベースにレコードとして保存しておく必要があることになります。

ですが、子モデル側はリレーション構築用のフィールドを持っており、そのフィールドが必須項目の場合、そのフィールドにリレーション構築先の親モデルのインスタンスのプライマリーキーを設定しないとレコードとして保存できないことになります。

addメソッドを実行するために子モデルのインスタンスの事前保存が必要だが、そもそも子モデルのインスタンスの保存ができない様子

つまり、add メソッドでリレーションを構築するために子モデルのインスタンスを事前に保存しておく必要があるのにも関わらず、子モデルのインスタンスは事前にリレーションの構築を行なっておかないと保存ができないということになります。

したがって、例えば下記のようにレコードとして保存していないインスタンスを add メソッドに指定した場合も例外が発生しますし、

addメソッド実行時に失敗する例
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

club.save()
club.student_set.add(student) # ここで例外発生

student.save()

下記のようにレコードとして保存してから add メソッドの引数に指定しようとした場合も例外が発生することになります。

インスタンスの保存に失敗する例
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

club.save()
student.save() # ここで例外発生
club.student_set.add(student)

この問題の解決方法はいくつかあって、親モデル側からのリレーションの構築をやめるというのが一番簡単な方法になると思います。子モデル側からリレーションの構築を行えるのであれば add メソッドの利用は不要であるため、子モデル側からリレーションの構築を行えば良いです。

addによる例外発生の解決策1
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

club.save()
student.club = club
student.save()

また、add メソッド実行時に bulk=False を引数指定することでも解決できます。これにより、add メソッドに指定するインスタンスが保存されていなくても add メソッドが実行可能となります。

これは、bulk=False を引数指定することで、リレーション構築後に子モデルのインスタンスが保存されるように add メソッド内部の処理の流れが変更されるようになるからです。この引数指定により、add メソッド内部でリレーションを構築してから子モデルの保存を行うように処理が切り替わるようになっているはずです。

addによる例外発生の解決策2
student = Student(name='YamadaTaro')
club = Club(name='BaseBall')

club.save()
club.student_set.add(student, bulk=False)

さらに、リレーション構築用のフィールドを必須項目ではなく任意項目として設定することでも解決できます。これは、子モデルの定義において、ForeignKey の引数に null=True を設定することで実現できます。これにより、リレーション構築用のフィールドが空でも子モデルのインスタンスの保存が可能となります。

addによる例外発生の解決策3
class Student(models.Model):
    name = models.CharField(max_length=256)
    club = models.ForeignKey(Club, on_delete=models.CASCADE, null=True)

1対多のリレーションでさえ上記のように面倒なので、多対多の場合はもっと面倒そうに感じるかもしれないですが、実はそうではありません。多対多の場合は、単に add メソッド実行前に各インスタンスを保存しておけば良いだけになります。

多対多のリレーションの場合、親モデルのテーブルと子モデルのテーブルは、それぞれのテーブル単体で見れば独立しています。より具体的に言えば、リレーション構築相手を特定するためのフィールドを各テーブルは持ちません。これを管理するのは、多対多のリレーションを設定することで自動的に追加されるリレーション管理用の中間テーブルとなります。

親モデルと子モデルのテーブルはそれぞれ独立しているため、親モデルのインスタンスも子モデルのインスタンスもリレーション構築前に保存可能です。

多対多のリレーションにおいて親モデルと子モデルのテーブルがそれぞれ独立している様子

そのため、それぞれのインスタンスを保存してからリレーションを構築してやれば、add メソッドの引数に指定するインスタンスも既にレコードとしてデータベースに保存されており、問題なくリレーションを構築することが可能となります。

ただし、多対多の場合でも、各インスタンスを保存する前に add メソッドを実行すれば例外が発生することになります。リレーションを構築するためには、各インスタンスのプライマリーキーが必要です。そのため、各インスタンスを保存してから add メソッドを実行する必要がある点については注意が必要となります。

子モデル側からリレーションを構築する方がオススメ

ここまで解説してきたように、リレーションを構築する際には、各インスタンスの保存順序や add メソッドの実行順序が重要になります。これを間違うと例外が発生してリレーションの構築やインスタンスの保存に失敗することになります。

難しそうに感じるかもしれませんが、基本的には親モデルのインスタンスを先に保存する&子モデル側から参照を行なってリレーションを構築するようにすれば、前述の例外の多くは防ぐことができます。

これにプラスして、多対多のリレーションの場合は親モデルと子モデルの両方のインスタンスを保存してからリレーションを構築すれば、前述の例外の発生は防ぐことができるはずです。

ただ、これらを意識していても実行時に例外が発生することもあると思いますので、その場合はここまで説明してきた内容を思い出して解決するようにしていただければと思います。

ここまで解説してきたように、リレーションの構築は、リレーションの設定によって追加されるデータ属性を利用することで実現することが可能です。

子モデル側は、リレーションの設定を行うために持たせたフィールドの名前のデータ属性が追加されることになります。これは分かりやすいと思います。それに対し、特に1対多のリレーション、多対多のリレーションにおいて、親モデル側に追加されるデータ属性は 子モデルのクラス名_set となり、ちょっとややこしいですね。

実は、この1対多のリレーション、多対多のリレーションにおける親モデル側に追加されるデータ属性の名称は変更可能です。この変更を行うためには related_name を利用することになります。

related_name とは

この related_name は下記の2種類のフィールドのコンストラクタに指定可能な引数となります。

  • ForeignKey(1対多のリレーション)
  • ManyToManyField(多対多のリレーション)

この引数を指定することにより、リレーション設定により利用可能となる親モデル側のデータ属性名に好きなものを指定することができるようになります。

前述の通り、1対多のリレーションにおける親モデル、さらに多対多のリレーションにおける親モデルにおいては、リレーションの設定により 子モデルのクラス名_set データ属性が追加されて利用可能となります。このデータ属性に add メソッドを実行することでリレーションの構築を行うことができましたね!

related_nameの説明図1

このように、親モデル側のデータ属性に 子モデルのクラス名_set が追加されるのは related_name 引数を指定せずにリレーションの設定を行なった場合のみとなります。related_name 引数を指定すれば、子モデルのクラス名_set の代わりに、related_name に指定した名前のデータ属性が追加されることになります。

そして、この related_name に指定した名前のデータ属性から、リレーションの構築を行うことが可能となります。また、後述で説明するリレーション構築相手の取得なども行うことができます。

related_nameの説明図2

分かりやすい名前のデータ属性が利用可能になる

この related_name を指定するメリットは、リレーション設定により親モデルに追加されるデータ属性の名称を分かりやすくすることができる点にあります。

related_name を指定しなかった場合、前述の通り親モデル側のデータ属性に 子モデルのクラス名_set が追加されることになりますが、この名称がピンと来ないようなケースもありますし、親モデル側だけ特別な名前のデータ属性を利用する必要があって実装しにくいようなこともあります。

ですが、related_name を指定すれば、自身の好きな名前のデータ属性が追加されるようになります。そして、related_name を指定した場合、リレーションの設定の実装部分から、子モデルに追加されるデータ属性と親モデルに追加されるデータ属性が明確に分かるようになります。

related_nameを利用するメリット1

同じ親モデルに対するリレーションを区別できるようになる

また、メリットがあるだけではなく、この related_name の指定が必須となる場合もあります。具体的には、子モデルから同じ親モデルに対して2つ以上のリレーション(例えば1対多のリレーションと多対多のリレーションの2つ)を設定する場合、この related_name の指定が必須となります。

具体例として Twitter のツイートについて考えてみましょう!

ユーザーはツイートを複数回行うことができます。ですが、1つのツイートに関しては複数人のユーザーからではなく一人のユーザーから行われるものですので、ユーザーとツイートの間のリレーションは、親モデルをユーザー、子モデルをツイートとする1対多のリレーションであると考えられます。

ユーザーとツイートの間に1対多のリレーションが存在することを示す図

さらに、ユーザーはツイートに対して「いいね!」することができます。一人のユーザーは複数のツイートに対して「いいね!」することができ、さらに、1つのツイートに対して複数のユーザーが「いいね!」することも可能です。つまり、ユーザーとツイートの間には多対多のリレーションも存在することなります。

ユーザーとツイートの間に多対多のリレーションも存在することを示す図

したがって、ユーザーを管理するモデルクラスを User とすれば、ツイートを管理するモデルクラス Tweet は下記のように定義する必要があります。

Tweetクラスの例(NG)
from django.db import models
from django.contrib.auth.models import User


class Tweet(models.Model):
    text = models.CharField(max_length=256)
    tweet_user = models.ForeignKey(User, on_delete=models.CASCADE)
    favo_users = models.ManyToManyField(User)

この定義により、Tweet には tweet_user という名称のデータ属性と、favo_users という名称のデータ属性が追加されることになります。そして、これらのデータ属性を利用して User のインスタンスとの間にリレーションを構築することが可能となります。

それに対し、User 側にはどのようなデータ属性が追加されるでしょうか?

まず、1対多のリレーションが設定された場合、親モデル側には 子モデルのクラス名_set のデータ属性が追加されることになります。さらに、多対多のリレーションが設定された場合も、親モデル側には 子モデルのクラス名_set のデータ属性が追加されることになります。

つまり、上記のように定義を行なった場合、親モデル側に追加される2つのデータ属性は同じものになります。具体的には、この場合は追加される2つのデータ属性が両方とも tweet_set となってしまうことになります。同じデータ属性となるため、これらを区別してリレーションを構築するようなことが不可能です。

そのため、上記の例のように、リレーションを設定することで親モデル側に追加されるデータ属性が重複してしまうようなモデルクラスを定義をした場合はエラーが発生するようになっています。具体的には、下記のようなエラーが発生します。

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

ですが、このエラーに関しては related_name の指定により簡単に解決することができます。これは、related_name の指定により、親モデル側に追加されるデータ属性に好きな名前をつけることができるようになるからです。そのため、related_name の指定を行なってデータ属性の名前が重複しないようにしてやれば、上記のエラーを解決することができます。

具体的には、前述の Tweet クラスを下記のように変更することでエラーを解決することができます。

Tweetクラスの例(OK)
from django.db import models
from django.contrib.auth.models import User

class Tweet(models.Model):
    text = models.CharField(max_length=256)
    tweet_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tweets')
    favo_users = models.ManyToManyField(User, related_name='favos')

この場合、User に追加されるデータ属性が tweetsfavos となりますので、データ属性の重複は発生せず、先ほど紹介したエラーを防ぐことができます。

リレーションの設定を行いだすと上記のようなエラーを目にする機会は結構多いと思います。このエラーは related_name の指定によって解決できること、そして、related_name に指定するのは親モデル側に追加されるデータ属性の名前であることを覚えておくと、エラーが出ても難なく解決することができると思います。これらに関しては是非頭の中に入れておいてください。

リレーション構築先のインスタンス取得

続いて、インスタンスに対してリレーションが構築されているインスタンス or インスタンスの集合を取得(参照)する方法について解説していきます。

特定のインスタンスとリレーションが構築されているインスタンスを取得する様子

リレーションが構築されているインスタンスの取得は、親モデル側のインスタンスからでも子モデル側のインスタンスからでも行うことが可能です。

ただし、下記のリレーションの種類や、取得するのが親モデル側 or 子モデル側のどちらかによって取得方法が異なります。

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

具体的には、リレーション構築時と同様に、利用するフィールドや取得方法が、リレーション設定相手が “1の関係” or ”多の関係” のどちらであるかや親モデル or 子モデルのどちらであるかによって異なることになります。

スポンサーリンク

リレーションの設定相手が “1の関係” である場合

まず、リレーションの設定相手が “1の関係” である場合、すなわち、1対1のリレーションにおける親モデルと子モデル、1対多のリレーションにおける子モデルのインスタンスである場合の取得方法について解説していきます。

リレーションの設定相手のモデルが"1の関係"であるケースを示す図

この場合は簡単で、リレーションの設定によって利用可能となるデータ属性を単に参照すれば良いだけです。

例えば、1対多のリレーションの構築 で紹介した ClubStudent の例で考えれば、Student のリレーション設定相手の Club は1対多における “1の関係” の方のモデルであるため、下記のように Student のインスタンスのデータ属性を参照することで、そのインスタンスがリレーションを構築している相手側のインスタンスを取得することができます。

相手が1の関係の場合のインスタンス取得
student = Student.objects.get(id=5)

# studentとリレーションが構築されているインスタンスを取得
club = student.club

print(club.name)

説明を簡単にするために print で出力を行なっていますが、本来は取得したインスタンスをテンプレートファイルに埋め込んでページとして表示するような処理が必要となります。

リレーションの設定相手が “多の関係” である場合

続いて、リレーションの設定相手が “多の関係” である場合、すなわち、多対多のリレーションにおける親モデルと子モデル、1対多のリレーションにおける親モデルのインスタンスである場合の取得方法について解説していきます。

リレーションの設定相手のモデルが"多の関係"であるケースを示す図

この場合はリレーションを構築している相手が複数存在する可能性があるため、基本的に取得できるのは単なるインスタンスではなくインスタンスの集合となります。具体的には QuerySet となります。

リレーションを構築している相手全てを含む集合を取得するためには、リレーションの設定により利用可能となるデータ属性に all メソッドを実行させる必要があります。この all メソッドにより、リレーションを構築している相手のインスタンス全てが含まれる集合を取得することができます。

例えば、1対多のリレーションの構築 で紹介した ClubStudent の例で考えれば、Club のリレーション設定相手の Student は1対多における “多の関係” の方のモデルであるため、下記のように Club のインスタンスの student_set データ属性から all メソッドを実行させることで、そのインスタンスがリレーションを構築している相手側のインスタンスを全て含む集合を取得することができます。

相手が多の関係の場合のインスタンス取得1
club = Club.objects.get(id=1)

# clubとリレーションが構築されているインスタンスを取得
students = club.student_set.all()

for student in students:
    print(student.name)

リレーションを構築している相手全てを取得するだけでなく、条件を指定して条件に当てはまる相手のみを参照することも可能です。これを実現するのが filter メソッドとなります。これにより、リレーションを構築している相手のインスタンスの中から条件に当てはまるインスタンスのみが含まれる集合を取得することができます。

例えば下記は、Club のインスタンスとリレーションが構築されているインスタンスの集合の中から、nameYamada から始まる Student のインスタンスのみを含む集合を取得する例となります。

相手が多の関係の場合のインスタンス取得2
club = Club.objects.get(id=1)

# clubとリレーションが構築されているインスタンスを取得
students = club.student_set.filter(name__istartswith='Yamada')

for student in students:
    print(student.name)

取得できるのは集合ですのでイテラブルなオブジェクトとして扱うことができ、上記のように for 文で各インスタンスを取得することができます。

また、related_name で解説したように、related_name を指定している場合、利用するデータ属性は 子モデルのクラス名_set ではなく related_name で指定した名前のものとなります。

テンプレートファイルからの取得

ここまでの説明は主にビューからのリレーション構築相手の取得方法の説明になります。

Django においてはテンプレートが存在し、テンプレートファイルから、特定のインスタンスとリレーションが構築されている相手のインスタンスを取得(参照)したい場合もあります。

このような場合も、基本的にはここまで説明してきた方法でリレーション構築相手を取得することが可能です。

具体的には、リレーションの設定相手が “1の関係” であれば、リレーションの設定によって利用可能となるデータ属性を単に参照してやれば良いだけになります。

例えば、1対多のリレーションの構築 で紹介した ClubStudent の例において、Student クラスのインスタンスがコンテキストの 'student' キーに設定されてテンプレートファイルから参照可能となっている場合、この student が所属するクラブは student.club によって取得可能です。したがって、下記のような処理をテンプレートファイルに実装しておくことで、この student が所属するクラブの名前を HTML に埋め込むことができます。

studentの所属するクラブの出力
{{ student.club.name }}

それに対し、リレーションの設定相手が “多の関係” の場合は、リレーションの設定によって利用可能となるデータ属性を単に参照するのではなく、そのデータ属性に all メソッドを実行させる必要があります。これにより、その返却値としてリレーションが構築されているインスタンスの集合を得ることができます。

例えば、1対多のリレーションの構築 で紹介した ClubStudent の例において、Club のインスタンスがコンテキストの 'club' キーに設定されてテンプレートファイルから参照可能となっている場合、この club に所属するインスタンスの集合は club.student_set.all によって取得可能です(テンプレートファイルの場合、all() ではなく all と記述する必要がある点に注意してください)。したがって、下記のような処理をテンプレートファイルに記述しておくことで、この club に所属する全生徒の名前を HTML に埋め込むことができます。

clubに所属する生徒の出力
{% for student in club.student_set.all %}
    {{ student.name }}
{% endfor %}

また、テンプレートファイルにおいても、追加されるデータ属性の名前の設定(related_name) で解説した related_name の指定が有効であり、related_name を指定している場合は、利用するデータ属性は 子モデルのクラス名_set ではなく related_name で指定した名前のものとなります。

ひとまず、上記の手順で views.py やテンプレートファイルでリレーションの構築相手のインスタンスを取得することは可能になると思います。ただし、実は上記のような書き方をするとデータベースに対するクエリの発行回数が多くなって処理が遅くなってしまう場合があります。もしかたら既にご存知の方もおられるかもしれませんが、N + 1 問題という問題が発生する可能性があります。

とりあえず、リレーション構築相手のインスタンスの取得を実現したいだけであれば上記の手順をそのまま実行しても良いですが、処理速度の向上も図りたいような場合や、実際にウェブアプリを公開するような際には、このクエリの発行量を減らしてやった方が良いです。

N + 1 問題と、N + 1 問題の解決方法については Django 連載における下記ページで解説していますので、この辺りについては下記ページを読む際に理解していただければと思います!

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門13】N+1問題とselect_related・prefetch_relatedでの解決

スポンサーリンク

フォームとの連携

リレーションを利用しだすと、フォームからリレーションの構築相手を設定できるようにしたい場合が出てきます。

例えば、ここまでの説明の中にも登場したクラブと生徒の例で考えると、ユーザーがフォームから生徒のレコードを追加する際に、生徒の所属するクラブが選択できると便利です。

実例で示すと、下の図のようなフォームであれば、追加する生徒に関するフィールドだけでなく所属するクラブを選択するフィールドも存在するため、生徒の追加と所属するクラブの設定をユーザーの操作によって行えるようになります。

ModelChoiceFieldの説明図

このフィールドは、リレーションの観点で考えると、生徒のレコードを追加する際に、その追加する生徒とリレーションを構築する相手となるクラブを選択するフィールドであると考えられます。このようなフィールドを持つフォームは、フォームクラスに ModelChoiceField を持たせることで実現することが可能です。

ModelChoiceField は特定のモデルクラスのインスタンスの候補を選択するためのフィールドです。このフィールドがクリックされた際に表示するインスタンスの候補の選択肢は、ModelChoiceFieldqueryset 引数に選択肢として表示するインスタンスの集合(クエリーセット)を指定することで設定可能です。

具体例を示すと、下記のようにフォームクラスを定義すれば、単に生徒の名前の入力受付を行うだけでなく、クラブも選択可能なフォームを実現することができます。

インスタンス選択用のフィールド
class StudentForm(forms.Form):
    name = forms.CharField()
    club = forms.ModelChoiceField(queryset=Club.objects.all())

club フィールドは ModelChoiceField のインスタンスであり、queryset 引数に Club.objects.all() を指定しているため、このフィールドがクリックされた際には、Club に対応するテーブルの全レコードが選択肢として表示されることになります。また、all() ではなく filter() を利用すれば、特定の条件を満たすレコードのみが選択肢として表示されるようになります。

ちなみに、フィールドで選択肢として表示される文字列は通常下記のような形式で表示されることになります。

モデルクラス名 object (プライマリーキーの値)

この文字列を変更したい場合は、モデルクラスに __str__ メソッドを実装しておく必要があります。__str__ メソッドについては下記ページで解説を行なっていますので、詳しく知りたい方は下記ページをご参照ください。

モデルの__str__メソッドの説明ページアイキャッチ 【Django】モデル(Model)の__str__メソッドとは?

あとは、フォームからデータが送信されてきた際に、そのデータから club フィールドの値を受け取り、その値を新たに追加した Student のインスタンスの club データ属性に設定してやれば、生徒とクラブの間にリレーションが構築され、追加した生徒が所属するクラブを管理することができるようになります。

この時の処理のイメージは下記のようなものになります。

選択されたインスタンスとのリレーション構築
def create_student(request):
    if request.method == 'POST':
        form = StudentForm(request.POST)

        if form.is_valid():
            name = form.cleaned_data.get('name')
            club = form.cleaned_data.get('club')

            student = Student(name=name)

            # フォームで選択されたインスタンスとリレーションを構築
            student.club = club
            student.save()

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

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

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

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

上記ページで紹介しているウェブアプリでは、モデルクラスとして UserComment の2つを定義しています。ですが、まだリレーションを利用していないため、各モデルクラスのインスタンスは独立したものとなっています。今回、リレーションを利用することで、各モデルクラスの間に関係性を持たせます。

具体的には、下記のリレーションを設定していきます。

  • User を親モデル・Comment を子モデルとする1対多のリレーション

これは UserComment の投稿者として管理するためのリレーションとなります。User は複数の Comment を投稿することができますが、各 Comment に対する投稿者は一人のみであるため1対多の関係としています。

UserとCommentの関係図

リレーションの設定

ということで、まずは models.py を変更して上記で挙げた2つのリレーションの設定を行なっていきたいと思います。

結論としては、次のように models.py を変更することで上記の2つのリレーションを設定することが可能です。

リレーションの設定
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]

下記ページの 掲示板アプリでモデルを利用してみる で紹介した models.py との違いは、Commentuser フィールドを追加した点のみとなります。

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

この新たに追加した userUserComment との間にリレーションを設定するフィールドになります。ForeignKey を利用しているため、1対多のリレーションが設定されることになります。また、Comment にフィールドを持たせているため、Comment が子モデルとなり、ForeignKey の第1引数に指定している User が親モデルとなります。したがって、Comment が1対多における “多の関係”、User が1対多における “1の関係” のモデルとなります。

このリレーションの設定によって Comment のインスタンスからはデータ属性 user が利用可能となり、このデータ属性から、そのインスタンスの投稿者の指定や取得を行うことができます。

Commentのインスタンスからリレーション構築相手のUserのインスタンスをセット・取得する様子

それに対し、上記のリレーションの設定によって User のインスタンスからは related_name に指定した comments という名前のデータ属性が利用可能となります。そして、この comments にメソッドを実行させることで、このインスタンスが投稿したコメントの追加やコメントの集合の取得を行うことが可能となります。

また、上記では ForeignKey の引数として null=True を指定していますが、これは user フィールドの指定を任意とするための指定となります。

この引数を指定しない or null=False を指定した場合、user フィールドには必ず User のインスタンスをセットしなければならないという制約が発生します。Comment のインスタンスは User のインスタンスとリレーションの構築を行わない限り、データベースに保存することができません。

ForeignKeyにおけるnull引数の意味合いの説明図

そうなると、掲示板アプリでモデルを利用してみる の動作確認時に投稿したコメントは全てその制約に違反していることになり、上記の models.py に変更した後に投稿済みのコメントを上手く扱うことが出来なくなってしまいます。せっかくなので、前回の動作確認時に投稿したコメントもそのまま利用できるようにしたかったため、null=True を指定するようにしています。

コメントに対して必ず投稿者を設定するようにしたい場合は、null=True は指定しないようにしてください。

スポンサーリンク

フォームの変更

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

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

今回のリレーションの設定によって、各コメントの投稿者を管理できるようになりました。そのため、下の図のように、コメント投稿フォームにフィールドを追加し、コメント投稿時に投稿者をフォームから指定できるようにしたいと思います。

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

このようなフォームを実現するために、forms.py を下記のように変更します。

forms.py
from django import forms
from .models import User

class RegisterForm(forms.Form):
    username = forms.CharField()
    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()

フォームとの連携 で解説したように、上記の PostForm のように ModelChoiceField を利用することでインスタンスの選択を行うためのフィールドを追加することができます。

また、このフィールドをクリックした際に表示されるインスタンスの選択肢は queryset 引数で指定されたクエリーセットに含まれるインスタンスとなります。上記では User.objects.all() を指定しているため、User に対応するテーブルに存在する全レコードが選択肢として表示されることになります。

ビューの変更

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

models.py でリレーションの設定を行いましたので、views.py では実際にインスタンス同士にリレーションの構築を行なっていきます。

まず、先ほどのフォームの変更によって、コメント投稿時に投稿者として User のインスタンスが user フィールドで選択されるようになります。この選択された User のインスタンスはフォームからのデータ送信時に text と一緒に送信されてくることになるため、その User のインスタンスをビューの関数でを受け取り、その User のインスタンスと投稿によって新規作成される Comment のインスタンスとの間にリレーションを構築するようにしていきたいと思います。

また、先ほど変更を行なったフォームは PostForm であり、この PostFormviews.py における post_view で扱っているため、post_view で上記のような動作を実現していくことになります。

具体的には、views.py における post_view を下記のように変更します。

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 を取得したりすることができるようになったことになります。

テンプレートの変更

次は、その取得を行うようにウェブアプリを変更していきたいと思います。今回は、この取得は全てテンプレートから行いたいと思います。そのため、テンプレートファイルの変更を行なっていきます。

コメントからの投稿者の取得

前述の通り、Comment のインスタンスと User のインスタンスが関連付けされたことにより、Comment のインスタンスから投稿者の User を取得することができるようになります。そのため、 コメント一覧ページでは各コメントの投稿者を取得して HTML に埋め込み、さらにコメント詳細ページでは、そのコメントの投稿者を取得して HTML に埋め込むように変更を行なっていきます。

前者のコメント一覧ページはテンプレートファイル comments.html から生成されるようになっていますので、まずは comments.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>
            <td>{{ comment.user.username }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

掲示板アプリでテンプレートを利用してみる で示した comments.html との違いは、コメント一覧の表に投稿者の列を追加した点になります。

Djangoのビューについての解説ページアイキャッチ 【Django入門3】ビューの基本とURLのマッピング

投稿者の列の各要素は {{ comment.user.username }} としており、この部分は comment との間にリレーションが構築されている User のインスタンスの username、すなわち投稿者の名前に置き換えられて 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">
<tr><td>投稿者</td><td><a href="{% url 'user' comment.user.id %}">{{ comment.user.username }}</a></td></tr>
<tr><td>本文</td><td>{{ comment.text }}</td></tr>
<tr><td>投稿日</td><td>{{ comment.date|date }}</td></tr>
</table>
{% endblock %}

こちらも変更点は comments.html の時とほぼ同様で、投稿者の名前を HTML に埋め込むようにしています。また、投稿者の名前には、url タグを利用してそのユーザーの詳細ページへのリンクを貼っています。つまり、このコメントの詳細ページはユーザーの詳細ページにつながっていることになります。このように、リレーションの利用により今まで独立していたコメントとユーザーの間に繋がりが発生し、ページとページの間にも関連性を持たせやすくなります。

投稿者からのコメントの取得

次は、ユーザー一覧ページとユーザー詳細ページにコメントの情報を表示するようにしていきたいと思います。

まず、ユーザー一覧ページでは、ユーザー名の横に、そのユーザーのコメント回数を表示するようにしたいと思います。これを実現するため、ユーザー一覧ページの基になる 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 の全コメントの長さ、すなわち user のコメント総数が HTML に埋め込まれることになります(|length は Django テンプレート言語におけるフィルターとなります)。

今回設定した1対多のリレーションにおいて、User は親モデルであるため、related_name に指定した名前 comments のデータ属性を利用してリレーション構築相手を取得する必要がある点、さらに、User のリレーション設定相手となる Comment は1対多のリレーションにおける “多の関係” にあるモデルとなるため、メソッドを利用してリレーション構築相手となるインスタンス全てを表示することができる点がポイントになると思います(ここでは all を利用)。

最後にユーザー詳細ページを変更していきます。ユーザー詳細ページでは、ユーザーのコメント履歴を表示するようにしていきたいと思います。そのために、下記のように 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 に埋め込むようにすることで、ユーザーのコメント履歴の表示を実現しています。

ここでも、各コメントの本文にリンクを貼っており、ユーザー詳細ページからコメント詳細ページに遷移することができるようになっています。

スポンサーリンク

動作確認

ソースコードの変更の解説は以上となります。

最後に動作確認を行なっておきましょう!

マイグレーションの実行

下記ページで解説したように、モデルの変更を行なった際には、それをデータベースに反映するためにマイグレーションを実行する必要があります。

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

ということで、最初にマイグレーションを行なっておきましょう!このマイグレーションは、プロジェクトフォルダの直下、つまり、manage.py が存在するフォルダで下記コマンドを実行することで行うことができます。

% python manage.py makemigrations
% python manage.py migrate

今回は models.py を変更して Commentuser フィールドを追加したため、マイグレーションの実行により、下の図のように forum_comment のテーブルに新たなフィールド(カラム)が追加されることになります。そして、この追加されたフィールドにはリレーション構築相手のインスタンスを特定するための情報が格納されることになります。

テーブルにリレーション構築用のフィールドが追加される様子

開発用ウェブサーバーの起動

マイグレーションが完了した後は、いつも通り Django 開発用ウェブサーバーの起動を行います。マイグレーションを実行した時と同じフォルダで下記コマンドを実行すれば、Django 開発用ウェブサーバーが起動します。

% python manage.py runserver

リレーションの効果の確認

ここからは今回追加したリレーションの効果を確認していきたいと思います。

まずウェブブラウザを開き、アドレスバーに下記 URL を指定してください。

http://localhost:8000/forum/users/

そうすると、ユーザー一覧ページが表示されるはずです。もし、前回の動作確認時にユーザーを登録している場合、そのユーザーが一覧表の中に表示されているはずです。

ユーザー一覧に今まで登録したユーザーが表示される様子

もし、ユーザーをまだ登録していない方がおられましたら、ナビゲーションバーの ユーザー登録 リンクをクリックしてユーザー登録フォームを表示し、ユーザーの登録を行なってください。

ユーザー登録フォーム

ユーザーの登録が完了したら、次はナビゲーションバーの コメント投稿 リンクをクリックしてください。すると、下の図のようなコメント投稿フォームが表示されるはずです。

表示されるコメント投稿フォーム

今まではコメント投稿フォームに text フィールドしか表示されていなかったのですが、今回フォームを変更したため、user フィールドが表示されるようになっているはずです。そして、この user フィールドはプルダウンメニューになっており、クリックすれば登録済みのユーザー一覧が表示されます。

userフィールドをクリックすれば登録済みのユーザー一覧が表示される様子

まずは、user フィールドから投稿者となるユーザーを選択してください。その後、text フィールドに適当なコメントを入力して 送信 ボタンを押してコメントの投稿を行なってください。

コメントを投稿する様子

送信 ボタンを押すと自動的にコメント一覧ページに遷移するはずです。注目していただきたいのが一覧の一番下のコメントで、コメント本文の右側に投稿者の名前が表示されているはずです。この投稿者は、このコメントにリレーションが構築された User のインスタンスとなります。

コメント一覧に投稿者の名前が表示される様子

また、前回のウェブアプリの動作確認時にコメントを投稿していた場合、そのコメントの投稿者の欄は None 表示になっていると思います。これは、今までのウェブアプリではコメントに投稿者の設定ができないようになっており(Commentuser フィールドが存在しなかった)、投稿者が未設定の場合に None と表示されるようになっているためです。

続いて、先ほど投稿したコメントの本文部分のリンクをクリックしてみてください。そうすると、そのコメントの詳細ページに遷移し、そこに投稿者の名前が表示されているはずです。

コメントの詳細が表示される様子

次は、この投稿者の名前部分のリンクをクリックしてみてください。これにより、その投稿者の詳細ページが表示されるはずです。さらに、その投稿者の詳細ページには投稿履歴が表示されており、そこに先ほど投稿したコメントが表示されているはずです。

ユーザーの詳細が表示され、そこにそのユーザーのコメント履歴が表示される様子

最後にナビゲーションバーの ユーザー一覧 リンクをクリックしてユーザー一覧ページを表示してみましょう!ユーザー一覧の右側にコメント数の列が追加されており、そこに各ユーザーのコメント数が表示されているはずです。

ユーザー一覧に各ユーザーのコメント総数が表示される様子

ここまで説明した通りに動作確認をしてくださった方であれば、最初のコメント投稿時に選択したユーザーのコメント数が 1 になっていることが確認できると思います。

コメント投稿を繰り返し行えば、コメント投稿時に選択したユーザーのコメント数が順次増えていくことも確認できると思いますし、そのユーザーの詳細ページでコメント履歴が増えていくことも確認できると思います。

コメントの投稿によりコメント数やコメント履歴が増えていく様子

この動作確認で一番のポイントになるのは、リレーションの設定によって User と Comment の情報を一緒に表示できていることが確認できる点になります。

今までは完全に UserComment が独立していたのですが、リレーションを利用することで互いのインスタンスの間に関連性が生まれ、User の情報を表示する際に Comment の情報を表示したり、逆に Comment の情報を表示する際に一緒に User の情報を表示するようなことも可能となります。

このように、リレーションを利用すれば2つのモデルクラスの間に関連性を持たせ、一方のモデルクラスのインスタンスから他方のモデルクラスのインスタンスを辿って情報を表示するようなことが可能となります。今回は UserComment という2つのモデルクラスの間にリレーションを設定することでユーザーのコメント履歴を表示するような機能等を実現したように、リレーションを利用することで新たな機能を実現することも可能です!

まとめ

このページでは、Django のリレーションについて解説を行いました!

基本的に、モデルクラスは互いに独立した状態となっていますが、このリレーションを用いることでモデルクラスの間に関連性を持たせることが可能となります。

そして、このリレーションにより、一方のモデルクラスのインスタンスから他方のモデルクラスのインスタンスの情報を取得するようなことが可能となり、これにより様々な機能を実現することが可能となります。

リレーションを設定するためには、モデルクラスに下記のいずれかのフィールドを持たせる必要があります。それぞれ設定可能なリレーションの種類が異なるため、実現したいウェブアプリに合わせて適切にフィールドを選択する必要があります。

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

また、リレーションの種類によってインスタンス同士の間にリレーションを構築する方法やリレーションの構築相手のインスタンスの取得方法が異なるので注意してください。ポイントは、構築相手となるインスタンス1つのみであるか複数であるかを意識する点になると思います。

ザーッとリレーションについて説明しましたが、リレーションの意味合いや各種リレーションの違いについては理解しやすいものの、実際に実装を行うとなると結構難しく感じるのではないかと思います。リレーションを使いこなすためには、実際にリレーションを利用するウェブアプリを開発し、リレーションに慣れていくことが重要です。例えば、自身の使ったことのあるウェブアプリを思い浮かべ、その際にどんなリレーションの設定が必要であるかを考え、さらに実際に実装してみることで、リレーションにも慣れることができると思います!

また、これは余談になりますが、今回紹介したウェブアプリの例では、コメント投稿時に投稿者となるユーザーを手動で選択するようにしましたが、下記ページで紹介しているログイン機能を利用すれば、投稿時に投稿者となるユーザーを手動で選択しなくても、ログイン中のユーザーを自動的に投稿者に設定するようなことも可能になります。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

さらに、下記ページで紹介している自己参照型リレーションを利用すれば、コメントの返信機能も簡単に実現することが可能となります。紹介しているウェブアプリの例は非常に単純なものではありますが、自身でカスタマイズに挑戦してみれば、自身の開発力も高めることができると思います。

Djangoにおける自己参照型リレーションの解説ページアイキャッチ 【Django】自己参照型リレーションについて分かりやすく解説

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

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

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