🐕

超個人的!!Djangoベストプラクティス

2022/09/12に公開

はじめに

プロジェクトに入るたび、「アイエエエ!? ジッソウ!? ジッソウナンデ!?」という Django 実装に出会う事が多いので、他山の石として。

極論な結論

ココを見、ココに準ずる。
https://django-best-practice-ja.readthedocs.io/ja/latest/

超個人的!!ベストプラクティス

以降は個人的に考えるベストプラクティス。
Django だけでなく、チーム開発や Django REST framework などの要素も含む。

Django 一般

Pipenv や poetry を入れ、本番環境と開発環境の module を切り分ける

不必要にモジュールを入れると、意図しない動作や想定外のバグの原因になる。開発環境と本番環境で module の管理をすること。

pipenv を使ったサンプル
インストールコマンド
モジュールのインストール
# pipenvを使う場合
# pipenvの使い方の詳細は略

# 本番環境用/開発環境共通のモジュールをインストール
pipenv install django

# 開発環境用にテスト用モジュールをインストール
pipenv install --dev nose
環境別のモジュールインストールコマンド

例えば、Dockerfile などで product / develop で異なるコマンドを実行する。

product環境へのモジュールインストール
# 本番環境用のモジュールのみインストール
# djangoがインストールされる
pipenv sync
develop環境へのモジュールインストール
# 開発環境用のモジュールも含めインストール
# django, noseがインストールされる
pipenv sync --dev

Lint、Formatter を適用する

Flake8 でも black でも autopep8 でもなんでもよいので、入れて適用すること。
コードの品質という観点でも、チーム開発という観点でも、一定のルールに基づいたフォーマットがなされるため、保守開発がしやすくなる。

アプリを適切に分割する

python manage.py startapp xxxを適切に実行すること。
適切にアプリを作成し、分割することにより責務を分けることで、修正の影響範囲が狭くなる。
少なくとも、1 つの app に全ブッコするのは辞めよう。

超個人的な考え

Django アプリケーションにただ 1 つ必要なものは models.py ファイルで提供されるものです。
とあるように、Django アプリの根本はmodels.pyである。
従って、models.py(=DB のテーブル)の関連元にアプリを分割する、という風に考えれば良い。

__init__.pyに必要以上にロジックを書かない

別アプリを参照する場合であっても、__init__.pyに記載しなくても import できる。
import の手間を減らそうとして、alias として__init__.pyに記載しても、逆に保守開発、メンテの手間を増やす羽目になる。

queryset は遅延評価であることを意識する

queryset は遅延評価。
必要な時のみに値を取得、評価するようにすること。

実際の動作などは以下が参考になる
https://qiita.com/ta_ta_ta_miya/items/7d02495eeee06958127a
https://djangobrothers.com/blogs/django_queryset_cache/

テンプレートエンジンを使うのであれば、Django Debug Toolbar
REST framework を使うのであればSilkを使って、発行される SQL を確認し、意図しない SQL 発行がないか確認すること。

範囲を考えて定義値を管理する

本当に Global にアクセスできていい値なのかを検討すること。
Enumで管理するような値をイメージしている。
その値は他のアプリでも使うものですか?settings.pyで設定するようなものなのか、アクセスできる範囲を考えること。
責務分離できていない、global 変数と変わらない使い方になっている実装は見直すこと。

以下、雑なサンプル。

# 定義部
class Role(models.TextChoices):
    GENERAL = 'general'  # 一般ユーザ
    STAFF = 'staff'      # スタッフ
    ADMIN = 'admin'      # アドミン

class User(models.Model):
    role = models.CharField(max_length=2,
                            choices=Role.choices,
                            default=Role.GENERAL)

    # 本来であればManager側に具備すべきmethod。
    # 簡易のためここで定義
    def create_as_general(self):
        self.role = Role.GENERAL
        ...

########################
# 別クラス、別アプリで実装する場合など

# 悪
# Userにroleがあり、かつその値の範囲がRoleである、ということを知っていなければならない
# Userの作成方法がCallする側でハンドリングされる=変更箇所や値が散財する
User.objects.create(role=Role.STAFF)

# 良
# Userを作る側は、作る時にはroleの値がなんであるかは知る必要がない
# もっというと、User作成時にrole値以外のロジックがあった場合、methodにしておくと抜け漏れがなくなる
User.create_as_general()

環境変数にしすぎない

主に外部 API の URI を想定。
環境変数にするのはドメインまでで OK。やりすぎた場合、その URI、どこ見ればわかりますか? って話。

# 悪
# ドメインを含め全てを環境変数にしている
APP_API = os.environ['OTHER_APP_API']

# APP_APIに対してgetをしているが、APP_APIが全て環境変数になっているため
# どのドメインの、どのAPIを実行しているかがわからない
requests.get(APP_API)

#################
# 良
# ドメインのみを環境変数にしている
APP_DOMAIN = os.environ['OTHER_APP_DOMAIN']

# ドメインはともかくとして、どのAPIをcallしているか一目でわかる
url = f'{APP_DOMAIN}/dummy-api'
requests.get(uri)

Django REST framework 一般

SwaggerUI を使い、API の I/F を分かりやすくする

Django REST framework を使う場合、drf-yasgを使い Swagger UI を生成すること。
ただし想定された実装をしている場合に限り Swagger UI が表示される点は注意する。
想定された実装 ≒ 一般的な実装(という認識)であるため、SwaggerUI が表示できなければ一般的な実装とズレているのでは?ということで実装を見直そう。
Swagger UI を表示することにより、Frontend とのやりとり(Request/Response)が明確になり、実装分担も容易になる。
※drf-yasg は OpenAPI ver3 をサポートしていない。OpenAPI ver3 は drf-spectacularの使用を検討を。

アプリ内のファイルは必要以上に分割しない

例えば、API ごとにXXXView.py, YYYView.pyなどファイル分割しない、ということ。
Views.pyの中にXXXViewYYYViewを定義することで、アプリごとにファイル構成が変わる、ということがなくなる。
その他、serializers や models も同様。
Java とは異なり、Python は 1 ファイルに複数クラスの定義をしても OK な言語です。

# 誤
├── XXXView.py
└── YYYView.py

# 正
└── views.py

View には基本的に Viewset を使う

RESTful な API を作成するのであれば、Viewsetで事足りるはず。
手を抜け手を。
Mixinもあるぞ。実装量を減らせ。手を抜け。

Serializer は HTTP Method 毎に作成する

Viewset(APIView)を継承した class-base の API 定義の場合、1class が複数の HTTP Method(GET/POST/PATCH/DELETE)を管理することになる。
そのような場合は、Method ごとに Serializer を分けて定義することで、Method 毎に Request/Response の内容を管理する(=責務分離する)ことができる

実装イメージ。

class Hoge(ViewSet):
    def get_serializer_class(self):
        # APIの種類毎に対応するSerializerクラスを返す
        if self.action == 'retrieve':
            return HogeRetrieveSerializer
        return HogeSerializer
GitHubで編集を提案

Discussion