前のレッスンでは、Photoモデルを作ることで画像を表示させました。今回のレッスンでは、Photoモデルにさらに変更を加えて、その投稿は「どのUserが投稿したものか」と「どのカテゴリーに属するものか」をわかるようにしていきます。

結論から言うと、Photoモデルに、userフィールドとcategoryフィールドを追加します。これらのフィールドには、他のモデルから生成されたインスタンスが保存されることになります。

つまり、userフィールドにはUserモデルから生成されたユーザーインスタンス、categoryフィールドにはCategoryモデルから生成されたインスタンスが保存されるようにします。

ForeignKeyを使ってモデル同士を紐づける

これまでにCharField、TextField、ImageField、DateTimeFieldなどを学習してきましたので、文字列型、画像ファイル、日時型を保存するフィールドはもう作れますよね。今回は、「モデルから作られたインスタンス」を保存できるフィールドを作ることになります。userフィールドには、Userテーブルにあるインスタンスのうちの1つが保存されます。

それでは実際に、userフィールドを作ってみましょう。Userモデルを使うのでimport文も忘れないでください。

~/PhotoService/app/models.py

from django.db import models
from django.contrib.auth.models import User   ← 追加

class Photo(models.Model):
    title = models.CharField(max_length=150)
    comment = models.TextField(blank=True)
    image = models.ImageField(upload_to = 'photos')
    # 追加
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title  

インスタンスを保存するフィールドを作るためにはForeignKeyというものが使えます。ForeignKeyでは、そのフィールドと紐づけるモデルを第一引数に指定します。userフィールドには、Userインスタンスを保存したいので、上記のようになります。

on_deleteでは、紐づけられたインスタンスが削除されたときの挙動を定義しています。具体的に上図の例で説明すると、『Rola』というユーザーインスタンスが削除された場合に、それと紐づいたPhotoインスタンス(Rolaの2つの投稿『id:2 初めての富士山』『id:3 美味しくできた』)も一緒に削除するのか、それとも投稿だけは残しておくのか、を定義しています。

on_delete=models.CASCADEのようにすると、Userインスタンスを削除すると、それと同時にそのユーザーと紐づいた投稿も全て削除されます。on_delete=models.PROTECTとすれば、特定のUserインスタンスを削除しようとしても、そのユーザーと紐づいた投稿が存在する場合は削除できないようになります。on_deleteの詳しい説明はこちら

null=Falseのフィールドを追加する

ここまでできたら、マイグレーションファイルを作ってみましょう。

すると、こんなメッセージが出るはずです。

userフィールドでは、null=Trueと指定していないので、デフォルトでnull=False、つまり空欄を許容しない設定になっています。Photoインスタンスのuserフィールドには、何かしらのUserインスタンスが保管されていないといけないということです。

今後新しくPhotoインスタンスを投稿するときは、Userを選んで保存すればいいだけなのですが、前回のレッスンまでに作成した既存のPhotoインスタンスに関しても、userフィールドは空欄にしてはいけないため、何かしらのUserを保存して必要があります。

既存のPhotoインスタンスのuserフィールドにUserを保存するためには、エラーメッセージにある通り2つやり方があります。

  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「一旦コマンドラインを閉じて、models.pyでデフォルト値を設定する。」

選択肢1を選んだ場合、コマンドライン上でデフォルト値を指定します。指定の仕方は、紐づくインスタンスのIDを指定します。例えば、User「Jobs」のIDが1だった場合、数字の1をコマンドライン上で入力すれば、既存の全投稿データのuserフィールドには、Jobsが保存されることになります。

選択肢2を選んだ場合、コマンドラインの入力モードは終了します。その後、自分でmodels.pyを修正します。具体的には、user = models.ForeignKey(User, on_delete=models.CASCADE, default=1)のように、任意のユーザーIDをdefaultとして指定します。これにより、マイグレートするタイミングで、既存の投稿は全てJobsと紐づきます。

以下の画像は、選択肢1を選んだ場合の例です。

マイグレーションファイルができたら、マイグレートしてDBに反映させましょう。

管理画面から、Photoインスタンスを確認すると、全てのuserフィールドに自分が設定したユーザーが保存されているはずです。

ForeignKeyを使ったフィールドへのアクセス

photo.titlephoto.created_atのように書くことでそれぞれの属性にアクセスできますが、これはuserフィールドでも同じことです。photo.userとすると、Usersテーブルを参照するようになります。

試しに、shellでデータを取得してみましょう。

~/PhotoService

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> from app.models  import Photo

# 1つめのPhotoインスタンスを取得
>>> photo1 = Photo.objects.all()[0]
>>> photo1
<Photo: お散歩>
>>> photo1.title
'お散歩'

# photo1のuserフィールドにアクセス(Userインスタンスが取得できる)
>>> photo1.user
<User: Jobs>

# photo1に紐づいたUserのフィールドにアクセス
>>> photo1.user.id
1
>>> photo1.user.username
'Jobs'
>>> photo1.user.email
'[email protected]'
>>> photo1.user.is_superuser
True

上記のように、PhotoインスタンスのuserフィールドにアクセスすればUserインスタンスを取得でき、さらにそのUserインスタンスのフィールドにアクセスすることができます。これにより、「お散歩」という投稿をした人のユーザー名やメールアドレス等を取得できるようになりました。

Categoryモデルを作る

次に、categoryフィールドを作ってみましょう。実装の流れは、userフィールドと同じです。ただ、UserモデルはDjangoデフォルトのものを使用していましたが、Categoryモデルは自分で作る必要があります。Categoryモデルは、titleフィールドを持つシンプルなモデルです。「動物」や「風景」といったタイトルをつけてインスタンスを作ることができます。

~/PhotoService/app/models.py

from django.db import models
from django.contrib.auth.models import User

# Categoryモデルを作成
class Category(models.Model):
    title = models.CharField(max_length=20)

    def __str__(self):
        return self.title

class Photo(models.Model):
    title = models.CharField(max_length=150)
    comment = models.TextField(blank=True)
    image = models.ImageField(upload_to = 'photos')
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title 

~/PhotoService/app/admin.py

from django.contrib import admin
from .models import Category, Photo

class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'title')
    list_display_links = ('id', 'title')

class PhotoAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'user')
    list_display_links = ('id', 'title')

admin.site.register(Category, CategoryAdmin)
admin.site.register(Photo, PhotoAdmin)

Categoryモデルを作ったら、一回このタイミングでマイグレートしましょう。そして、Admin画面からカテゴリーをいくつか作成します。

次に、Photoモデルにcategoryフィールドを追加します。

~/PhotoService/app/models.py

class Photo(models.Model):
    title = models.CharField(max_length=150)
    comment = models.TextField(blank=True)
    image = models.ImageField(upload_to = 'photos')
    # 追加
    category = models.ForeignKey(Category, on_delete=models.PROTECT)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title 

再度マイグレーションファイルを作ります。先ほどと同じようにdefault値を聞かれるので、既存のCategoryインスタンスから任意のものを1つ選び、そのIDを入力して設定しましょう。マイグレーションファイルができたら、マイグレートコマンドを打ってください。

無事マイグレートできたら、Admin画面からいくつかPhotoインスタンスを作成してみましょう。そのとき、ユーザーとカテゴリーが選択できるようになっているはずです。

ForeignKeyはー対多(OneToMany)の関係を作る

photo1.userとすることで、Userインスタンスを取得できると説明しましたが、これを参照するといいます。photo1のuser属性を参照すると、Jobsが取得できます。

userフィールドのようにForeignKeyを使ったフィールドは、第一引数に指定されたモデルを参照することになります。その際、参照するインスタンスは1つだけです。つまり、Photoのuserフィールドには、1つだけのUserインスタンスが保存されるということです。photo1にはuser1(Jobs)のみが保存され、2つ以上のユーザーを保存することはできません。

一方、Userモデル側からみると、UserモデルはPhotosに参照されていると言うことができます。user1(Jobs)が、どのPhotoに参照されているかは、user1.photo_setとすることで取得できます。これにより、user1を参照している全てのphotoインスタンスを取得できます。このように、参照してきているインスタンスを取得することを逆参照といいます。

  • 参照:photo1.user → Photo側からUserを取得(参照するインスタンスは1つだけ)
  • 逆参照:user1.photo_set → User側からPhotoを取得(参照してくるインスタンスは複数ある)

Photoは1つのUserインスタンスを参照するのに対して、Userは多数のPhotoインスタンスに参照されています。この関係性を一対多(OneToMany)の関係といいます。ForeignKeyは一対多の関係性を作ります。

多数のインスタンス同士を参照させ合う多対多(ManyToMany)や、1つのみのインスタンス同士を参照させあう一対一(OneToOne)という関係性もありますので、別の機会に紹介します。

shell画面で逆参照の操作をしてみましょう。

~/PhotoService

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> from app.models import Photo, Category
>>> user1 = User.objects.all()[0]
>>> user1
<User: Jobs>
# 逆参照する
>>> user1.photo_set
<django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager object at 0x111016240>
# user1を参照している全てのPhotoインスタンスをクエリセットとして取得する
>>> user1.photo_set.all()
<QuerySet [<Photo: お散歩>, <Photo: 料理作った>]>

最後に、views.pyで、逆参照してユーザーに紐づいた写真を全て取得してみましょう。

~/PhotoService/app/views.py

def users_detail(request, pk):
    user = get_object_or_404(User, pk=pk)
    photos = user.photo_set.all().order_by('-created_at') ← 追加
    return render(request, 'app/users_detail.html', {'user': user, 'photos': photos}) ← 更新

viewから受け取ったphotosをusers_detail.htmlで表示します。index.htmlと全く同じ方法で表示できますので、for文の部分をコピペしましょう。

~/PhotoService/app/templates/app/users_detail.html

{% extends 'app/base.html' %}

{% block content %}

<h2 class="user-name">@{{ user.username }}</h2>

{% for photo in photos %}
    <img src="{{ photo.image.url }}" class="photo-img">
{% endfor %}

{% endblock %}

これでユーザーのページでは、その人が投稿した写真の一覧が表示されるようになりました。

< PREV NEXT >
SHARE ! Tweet