Developersをフォローする

【Django】Django REST Frameworkを使用して理想の体重計算APIを作ってみた

バックエンド

社内でDjangoを使い始めたということもあり、Python🐍推しとしては乗り遅れるわけにはいかぬということでDjangoとDjango REST Framework(以下DRF)の学習を始めましたのでそのアウトプットになります。

作成したもの

以前体重100kg記念としてReactを使用して作成した理想の体重をグラフ化したブログの計算処理部分のAPIを作ってみました。(一部別機能を想定したAPIも作成しています。)
https://cloudsmith.co.jp/blog/frontend/2022/08/2158115.h
※ ただし、APIの組み込みはできていないのでご了承ください。

GitHub: https://github.com/ryu-0729/ideal-body-weight-api/tree/main

使用技術

Python3.9  ※ デプロイをDeta Spaceと考えていてまだ3.9までしか対応していないため仕方なく…(2023/08)
Django4.2.4
djangorestframework3.14.0
djangorestframework-simplejwt5.2.2

記事の内容に含まれないもの

  • 環境構築
  • Djangoの基礎知識

Djangoプロジェクトの構成

ideal-body-weight-api/
├── .flake8
├── .gitignore
├── .vscode/
│   └── settings.json
├── README.md
├── project/                  (← ベースディレクトリ)
│   ├── accounts/             (← アカウント用のアプリケーション)
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations/
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── apis/                 (← API用のアプリケーション)
│   │   ├── __init__.py
│   │   ├── apps.py
│   │   ├── migrations/
│   │   ├── tests.py
│   │   └── urls.py
│   ├── bodydata/             (← 身体データ用のアプリケーション)
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations/
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── config/               (← 設定ディレクトリ)
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── db.sqlite3
│   └── manage.py*
├── requirements.txt
└── venv/

主な構成についてですが、projectがベースディレクトリになっています。
configが設定ディレクトリです。
Django触りたての人なら「ん??」と感じる方もいるかと思いますが、ベースディレクトリと設定ディレクトリの名前が異なります。
あとで紹介はさせていただきますが、「現場で使えるDjangoの教科書」に記載されていた3.4 ベストプラクティス1: わかりやすいプロジェクト構成を参考にさせていただいております。
簡単に実行方法ですが、↓で作成可能です!

$ mkdir ベースディレクトリ名
$ cd ベースディレクトリ名
$ django-admin startproject config .

その他のaccountsとapis、bodydataはアプリケーションディレクトリになっています。

  • accounts(アカウント情報のアプリ)
  • apis(APIのURLを主に設定)
  • bodydata(身体データ情報のアプリ)

理想の体重計算API

理想の体重計算APIでは、フロント側で計算していた処理をAPI化したシンプルなものになります。
DRFでは、入力データの変換やバリデーション等を行うシリアライザとリクエストを受け取り必要な処理を実行してレスポンスを返すビューの実装が必要になります。

今回実装したシリアライザとビューになります。

from decimal import ROUND_HALF_UP, Decimal

from rest_framework import serializers

from .models import BodyData


class BodyDataSerializer(serializers.ModelSerializer):
    """身体データ用のシリアライザ"""

    class Meta:
        model = BodyData
        fields = ["height", "weight", "created_at"]


class WeightCalculationSerializer(serializers.Serializer):
    """理想の体重計算用のシリアライザ"""

    height = serializers.FloatField()
    weight = serializers.FloatField()
    bmi = serializers.SerializerMethodField()
    appropriate_body_weight = serializers.SerializerMethodField()
    beauty_weight = serializers.SerializerMethodField()
    cinderella_weight = serializers.SerializerMethodField()

    def height_squared(self, height: float) -> float:
        """身長をcm→mに変換し2乗する"""
        return (height / 100) ** 2

    def round_up_and_down(self, target):
        """四捨五入メソッド"""
        exp = Decimal("0.1")
        return str(Decimal(target).quantize(exp, ROUND_HALF_UP))

    def get_bmi(self, obj):
        return self.round_up_and_down(
            obj["weight"] / self.height_squared(obj["height"])
        )

    def get_appropriate_body_weight(self, obj):
        return self.round_up_and_down(self.height_squared(obj["height"]) * 22)

    def get_beauty_weight(self, obj):
        return self.round_up_and_down(self.height_squared(obj["height"]) * 20)

    def get_cinderella_weight(self, obj):
        return self.round_up_and_down(self.height_squared(obj["height"]) * 18)

使用するシリアライザではModelは使用しないので、serializers.Serializerを継承したクラスを実装しています。(※ 入力内容がModelのフィールド定義を使用したい場合には、serializers.ModelSerializerを使用したほうが楽に実装できます。)

16行目からのWeightCalculationSerializerが今回使用するシリアライザになります。
フィールドの定義方法は一部省略しますが、SerializerMethodFieldを使用し、get_フィールド名を定義することで戻り値となる値を追加することもできます。
追加している値としては、理想の体重の指標として欠かせないBMIと適正体重、美容体重、シンデレラ体重になります。

from rest_framework import permissions, status, views
from rest_framework.response import Response

from .serializers import BodyDataSerializer, WeightCalculationSerializer


class BodyDataCreateAPIView(views.APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request, *args, **kwargs):
        serializer = BodyDataSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        request.user.bodydata_set.create(**serializer.validated_data)
        return Response(serializer.data, status.HTTP_201_CREATED)


class WeightCalculationAPIView(views.APIView):
    def get(self, request, *args, **kwargs):
        serializer = WeightCalculationSerializer(data=request.query_params)
        serializer.is_valid(raise_exception=True)
        return Response(serializer.data, status.HTTP_200_OK)

17行目に定義されているWeightCalculationAPIViewクラスを使用しています。
シリアライザにクエリパラメータのチェックをしてもらいレスポンスを返すだけなので、特に難しいことはしていません。

理想の体重計算APIの実行結果です↓

現在、体重は98.0 ~ 97.0を行ったり来たりしております。(2023/8時点)
実行結果からわかるように適正体重(69.7kg)には程遠いですね。。

他の機能の実装で使用した技術について

理想の体重計算アプリに、ユーザーの身長と体重をグラフに可視化する機能を追加したいなーと思っています。
追加機能実現のためのAPI実装で使用した技術的なところについて少し紹介できたらと思います。

Userモデルの拡張

Userモデルの拡張方法も「現場で使えるDjangoの教科書」を参考にしています。
方法として、

1. 抽象クラス「AbstractBaseUser」を継承する

2. 抽象クラス「AbstractUser」を継承する

3. 別モデルを作成して「OneToOneField」で関連させる

の3つの方法が紹介されています。
どのパターンを利用するかの詳細は書籍を見ていただきたいのですが、今回はUserモデルに性別を追加したいだけなので、AbstractUserを継承してUserモデルを拡張しています。

from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models


class CustomUser(AbstractUser):
    class Meta:
        db_table = "custom_user"

    objects = UserManager()

    GENDER_TYPE = (
        (1, "男性"),
        (2, "女性"),
    )

    gender = models.IntegerField(verbose_name="性別", choices=GENDER_TYPE, default=1)

作成後は、設定ファイルに拡張したUserモデルの定義してあげる必要があるので注意が必要です。
※ 「アプリ名.拡張Userモデル」で定義します。

Django
AUTH_USER_MODEL = "accounts.CustomUser"

拡張UserモデルをDBに反映するためマイグレーションの実行も必要になるのですが、エラーになる可能性があります。
エラーの対応としては、下記のリンクが参考になったのでエラーの際は参考にしてみてください。
https://qlitre-weblog.com/cant-migrate-django-custom-user/

また、拡張Userモデルのobjectsにはデフォルトで用意されているUserManagerを使用するのが良いかなと感じました。(ユーザーの作成メソッドcreate_user()を再利用できるのが良い!)

objectsやmanagerについて詳しくは公式ドキュメントを参照ください。
objects: https://docs.djangoproject.com/ja/4.2/topics/db/models/#model-attributes
manager: https://docs.djangoproject.com/ja/4.2/topics/db/managers/#django.db.models.Manager

認証、認可

APIの認証方法はいくつかありますが、今回はSimple JWTを使用して実装しています。
ここでは簡単にセットアップ方法とパーミッションの設定、ログインユーザーの取得を紹介できればと思います。

セットアップ方法

まずパッケージをインストールします。

pip install djangorestframework-simplejwt

次に設定ファイルに設定を追加します。

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication"
    ]
}

SIMPLE_JWT = {
    "AUTH_HEADER_TYPES": ("Bearer",),
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
}

最後に認証周りのURLを設定すれば、トークンの取得とトークン再取得のAPIが実装できます。

urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

パーミッション設定と認証済みユーザーの取得

パーミッションの設定についてです。
パーミッションの設定方法もいくつかあり、詳しくは公式ドキュメントを見ていただければと思いますが、今回は認証不要のAPIもあるのでViewクラスごとに設定する方法を採用しています。

それぞれのViewクラスに対してpermission_classesを渡してあげることでパーミッションの設定をすることができます。
認証済みのユーザーしかアクセスできないIsAuthenticatedを設定しています。(IsAuthenticatedOrReadOnlyという設定もできるのですが、具体的な使用イメージがもてなかったので説明は省略します。)

認証済みユーザーの取得はシンプルでrequest.userで取得できます。
ごく稀に取得できない場合があるみたいですが、その際は下記のissueを参考にしていただければ問題ないかと思います。
https://github.com/jazzband/djangorestframework-simplejwt/issues/140
ちなみに取得できるデータ型は認証で使用しているモデルになるので、リレーション先のデータの取得や更新もできます。(ここではCustomUser)

↓具体例(理想の体重計算APIで紹介したソースの一部を切り取っています。)

class BodyDataCreateAPIView(views.APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request, *args, **kwargs):
        serializer = BodyDataSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        request.user.bodydata_set.create(**serializer.validated_data)
        return Response(serializer.data, status.HTTP_201_CREATED)

まとめ

  • 体重はこまめに測ろう(気づいた時には。。。)
  • 入社前の78kgに絶対戻す!
  • 個人的にFastAPIを激押しは変わりませんが、Djangoも使いこなせるように頑張ります!

参考文献

「現場で使えるDjangoの教科書」著者 横瀬 明仁(akiyoko)
「現場で使えるDjango REST Frameworkの教科書」著者 横瀬 明仁(akiyoko)

↑2冊とも控えめに言って神書籍でした!