Djangoで子テーブルのデータをテンプレートで表示させる方法

### はじめに

日記アプリなどのアプリケーションで、ユーザーのプロフィールページにそのユーザが投稿した日記を表示させたい場合など、小テーブルのデータをテンプレートに表示させる方法を紹介します。

###  サンプルケース

今回はマイページで自分が投稿した記事をテンプレートに表示させるケースを想定します。

また、CustomUserとArticleの2つのモデルを用意し、CustomUserを親テーブル、Articleモデルを子テーブルとして、一対多の関係でリレーションシップを取ります。

![スクリーンショット 2023-04-28 4.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743446/9d3b5c45-a415-886d-9818-8a9fa5b7ddab.png) 

```accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

class CustomUser(AbstractUser):
    """拡張ユーザーモデル"""
    username = models.CharField(
        _("username"),
        max_length=30,
        help_text='Required 30 characters or fewer.',
        unique=True,
        error_messages={
            'unique': _("This Username already exists."),
        },)
    email = models.EmailField(
        _('email'),
        unique=True,
        error_messages={
            'unique': _("A user with that email address already exists."),
        },)
    fb_link = models.URLField(verbose_name='Facebook Link', null=True, blank=True)
    ig_link = models.URLField(verbose_name='Instagram Link', null=True, blank=True)
    tw_link = models.URLField(verbose_name='Twitter Link', null=True, blank=True)
    bg_image = models.ImageField(verbose_name='Backgroung Image', null=True, blank=True, upload_to='bgimage/')
    icon = models.ImageField(verbose_name='Icon Image', null=True, blank=True, upload_to='icon/')
    profession = models.CharField(verbose_name='Profession', null=True, blank=True, max_length=20)
    introduction = models.TextField(verbose_name='Introduction', null=True, blank=True, max_length=500)
    
    class Meta:
        verbose_name_plural = 'CustomUser'
```

```article/model.py
from django.db import models
from accounts.models import CustomUser
from mdeditor.fields import MDTextField

class Article(models.Model):
    """記事タグ"""
    tag_choices = (
        ('ELECTRONICS','ELECTRONICS'),
        ('INSTALLATION','INSTALLATION'),
        ('SERVICES','SERVICES'),
        ('CRAFT','CRAFT')
    )
    post_user = models.ForeignKey(CustomUser, verbose_name='Post User', on_delete=models.CASCADE, related_name='name',)
    title = models.TextField(verbose_name='title', max_length=50,)
    content = MDTextField()
    photo = models.ImageField(verbose_name='photo', null=True, blank=True,)
    thumbnail = models.ImageField(verbose_name='thumbnail', null=True, blank=True,)
    tag = models.CharField(verbose_name='article tag', choices=tag_choices, max_length=30,)
    created_at = models.DateField(verbose_name='created_at', auto_now_add=True,)
    view_count = models.PositiveIntegerField(verbose_name='view count', default=0,)
```
親テーブルには子テーブルのフィールドが定義されていません。一方で参照はされており、それを利用するために、**related_name**をmodels.pyのリレートフィールドに定義する必要があり、このrelated_nameで設定した名前を使用して取得することが出来ます。今回は`related_name='name'`と指定しているため、例えばテンプレート側では`{{ 変数.name.all }}`で紐付く子テーブルの全レコードを取得できるようになります。

### ビューの作成

マイページの表示はListlViewを継承したクラスビューで作成します。クラス変数のmodelにはCustomUserを指定しており、常に表示されるレコードが1つ(マイページへアクセスするログインユーザー)ですが、紐付くArticleモデルのレコードが0以上の複数であるため、**単一のオブジェクトを返すことが期待されているDetailViewを使わずにListViewを使用しました。**
```article/views.py
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
from accounts.models import CustomUser
from .models import Article

class MyPageView(LoginRequiredMixin, generic.ListView):
    template_name = 'mypage.html'
    model = CustomUser
    
    def get_queryset(self):
        articles = CustomUser.objects.filter(username=self.request.user).prefetch_related('name')
        return articles
```

ここで注意したいのが、get_querysetメソッドのオーバーライドです。prefetch_related()メソッドは、リレーション関係があるレコード抽出のクエリの最適化を行うメソッドです。models.pyのrelated_nameで指定した値を入れます。


### テンプレートの作成
次にテンプレートの作成をします。

```mypage.html

                                    --中略--

{% block contents %}
{% for s in customuser_list %}
<section>
  <div class="container">
    <!-- Profile Block -->
    <div class="row">
      <div class="col-md-4 mx-auto">
        <div class="u-pull-half text-center">
        {% if s.icon %}
          <img class="img-fluid u-avatar u-box-shadow-lg rounded-circle mb-3" width="200" height="auto" src="{{ s.icon.url }}" alt="Image Description">
        {% else %}
          <img class="img-fluid u-avatar u-box-shadow-lg rounded-circle mb-3" width="200" height="auto" src="{% static 'img/default_icon.png' %}" alt="Image Description">
        {% endif %}
        </div>
      </div>
    </div>
    <!-- End Profile Block -->

    <!-- About -->
    <div class="row u-content-space-bottom">
      <div class="col-lg-6 mb-5 mb-lg-5 pl-lg-5 mx-auto">
        {% if s.introduction %}
        <h4 class="mb-3">About me</h4>
        <p>{{ s.introduction }}</p>
        {% endif%}
      </div>
    </div>
    <!-- End About  -->
  </div>
</section>
<!-- End About Section -->

<!-- Portfolio -->
<section class="u-content-space">
  <div class="container">
    <header class="text-center w-md-50 mx-auto mb-8">
      <h2 class="h1">Prototyping Works</h2>
    </header>
    <!-- Work Content -->
    <div class="js-shuffle u-portfolio row no-gutters mb-6">
      {% for article in s.name.all %}
      <figure class="col-sm-6 col-md-4 u-portfolio__item" data-groups='["its-illustration"]'>
        <img class="u-portfolio__image" src="{{ article.thumbnail.url }}" alt="Image Description">
        <figcaption class="u-portfolio__info">
          <h6 class="mb-0">{{ article.title }}</h6>
          <small class="d-block">Branding</small>
        </figcaption>
        <a class="js-popup-image u-portfolio__zoom" href="assets/img-temp/portfolio/img1.jpg">Zoom</a>
      </figure>
      {% endfor %}
      <!-- sizer -->
      <figure class="col-sm-6 col-md-4 u-portfolio__item shuffle_sizer"></figure>
    </div>
    <!-- End Work Content -->   
  </div>
</section>
<!-- End Portfolio -->
{% endfor %}
{% endblock %}
```
ListViewではビューからテンプレートには**model名(小文字)]_list**という名前でオブジェクトが渡されます。今回は`{% for s in customuser_list %}`でレコードをそれぞれ取り出し、`{{ s.フィールド名 }}`で各フィールドのデータを取り出しています。また、子テーブルのデータの取り出しには、`{% for article in s.name.all %}`でレコードをそれぞれ取り出し、`{{ article.フィールド名 }}`で子テーブルの各フィールドのデータを取り出しています。

### 参考
・Djangoでデータを取得してみる(外部キー)
https://yk5656.hatenablog.com/entry/20210410/1617980400
・Django 1対多(one2many)でリレーションされた子テーブルの集計値を取得する
https://igreks.jp/dev/django-1%E5%AF%BE%E5%A4%9Aone2many%E3%81%A7%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%95%E3%82%8C%E3%81%9F%E5%AD%90%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E9%9B%86%E8%A8%88/
・【Django2.2】Djangoでリレーション先はどう表示するの？ 【ListView編】
https://qiita.com/ragnar1904/items/1afaeb6601cc490fb70a