7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Django】Model継承時の挙動まとめ AbstractModel, ProxyModel

Last updated at Posted at 2022-05-27

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.Fieldmodels.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をプレミアムユーザー、ベーシックユーザー、スタッフユーザーそれぞれに分けて作成することで、
それぞれが別のページで一覧表示される、混乱の少ない管理画面を作成することができます。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?