はじめに
この記事は作者があるSlackで設計問題のヒステリーを起こしながらダラダラ愚痴った暴言チャットをまとめたものです。
TL;DR
Django ModelのAbstrat base classってぶっちゃけいらなくね?
Django: モデル継承問題
例えば、次のようなモデル継承。
class Player(models.Model):
name = models.CharField(max_length=128)
level = models.PositiveIntegerField(default=1)
class Meta:
# ...
# ...
class Equipment(models.Model):
name = models.CharField(max_length=128)
owner = models.ForeignKey(Player,
null=True,
on_delete=models.SET_NULL)
class Meta:
# ...
# ...
class Weapon(Equipment):
damage = models.PositiveIntegerField()
class Armor(Equipment):
defense = models.PositiveIntegerField()
わかりやすくRPG風に例えました。Player
(プレイヤ)が所有するEquipment
(装備)があり、Weapon
(武器)かArmor
(防具)か2種類が存在する。
Abstract base class
本来なら、Equipment
をAbstract base class(以下、ABC)にしてインスタンス化を禁止すべきです。
# ...
class Equipment(models.Model):
# ...
class Meta:
abstract = True
# ...
だがこのやり方じゃ、次のようにPlayer
の装備を一つのQuerySetにまとめることが出来ません。
>>> tirion = Player.objects.create(name='Tirion Fordring', level=120)
>>> tirion
<Player: Tirion Fordring>
>>> ashbringer = Weapon.objects.create(name="Ashbringer", owner=tirion, damage=160)
>>> lightbringer = Armor.objects.create(name="Lightbringer", owner=tirion, defense=69)
>>>
>>> tirion.equipment_set
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Player' object has no attribute 'equipment_set'
>>>
>>> tirion.weapon_set.all()
<QuerySet [<Weapon: Ashbringer>]>
>>> tirion.armor_set.all()
<QuerySet [<Armor: Lightbringer>]>
ABCを使うと継承したConcrete Model以外はテーブルとManagerを作らないので当たり前ではあります。
Multi-table Inheritance
一方、上記models.py
でMetaオプションの特別な指定がなければ、Multi-table Inheritance(以下、MTI)になります(裏ではもちろんOneToOne Relation)。
>>> tirion = Player.objects.create(name='Tirion Fordring', level=120)
>>> tirion
<Player: Tirion Fordring>
>>>
>>> ashbringer = Weapon.objects.create(name="Ashbringer", owner=tirion, damage=160)
>>> frostmourne = Weapon.objects.create(name="Frostmourne", owner=tirion, damage=896)
>>> lightbringer = Armor.objects.create(name="Lightbringer", owner=tirion, defense=69)
>>> robe = Armor.objects.create(name="Devout Robe", owner=tirion, defense=89)
>>>
>>> tirion.equipment_set.all()
<QuerySet [<Equipment: Ashbringer>, <Equipment: Frostmourne>, <Equipment: Lightbringer>, <Equipment: Devout Robe>]>
>>> tirion.equipment_set.filter(weapon__isnull=False)
<QuerySet [<Equipment: Ashbringer>, <Equipment: Frostmourne>]>
>>> tirion.equipment_set.filter(armor__isnull=False)
<QuerySet [<Equipment: Lightbringer>, <Equipment: Devout Robe>]>
>>>
>>> tirion.weapon_set
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Player' object has no attribute 'weapon_set'
綺麗に装備をまとめることが出来ましたが、abstractにすべき装備モデルが単独でインスタンス化してしまいます。
あ、SQL文も増えます…
django-model-utils
ここで、django-model-utils: InheritanceManager
の登場です。
https://github.com/jazzband/django-model-utils
https://django-model-utils.readthedocs.io/en/latest/managers.html#inheritancemanager
from model_utils.managers import InheritanceManager
# ...
class Equipment(models.Model):
# ...
objects = InheritanceManager()
# ...
すると、以下のことが出来ます。
>>> tirion.equipment_set.select_subclasses()
<InheritanceQuerySet [<Weapon: Ashbringer>, <Weapon: Frostmourne>, <Armor: Lightbringer>, <Armor: Devout Robe>]>
>>> tirion.equipment_set.get_subclass(pk=1)
<Weapon: Ashbringer>
mixed iterableだけど同じEquipment
インスタンスだしいいんじゃね?
Equipment
のインスタンス化問題はvalidationやassertionなどで補うとなんとか…なところでしょうか。
あと気になるのは武器と防具の更なるsubclassを作ったらどうなるんでしょう…
ABCの存在意義とは
ここから愚痴です。
- Django ModelのABCってただ、フィールド・メソッドのコード共通化以外意味なくね?動的型付けだからPolymorphismの意味もないし…
- Interface作るなら
metaclass=abc.ABCMeta
して@abstractmethod
使って多重継承(Multiple-inheritance)すればいいよね?さっぱりして衝突もなさそうだしProxyってなんか変だし…
まとめ
- Multi-table Inheritanceで行こう
-
import abc
してInterfaceはいかが? - Django2.0で
InheritanceManager
がまだ使えん… - 経験豊富な方のマサカリ、お待ちしております。
etc.
Human.objects.all()
にして人種・国・性嗜好関係なくmixedされたら世界平和なのに、
Human._meta.abstract = True
なのが諸悪の根源。