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

# 1. 通常の継承の場合

以下のようなモデルを例とします。
```python
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='単行本？')

```
データを入れて、テーブル構成を確認してみましょう。
```python
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
)
```
```sql
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()` メソッドも問題なく使えます。
```python
>>> 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` というプロパティを持っています。
このプロパティを使用することで、親モデルのインスタンスを参照することができます。
```python
>>> novel.id  # book_ptr_id と同じ
3
>>> novel.book_ptr
<Book: Book object (3)>
```

## 親から子を参照する場合
親から子へは子のモデル名で参照することができます。
```python
>>> book = Book.objects.get(pk=3)
>>> book.novel
<Novel: Novel object (3)>
```

### 親から子を作成する場合
すでに作成してある親から子を作成する場合には注意が必要です。
`objects.create()`メソッドでは、`<親モデル名>_ptr`にインスタンスを渡すことで、子モデルのデータを作成することができます。
しかしこの方法で作成してしまうと、親モデルのデータが全てNullと空白に上書きされてしまいます。
```python
>>> 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)` メソッドを使用する必要があります。
```python
# ↓ のように 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`を設定することで作成できます。
```python
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`意外のオブジェクトに持たせてもマイグレーションなどは実行されず、参照してもフィールドオブジェクトとしか返されません。
```python
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`にすることで、単一テーブルに対して複数のモデルオブジェクトを作成することができます。
```python
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`をプレミアムユーザー、ベーシックユーザー、スタッフユーザーそれぞれに分けて作成することで、
それぞれが別のページで一覧表示される、混乱の少ない管理画面を作成することができます。