超個人的!!Djangoベストプラクティス
はじめに
プロジェクトに入るたび、「アイエエエ!? ジッソウ!? ジッソウナンデ!?」という Django 実装に出会う事が多いので、他山の石として。
極論な結論
ココを見、ココに準ずる。
超個人的!!ベストプラクティス
以降は個人的に考えるベストプラクティス。
Django だけでなく、チーム開発や Django REST framework などの要素も含む。
Django 一般
Pipenv や poetry を入れ、本番環境と開発環境の module を切り分ける
不必要にモジュールを入れると、意図しない動作や想定外のバグの原因になる。開発環境と本番環境で module の管理をすること。
pipenv を使ったサンプル
インストールコマンド
# pipenvを使う場合
# pipenvの使い方の詳細は略
# 本番環境用/開発環境共通のモジュールをインストール
pipenv install django
# 開発環境用にテスト用モジュールをインストール
pipenv install --dev nose
環境別のモジュールインストールコマンド
例えば、Dockerfile などで product / develop で異なるコマンドを実行する。
# 本番環境用のモジュールのみインストール
# djangoがインストールされる
pipenv sync
# 開発環境用のモジュールも含めインストール
# 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 は遅延評価。
必要な時のみに値を取得、評価するようにすること。
実際の動作などは以下が参考になる
テンプレートエンジンを使うのであれば、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
の中にXXXView
とYYYView
を定義することで、アプリごとにファイル構成が変わる、ということがなくなる。
その他、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
Discussion