👌

Django REST frameworkのserializersを使った外部キーモデルの参照

に公開

概要

Django REST frameworkの serializers ライブラリを使い、dict型で取得されたdjango-adminのデータをJSONに変換した状態でフロントエンドに連携する仕組みを実装したのですが、外部キーを使ったモデル設計の場合はもう一工夫必要であることがわかったため記載します。

実装機能

項番 記事
1 React + Django + CORSを使ったフロントエンド / バックエンドのデータ連携
2 Django 管理画面のカスタマイズ
3 Django REST framework(DRF)を使ったAPIサーバーとReactとのデータ連携
4 Django REST frameworkのserializersを使った外部キーモデルの参照(本記事)
5 React + Redux / Redux Toolkitを使った非同期通信の検証(後日公開)
6 APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(設計編)(後日公開)
7 APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(実装編)(後日公開)

参考文献

https://qiita.com/kamontia/items/8d3fdecadeb436431967

https://docs.djangoproject.com/ja/4.2/topics/db/examples/

フォルダ構成

今回使用しているものをメインに抜粋

  • バックエンド(Python / Django)
[Djangoプロジェクト]
├── [Djangoプロジェクト_meta]
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── [Djangoアプリ]
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   ├── serializers.py
│   └── views.py
├── db.sqlite3
└── manage.py

実装方法

バックエンド

まずはmodels.pyでモデルの設計をします。

app/models.py
# 全てのモデルに共通する項目を定義する
class BaseMeta(models.Model):
  created_at = models.DateTimeField(auto_now_add=True)
  updated_at = models.DateTimeField(auto_now=True)

  class Meta:
    abstract = True  # 抽象クラスとして定義する


class TopicsCategory(BaseMeta):
  id = models.AutoField(primary_key=True)
  category_name = models.CharField(max_length=100)

  class Meta:
    db_table = 'topics_category'
    verbose_name_plural = 'トピックスカテゴリ'

  def __str__(self):
    return self.category_name


class ProjectTopics(BaseMeta):
  id = models.AutoField(primary_key=True)
  date = models.DateField()
  content = models.CharField(max_length=1000)
  category = models.ForeignKey(TopicsCategory, on_delete=models.PROTECT, null=True, default=1)

  class Meta:
    db_table = 'project_topics'
    verbose_name_plural = 'プロジェクトのトピック一覧'

  def __str__(self):
    return self.content

ProjectTopics が抽象クラス BaseMeta 及び外部キーの参照元となる TopicsCategory を読み込む構成となっています。
外部キーの構成には

  • 多対多 (many-to-many) 関係
  • 多対一 (many-to-one) 関係
  • 一対一 (one-to-one) 関係

の種類があるのですが、今回は ForeignKeyを使った多対一 (many-to-one)を使用します。

ForeignKeyはon_deleteオプションが必要な他、null値の許容やデフォルト値の指定等が可能です。

  • TopicsCategoryのテーブル構成
id	category_name
1	プロジェクト
2	ポートフォリオ
3	スカウト

次のステップに入る前に、views.pyで取得データの構成を確認してみます。

app/views.py
from django.shortcuts import render
from django.http import JsonResponse
from rest_framework import viewsets

# Model, Serializerをインポートする
from .serializers import ProjectTopicsSerializer
from .models import ProjectTopics
  :
def project_topics(request):
  # data = [
  #   {
  #     "id": 1,
  #     "date": "2022-10-01",
  #     "content": "【テスト】[プロジェクト]「プロジェクト名1」デプロイされました。",
  #     "category": {
  #         "id": 1,
  #         "category_name": プロジェクト,
  #     },
  #   },
  # ]

  queryset = ProjectTopics.objects.all()

  for ProjectTopic in queryset:

    print("ProjectTopic: ", ProjectTopic)
    print("ProjectTopic.category: ", ProjectTopic.category)
    print("ProjectTopic.category.id: ", ProjectTopic.category.id)
    print("ProjectTopic.category.category_name: ", ProjectTopic.category.category_name)

  serializer_class = ProjectTopicsSerializer(queryset, many=True)
  data = serializer_class.data

  for item in data:
    print("item: ", item)

  return JsonResponse(data, safe=False)

本来はコメント内に記載した形式で取得したいのですが、 print 構文の仕様なのか多階層のデータをそのまま取得することが難しいようです。

ProjectTopic:  「プロジェクト名1」デプロイされました。
ProjectTopic.category:  プロジェクト
ProjectTopic.category.id:  1
ProjectTopic.category.category_name:  プロジェクト

item:  OrderedDict([('id', 1), ('date', '2022-10-01'), ('content', '「プロジェクト名1」デプロイされました。'), ('category', OrderedDict([('id', 1), ('category_name', 'プロジェクト'), ('created_at', '2024-01-15T14:50:49.590256+09:00'), ('updated_at', '2024-01-15T14:50:49.590341+09:00')])), ('created_at', '2024-01-10T18:06:19.942784+09:00'), ('updated_at', '2024-01-15T16:43:19.660215+09:00')])

このようにserializerを通した後はDict型で取得できていますが、querysetの状態だと特定の1項目のみ出力されるようになります。
ここでデータ形式が崩れると後々フロントエンドへの連携やCRUD管理に影響を及ぼすため、それを回避するためにserializers.pyを改修します。

app/serializers.py
from rest_framework import serializers
from .models import ProjectTopics, TopicsCategory

class TopicsCategorySerializer(serializers.ModelSerializer):
  class Meta:
    model = TopicsCategory
    fields = ('id', 'category_name', 'created_at', 'updated_at')

class ProjectTopicsSerializer(serializers.ModelSerializer):
  # 外部キーのカテゴリーを取得する
  category = TopicsCategorySerializer()
  
  class Meta:
    model = ProjectTopics
    fields = ('id', 'date', 'content', 'category', 'created_at', 'updated_at')

外部キー用のserializer TopicsCategorySerializer を新しく定義し、読み込む側の ProjectTopicsSerializer で項目名を指定した後、fieldsでその名前を追加します。
(このタイミングで任意の項目名にすることも可能のよう)

この指定により外部キーの category モデルから「id」「category_name」が入れ子で取得できるようになります。

フロントエンド

/src/BaseApp.js
<div className='section-wrapper-contents'>
    :
  {filteredProjectList.map((item) => (
    <dl key={item.id}>
      <dt>{item.date}</dt>
      <dd>
        <span className="tag_category">
          {item.category.id}
          {item.category.category_name}
        </span>
        {item.content}
      </dd>
    </dl>
  ))}
</div>

一例として、バックエンドから取得したJSON形式のデータを filteredProjectList として出力しています。
構成が変わらずそのまま連携できるようになりました。

Discussion