1. 通常の継承の場合
以下のようなモデルを例とします。
class Book(models.Model):
title = models.CharField(max_length=100)
published_at = models.DateField()
written_by = models.CharField(max_length=100)
class Manga(Book):
illustrated_by = models.CharField(
max_length=100, verbose_name='作画'
)
class Novel(Book):
is_hardcover = models.BooleanField(verbose_name='単行本?')
データを入れて、テーブル構成を確認してみましょう。
Book.objects.create(
title='Book1', published_at=datetime(2022, 1, 1), written_by='著者1'
)
Manga.objects.create(
title='Manga1', published_at=datetime(2022, 1, 1), written_by='著者1',
illustrated_by='作画1'
)
Novel.objects.create(
title='Novel1', published_at=datetime(2022, 1, 1),
written_by='著者3', is_hardcover=True
)
mysql> select * from override_test_book;
+----+--------+--------------+------------+
| id | title | published_at | written_by |
+----+--------+--------------+------------+
| 1 | Book1 | 2022-01-01 | 著者1 |
| 2 | Manga1 | 2022-01-01 | 著者1 |
| 3 | Novel1 | 2022-01-01 | 著者2 |
+----+--------+--------------+------------+
mysql> select * from override_test_manga;
+-------------+----------------+
| book_ptr_id | illustrated_by |
+-------------+----------------+
| 2 | 作画1 |
+-------------+----------------+
mysql> select * from override_test_novel;
+-------------+--------------+
| book_ptr_id | is_hardcover |
+-------------+--------------+
| 3 | 1 |
+-------------+--------------+
このように通常の継承をした場合、子モデルは <親モデル名>_ptr_id という列をプライマリーキーとして持ち、親モデルと1対1の関係で結びつけられます。Djangoで使用する際はそれらが自動で結合されて渡されるので、継承を意識せずに使用することができます。もちろん、親モデルのカラムに対する filter()
や save()
メソッドも問題なく使えます。
>>> novel = Novel.objects.filter(published_at=datetime(2022, 1, 1)).get(title='Novel1')
>>> novel.title
'Novel1'
>>> novel.published_at = datetime(2022, 4, 1)
>>> novel.save() # 親に対する変更も問題なく save で保存できる
子から親を参照する場合
子モデルは <親モデル名>_ptr
というプロパティを持っています。
このプロパティを使用することで、親モデルのインスタンスを参照することができます。
>>> novel.id # book_ptr_id と同じ
3
>>> novel.book_ptr
<Book: Book object (3)>
親から子を参照する場合
親から子へは子のモデル名で参照することができます。
>>> book = Book.objects.get(pk=3)
>>> book.novel
<Novel: Novel object (3)>
親から子を作成する場合
すでに作成してある親から子を作成する場合には注意が必要です。
objects.create()
メソッドでは、<親モデル名>_ptr
にインスタンスを渡すことで、子モデルのデータを作成することができます。
しかしこの方法で作成してしまうと、親モデルのデータが全てNullと空白に上書きされてしまいます。
>>> book = Book.objects.create(title='Book4', published_at=datetime(2022, 1, 1), written_by='作者4')
>>> Novel.objects.create(book_ptr=book, is_hardcover=True) # 親から子を作成してみる
Traceback (most recent call last):
...
django.db.utils.IntegrityError: (1048, "Column 'published_at' cannot be null")
# book の published_at のデータが None に上書きさるためエラーが出る
今回の場合はNullを禁止しているためにエラーがでますが、テーブル構成によってはエラーも出ずに登録が完了してしまいます。
これを回避するためには、save_base(raw=True)
メソッドを使用する必要があります。
# ↓ のように save_base を使うと安全に保存ができる
>>> novel = Novel(book_ptr=book, is_hardcover=True)
>>> novel.save_base(raw=True) # 保存完了
>>> novel.published_at # ただし、このままでは親の値は参照できない
>>> novel = Novel.objects.get(pk=book.pk) # 再度DBから取得することで参照できるようになる
>>> novel.published_at
datetime.date(2022, 1, 1)
2. AbstractModel の場合
Djangoのモデルには、AbstractModel
というものがあります。
これは、Metaクラスのabstract=True
を設定することで作成できます。
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Book(TimestampMixin, models.Model):
title = models.CharField(max_length=100)
published_at = models.DateField()
written_by = models.CharField(max_length=100)
AbstractModel
に対してはマイグレーションファイルは作成されません。そのため、列の情報だけを持ったMixinのように使用することができます。
ちなみに、models.Field
はmodels.Model
意外のオブジェクトに持たせてもマイグレーションなどは実行されず、参照してもフィールドオブジェクトとしか返されません。
class TimestampMixin: # models.Model を使わない
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Book(TimestampMixin, models.Model):
title = models.CharField(max_length=100)
published_at = models.DateField()
written_by = models.CharField(max_length=100)
>>> book = Book.objects.create(title='test', published_at=datetime(2022, 1, 1), written_by='')
>>> book.created_at
<django.db.models.fields.DateTimeField> # datetime.date(2022, 1, 1) は返ってこない
3. ProxyModelの場合
ProxyModel
も、AbstractModel
と同様にMeta
クラスに設定することで実装できます。
子モデルをProxyModel
にすることで、単一テーブルに対して複数のモデルオブジェクトを作成することができます。
class User(models.Model):
username = models.CharField(max_length=100)
user_status = models.CharField(
max_length=20,
choices=(
('basic', 'ベーシック'),
('premium', 'プレミアムユーザー')
),
default='basic'
)
is_staff = models.BooleanField(default=False)
class Meta:
verbose_name = 'ユーザー'
class PremiumUserManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(user_status='premium')
def create(self, **kwargs):
kwargs['user_status'] = 'premium'
kwargs['is_staff'] = False
return super().create(**kwargs)
class PremiumUser(User):
objects = PremiumUserManager()
class Meta:
verbose_name = 'プレミアムユーザー'
proxy = True
上の例の場合、PremiumUser
テーブルは作成されません。
ProxyModel
を作成することで、プレミアムユーザーにしか使わないメソッドをUser
モデルから分離できたり、objects
を独自にカスタマイズすることでuser_status
を意識することなく開発をすることができます。また、ProxyModel
は管理画面にも独立したモデルとして登録することができます。
例えば、今回の場合はProxyModel
をプレミアムユーザー、ベーシックユーザー、スタッフユーザーそれぞれに分けて作成することで、
それぞれが別のページで一覧表示される、混乱の少ない管理画面を作成することができます。