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

## 概要

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


### 実装機能

- [Django REST framework(DRF)](https://www.django-rest-framework.org/)の導入
- 外部キーを用いたモデルの設計、実装


| 項番 | 記事 |
| :--- | :--- |
| 1 | [React + Django + CORSを使ったフロントエンド / バックエンドのデータ連携](https://qiita.com/xinome/items/8319a7f85424bd6e62de) |
| 2 | [Django 管理画面のカスタマイズ](https://qiita.com/xinome/items/f883ce4565cd9c56ad17) |
| 3 | [Django REST framework(DRF)を使ったAPIサーバーとReactとのデータ連携](https://qiita.com/xinome/items/a524522b6697a3d9ffa9) |
| 4 | Django REST frameworkのserializersを使った外部キーモデルの参照（本記事） |
| 5 | [React + Redux / Redux Toolkitを使った非同期通信の検証](https://qiita.com/xinome/items/7971ac9091d126f3861a) |
| 6 | [APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(設計編)](https://qiita.com/xinome/items/1eaac0c02bf3bf20f75b) |
| 7 | [APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(実装編)](https://qiita.com/xinome/items/9541aa8afe7d77ea0b1e) |

### 参考文献

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でモデルの設計をします。

```python: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](https://docs.djangoproject.com/ja/4.2/topics/db/examples/many_to_one/)を使った多対一 (many-to-one)を使用します。

ForeignKeyは[on_deleteオプション](https://docs.djangoproject.com/ja/4.2/ref/models/fields/#django.db.models.ForeignKey.on_delete)が必要な他、null値の許容やデフォルト値の指定等が可能です。

- TopicsCategoryのテーブル構成

```
id	category_name
1	プロジェクト
2	ポートフォリオ
3	スカウト
```

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

```python: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` 構文の仕様なのか多階層のデータをそのまま取得することが難しいようです。

```bash
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を改修します。

```python: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」が入れ子で取得できるようになります。


### フロントエンド

```javascript:/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` として出力しています。
構成が変わらずそのまま連携できるようになりました。