MTIで定義したDjangoモデルをより柔軟に扱う方法はないかなぁと調べていたところdjango-polymorphic
というよさげなライブラリを発見しました。
今回はそんなよさげなライブラリdjango-polymorphic
を使ってみたの会になります。
また、今回は具体例として以下のようなPlan
をスーパータイプとして、サブタイプにHobbyPlan
とProPlan
が存在するサブタイプを例にまとめていきます。
概念データモデルのサブタイプを、テーブルに定義する際にいくつかの方法があるかと思います。
(サブタイプにも排他的サブタイプであったりといくつか種類があるのですが、本質ではないので割愛します。)
各テーブル継承のメリデメ等はこちらがわかりやすかったです。
上記をDjangoのModelで定義する。となった場合に一番に思いつくのは以下2つになるかなと思います。
また、サブタイプの各属性の定義をスーパータイプに譲渡して、Proxyを用いることもあり得るかと思います。
しかし、上記どの方法を選択したとしても親クラスから子クラスのインスタンス
としてクエリ結果を取得することが出来ず(Abstractの場合はそもそもクエリ自体投げれませんが...)、結果として判定処理が増えてしまうことが多々ありました。
以下MTIで、Planの例を実装します。
from django.db import models # Create your models here. class Plan(models.Model): name = models.CharField(max_length=100) monthly_base_fee = models.IntegerField(default=0) def price(self): raise NotImplementedError class HobbyPlan(Plan): def price(self): return self.monthly_base_fee class ProPlan(Plan): pro_rate = models.IntegerField(default=100) def price(self): return self.monthly_base_fee * self.pro_rate
適当にデータを作成後、Planに対してallクエリを投げるてみます。
In [1]: from demo.models import * In [2]: HobbyPlan.objects.create(name='hobby') Out[2]: <HobbyPlan: HobbyPlan object (1)> In [3]: ProPlan.objects.create(name='pro', monthly_base_fee=5000) Out[3]: <ProPlan: ProPlan object (2)> In [4]: for p in Plan.objects.all(): ...: print(type(p)) ...: <class 'demo.models.Plan'> <class 'demo.models.Plan'>
allクエリセットでは子クラス
ではなくPlanのインスタンス
として返却されます。
また、インスタンスがPlanな為、もちろん各子クラスでOverrideしたメソッドは呼ばれません。
In [5]: for p in Plan.objects.all(): ...: p.price() ...: --------------------------------------------------------------------------- NotImplementedError Traceback (most recent call last)
このため、実際に使用する場合には子クラスに条件分岐で絞り込んだ
うえでメソッドを実行する。若しくは子クラスとしてインスタンス取得を複数回繰り返す
必要がありました。
上記のような場合にdjango-polymorphic
のライブラリを用いることで、Planからクエリセットを呼んだ場合でも子クラスのインスタンスとして取得することができ、実装をシンプルにすることができます。
以下を参考にセットアップします。(と言ってもsettingsに値を追加するだけですが)
INSTALLED_APPS += ( 'polymorphic', 'django.contrib.contenttypes', )
先ほどのPlanの継承元をPolymorphicModel
に変更します。
from django.db import models from polymorphic.models import PolymorphicModel class Plan(PolymorphicModel): name = models.CharField(max_length=100) monthly_base_fee = models.IntegerField(default=0) def price(self): raise NotImplementedError class HobbyPlan(Plan): def price(self): return self.monthly_base_fee class ProPlan(Plan): pro_rate = models.IntegerField(default=100) def price(self): return self.monthly_base_fee * self.pro_rate
なんとこれだけ。
DBのmigrationを再作成+再実行し、先ほどのデフォルトで実装したMTIモデルと同様の操作を行ってみます。
In [1]: from demo.models import * In [2]: HobbyPlan.objects.create(name='hobby') Out[2]: <HobbyPlan: HobbyPlan object (1)> In [3]: ProPlan.objects.create(name='pro', monthly_base_fee=5000) Out[3]: <ProPlan: ProPlan object (2)> In [4]: for p in Plan.objects.all(): ...: print(type(p)) ...: <class 'demo.models.HobbyPlan'> <class 'demo.models.ProPlan'>
先ほどまでは<class 'demo.models.Plan'>
と親クラスになっていたのですが、今回は子クラスのインスタンスとして返却されました!これは良さそう。
勿論子クラスのインスタンスな為、メソッドもOverrideした子クラスのメソッドが呼ばれます。
In [11]: for p in Plan.objects.all(): ...: print(f'{p.__class__.__name__}: {p.price()}') ...: HobbyPlan: 0 ProPlan: 500000
実際にどのようにして親クラスのクエリから子クラスのインスタンスとして返却するのか少し覗いてみます。
公式Docをちらっと見たところ、non-polymorphic
なオブジェクトをreal class/type
に変換するget_real_instance
メソッドがありました。(要は親から子クラスにキャストできるってことですね)
使用するとこんな感じです。
In [20]: data = Plan.objects.all().non_polymorphic()[0] In [21]: data Out[21]: <Plan: Plan object (1)> In [22]: data.get_real_instance() Out[22]: <HobbyPlan: HobbyPlan object (1)>
また、上記のNote
にManagerのget_real_instances
について記述がありました。
両者の違いは主に、get_real_instance
はオブジェクトに対してのキャストに対し、get_real_instances
の場合はManagerに定義されており、return値がQuerySet
な為、QuerySetに対してのキャストとなります。
また、get_real_instances
はPolymorphicModelIterable
の__iter__特殊メソッドから呼ばれているようでした。
そのため、forループなどのイテレータ操作を行うと、各オブジェクトが自動的に子クラスとしてインスタンス化されるのだと解釈しました。
また、もし親クラスとして取得したい場合はnon_polymorphic
を用いることで実現可能です。
(上記で既に使用していますが...)
In [9]: for p in Plan.objects.all().non_polymorphic(): ...: print(type(p)) ...: <class 'demo.models.Plan'> <class 'demo.models.Plan'>
django-polymorphicを使うことで、より柔軟性のあるMTIが定義できるようになるなぁと思いました。
また、今回は触れませんがdjango-typed-models
というフレームワークもあります。
こちらはテーブルがMTIではなくSTI
で実装されます。そのためテーブル設計方法のメリデメ+PJによってはこちらを採用することもありかなと思いました。
(まだメジャーバージョンリリースがないので、少し不安ではありますが...)
今回のコードはこちらのgithubに上げてあります。