サイトアイコン ITC Media

【Django】ForeignKeyの基本から応用まで実例付で解説

(最終更新月:2022年3月)

✔このような方へ向けて書かれた記事となります

「Django ForeignKeyとは?役割は?」

「Django ForeignKeyの書き方が知りたい!」

「Django ForeignKeyを使った実例が知りたい!」

✔当記事を通じてお伝えすること

Django modelのForeignKeyとは?役割をわかりやすく解説

ForeignKeyとは、Djangoのモデルフィールドのうちの一つで、別のモデルと一対多の関係を作ることができるフィールドになります。

Djangoのモデルフィールドについてはこちらで解説しています。

SNSのTwitterを例にTweetというモデルで考えてみます。

Tweetには、コンテンツのフィールド、投稿日のフィールドなどに加えて、ユーザーというForeignKeyフィールドがあると考えられます。

その場合、Tweetモデルからは一つのユーザーのみ参照ができるのに対し、ユーザーからは複数のTweetが参照できることになります。

これが一対多の考え方になります。

ForeignKeyフィールドの書き方

ForeignKeyフィールドの基本的な書き方は下記の通りです。

フィールド名 = models.ForeignKey(モデルクラス, on_delete=models.CASCADE)

TweetモデルとUserモデルと紐付けるという例では下記のようになります。

class Tweet(models.Model):
    #....他のフィールド
    user = models.ForeignKey(User, on_delete=models.CASCADE)

また、場面によってはon_deleteだけでなく他のフィールドオプションも必要になることもありますので、それぞれ簡単に解説していきます。

on_delete

on_deleteオプションでは、対象のモデルが削除された場合にもう片方をどうするかということを指定します。

下記の説明においては、上記のTweetを例に親データが「Userオブジェクト」、子データが「Tweetオブジェクト」と考えると少しわかりやすいかもしれません。

on_deleteオプション説明
models.CASCADE親データが削除された場合、子データも削除される
models.PROTECT子データを一つでも持つ親データを削除することはできない
models.SET_DEFAULT親データが削除された場合、子データにはデフォルト値をセットする
models.SET_NULL親データが削除された場合、子データにNULLをセットする
models.DO_NOTHING親データが削除された場合でも、子データには何もしません

related_name

親データから子データ一覧を逆参照する際の名前を指定できます。

書き方の例はこちらの通りです。

user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_tweets")

通常ですと、下記の通りで逆参照となります。

user1 = User.objects.get(pk=1)
tweets = user1.tweet_set.all()

related_nameを使うと下記のようになります。

user1 = User.objects.get(pk=1)
tweets = user1.user_tweets.all()

特に、同じモデルに対して複数のForeignKeyフィールドを設定する場合などは、必須のオプションになります。

to_field

子データから参照する親データのフィールドを指定します。

user = models.ForeignKey(User, on_delete=models.CASCADE, to_field="email")

「email」はユーザーモデル内のフィールドの一つになります。

実例|日報モデルとユーザーモデルを紐付けよう

下記のモデルに、ForeignKeyフィールドを追加していきます。

from django.db import models

class NippoModel(models.Model):
    title = models.CharField(max_length=100, verbose_name="タイトル")
    content = models.TextField(max_length=1000, verbose_name="内容")
    timestamp = models.DateTimeField(auto_now_add=True)

ForeignKeyフィールドの追加

ForeignKeyフィールドの追加後は下記のコードとなりました。

from django.db import models
from django.contrib.auth import get_user, get_user_model

User = get_user_model()

class NippoModel(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100, verbose_name="タイトル")
    content = models.TextField(max_length=1000, verbose_name="内容")
    timestamp = models.DateTimeField(auto_now_add=True)

変更点は2点です。

  1. ユーザーモデルクラスの取得
  2. 「user」というForeignKeyフィールドの追加

今回の日報モデルはあくまでも該当するユーザーが作成した日報になりますので、on_deleteオプションは「models.CASCADE」が適しています。

マイグレーションをする前に

モデルクラスを変更したら、マイグレーションの実行が必要です。

ただ、既存のデータが存在する場合、既存データのForeignKeyフィールドにデフォルト値などを設定する必要が出てきます。

そのため、スーパーユーザーのIDを事前に確認しておくとスムーズです。

管理画面でユーザーの編集ページへ進むと、アドレスバーから「pk」が読み取れます。

マイグレーションの実行

マイグレーションを実行します。

$python manage.py makemigrations

ただ、いつもと違うメッセージが出てきましたね。

You are trying to add a non-nullable field 'user' to nippomodel without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

メッセージは「デフォルト値なしではフィールドの登録ができない」といった内容です。

新たにフィールドを追加した場合、原則必須項目と認識されます。

そのため、このままだと既存の日報データでは、必須項目である「userフィールド」が空欄となってしまい、エラーとなってしまったのです。

方法は以下のどちらか。

今回はこのまま進めていくので、「1」を入力します

Select an option: 1

するとどんな値を入れるか聞かれます。

Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1

デフォルト値に、ユーザーのpkを入力する必要がありますので、先ほど調べたスーパーユーザーのpkを入力しましょう。

エラーなく終了しましたら、データーベースの書き込みです。

$python manage.py migrate

エラーが出なければ完了となります。

確認

念の為管理画面へ行き、確認しましょう!

デフォルトで設定したスーパーユーザーが保存されています。

問題発生!?

ただし、このままだと日報の新規作成画面で問題が生じます。

新たなユーザーを作ってみて下さい。

localhost:8000/accounts/signup/

流れに沿ってユーザーを作成し、新しいユーザーでログインします。

localhost:8000/accounts/login/

ログイン後、日報を新たに登録するといかがでしょうか?

新規登録画面にユーザーフィールドも追加されています。

そのため、ユーザーを選ぶ画面で全ユーザーが出てきてしまいます

これは現実的ではないですね。

ユーザーの登録は、選択によるものではなくログインユーザー自身を自動で保存する方法が適しています。

次章でその方法を解説していきます。

【※重要】フォームクラスでログインユーザーを自動保存にする方法

フォームフィールドで「ユーザー」を非表示にする

現在のModelFormでは、fields変数は「__all__」としているため、「user」フィールドも表示されてしまいます。

まずは、excludeを使い、「user」フィールドを非表示にしましょう。

nippo > forms.py

class NippoModelForm(forms.ModelForm):
    class Meta:
        model = NippoModel
        exclude = ["user"]

新規登録画面で、ユーザーフィールドは非表示になりました。

一見「これで完成?」とも思えますが、新規保存を試みるとエラーが発生します。

入力必須な「user」フィールドにデータが保存されませんのでエラーとなってしまいます。

ここで、入力する情報とは別にログインユーザーの情報を自動で保存する機能を追加していきます。

ビュークラスからフォームクラスへユーザー情報を渡す

ModelFormクラスには、「request.user」という属性は存在しません。

ビュークラスから渡す必要がありますので、下記の通り記述していきます。

nippo > views.py

class NippoCreateFormView(CreateView):
    template_name = "nippo/nippo-formclass.html"
    form_class = NippoModelForm
    success_url = reverse_lazy("nippo-list")

    def get_form_kwargs(self):
        kwgs = super().get_form_kwargs()
        kwgs["user"] = self.request.user
        return kwgs

get_form_kwargsメソッドを使い、フォームへ「self.requets.user」を渡します。

nippo > forms.py

class NippoModelForm(forms.ModelForm):
    #...(他のコードは割愛してます)

    def __init__(self, user=None, *args, **kwargs):
        #...(他のコードは割愛してます)
        self.user = user
        super().__init__(*args, **kwargs)

「__init__」関数で「user」を引数で受け取り、self.userへ格納します。

request.userの受け渡しは完了です。

フォームクラスのsaveメソッドを使ってユーザーを保存する

saveメソッドは下記のような記述となります。

def save(self, commit=True):
    nippo_obj = super().save(commit=False)
    if self.user:
        nippo_obj.user = self.user
    if commit:
        nippo_obj.save()
    return nippo_obj

saveメソッドの構造は下記の様になります。テンプレートとして覚えておくと便利です。

def save(self, commit =True):
    インスタンス名 = super().save(commit=False)
    #保存前の処理
    if commit:
        #保存の実行
    return インスタンス名

全体のコードは下記の通りです。

class NippoModelForm(forms.ModelForm):
    class Meta:
        model = NippoModel
        exclude = ["user"]

    def __init__(self, user=None, *args, **kwargs):
        #...(他のコードは割愛してます)
        self.user = user
        super().__init__(*args, **kwargs)

    def save(self, commit=True):
        nippo_obj = super().save(commit=False)
        if self.user:
            nippo_obj.user = self.user
        if commit:
            nippo_obj.save()
        return nippo_obj

まとめ

当記事の内容をまとめていきます。

ForeignKeyとは、Djangoのモデルフィールドのうちの一つで、別のモデルと一対多の関係を作ることができるフィールドです。

ForeignKeyフィールドの構成は下記のようになります。

フィールド名 = models.ForeignKey(モデルクラス, **フィールドオプション)

フィールドオプションは主に3つです。

  1. on_delete
  2. related_name
  3. to_field

場面により使い分けることが必要となります。

また、既存のモデルにForeignKeyを設定する場合のステップも実例を元にご紹介しました。

  1. フィールドの記述
  2. デフォルト値の設定
  3. マイグレーションの実行

ForeignKeyフィールドを使用した場合、ModelFormのフィールドを調整する必要がある場面もありますので、注意が必要です。

以上でForeignKeyに関しての解説は完了です。

ただ、ブログを通じて作成している日報アプリでは、新たなエラーが発生してしまいます。

ログインしていないユーザーが日報を作成すると、下記の画面となります。

Anonymousユーザーは保存できないとエラーです。

ログインユーザーを自動保存できるようにしたため、ログインしていないユーザーが日報を作るとエラーになるのは当然です。

次回の記事では、日報の作成ページへ「ログインユーザー以外アクセスできない」アクセス制限を設定する方法について解説していきます。

モバイルバージョンを終了