- 課題: Djangoのモデルのフィールドにおいて、プリミティブ(int/str)ではなくなんらかの独自のObjectで扱いたい
- 結論:
- 定数値:
django.db.models.enums.TextChoices
とdjango-enum
を使用する - ValueObject: dataclassで定義し、Djangoのカスタムモデルフィールドを定義する
- 定数値:
- 複雑なアプリケーション開発ではなんらかまとまりで意味を持つ値が必要になることが多々ある
- 例: 「桁数が20桁で有意コードを含む注文番号」「外部連携で必要になる特別な年月」など
- そのときに、strやintのようなプリミティブしか使用しないと、コードの表現力が足りない
- リファクタリング案件では、素朴な関数化とプリミティブな値と大量のNull許可が多く防御的コードが増え、だいたいにおいて複雑性の制御に失敗している
- そのため、いわゆるValueObject的な概念の処理単位を定義したくなる
- 一応戦術的ドメイン駆動開発みたいな文脈っぽい内容で書いているつもりだが、あまり厳密には考えていない。とはいえ、多かれ少なかれどんなコードでも必要な内容にはなる
- 問題:
Prefecture.hokkaido
というような定数のDBではどの値で格納するか1
という数値で格納するかhokkaido
という文字列で格納するか
- 結論: 文字列の方がよい
- 理由:
- DBを直接参照する時に数値で格納してあると、仕様書か実装されたコードと対比させる必要があり、手間が増える
- DBに数値で格納してもほぼ全ての場合においてデータ量の削減にはならない
- データ量の削減等を考えるなら先に他にやることがある
- 数値で表現すると数値に意味を与えるコードが生まれることがある
- 例:
0
や-1
ならばエラーとか。2桁など桁数で管理しようとして後からその意図を無視したコードが書かれるとか - そういう議論やコードが生まれる前に、文字列の値をすべて個別に指定した方が確実
- 例:
$ pip install django-enum
import django_enum
from django.db.models import enums
class NameType(enums.TextChoices):
"""名義"""
personal = "personal", "個人"
business_normal = "business_normal", "法人"
business_special = "business_special", "法人(特別対応)"
@property
def special_code(self) -> str:
cls = NameType
d = {
cls.personal: "1A2B",
cls.business_normal: "3C4D",
cls.business_special: "XXXX",
}
return d[self]
@property
def is_business(self) -> bool:
cls = NameType
return self in [cls.business_normal, cls.business_special]
class Account(BaseModel):
name_type = django_enum.EnumCharField("名義", max_length=32, enum=NameType)
account = Account.objects.craete(name_type="personal")
account.name_type # `"personal"`という文字列ではなく、 `NameType.personal` が取得できる
account.name_type.special_code # -> `"1A2B"`
YearMonth
型をつくる
- 年月であり日を含まない。そのため、日付型にはしたくない
- ただし、内部的な値の比較では日付型を流用してもよい
- Pythonでは年や月を個別で扱いたい
Model.some_year_month
で年月(YearMonth
)を取得Model.some_year_month.year
で年(int)を取得Model.some_year_month.month
で月(int)を取得- 必ず文字列6桁であり、年月の数値が日付的に正しいことを保証したい
- DBには
YYYYMM
のような6文字の文字列で格納する - SQLでは
x_column = 202406
のように6桁の文字列で検索したい
- immutableなので
frozen=True
- 1つの引数しかとらない前提なので
kw_only=False
- 文字列が来ることを保証する
- 6桁であることを保証する
- 日時でパースできることを保証する
- 年と月をそれぞれ int で取得できる
from dataclasses import dataclass
@dataclass(frozen=True, kw_only=False)
class YearMonth:
"""年月"""
value: str
def __post_init__(self):
self._as_date(self.value) # 今回はパースできるかどうかで保証
if len(self.value) != 6:
raise ValueError("6桁の文字列を指定してください")
@property
def as_date(self) -> tuple[int, int]:
d = self._as_date(self.value)
return d.year, d.month
@classmethod
def _as_date(cls, v: str):
d = datetime.datetime.strptime(v, "%Y%m")
return datetime.date(year=d.year, month=d.month, day=1)
- max_lengthとvalidatorsはkwargsでの指定を禁止してもよい
from django.db import models
from django.core.validators import MinLengthValidator
class YearMonthField(models.CharField):
def __init__(self, *args, **kwargs):
kwargs["validators"] = [MinLengthValidator(6)]
kwargs["max_length"] = 6
super().__init__(*args, **kwargs)
def to_python(self, value: str | YearMonth | None) -> YearMonth | None:
if value is None:
return None
if isinstance(value, YearMonth):
return value
return YearMonth(value)
def from_db_value(self, value: str, expression, connection) -> YearMonth | None:
# DB -> Python
if value is None:
return None
return YearMonth(value)
def get_prep_value(self, value: str | YearMonth | None) -> str | None:
# Python -> DB
if value is None:
return None
if isinstance(value, YearMonth):
return value.value
return value