Django Polymorphic Associations Tutorial

## はじめに
私の記事[Djangoが他のPolymorphicと挙動が異なる件
](https://qiita.com/cohey0727/items/c5c0c03670ae191c6348)でDjangoのPolymorphicModelがいわゆるポリモーフィック関連と異なることを記事にしました。
今回は、「ではDjangoで**ポリモーフィック関連**を実装するにはどうすのか」という点について解説します。

## 環境
- Python 3.7.5
- Django 2.2.1
- SQLite

## ゴール
RailsGuideの[PolymorphicAssociations](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations)の章では以下のようにポリモーフィック関連が実装されています。

```ruby
class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end
```

これに以下のERのような属性を持たせたModelを実装することをゴールにします。

![imageable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235240/29c91dae-5bcc-e9f0-e6fa-01c48a02e79a.png)


`coontent_type`はどのテーブルと関連しているかを示し、`object_id`はどのレコードを関連しているかを示します。


## 実装
### モデル作成
```python
from django.db import models
from django.contrib.contenttypes.models import ContentType


class Picture(models.Model):
    object_id = models.IntegerField(db_index=True)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    file_name = models.CharField()


class Employee(models.Model):
    name = models.CharField()
    email = models.EmailField()


class Product(models.Model):
    name = models.CharField()
    price = models.IntegerField()
```

### Imageableクラスの実装
```python
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
from django.contrib.contenttypes.models import ContentType


class Picture(models.Model):
    object_id = models.IntegerField(db_index=True)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    file_name = models.CharField(max_length=256)
    content_object = GenericForeignKey('content_type', 'object_id')


class Imageable(models.Model):
    class Meta:
        abstract = True

    pictures = GenericRelation(Picture)


class Employee(Imageable):
    name = models.CharField(max_length=256)
    email = models.EmailField()


class Product(Imageable):
    name = models.CharField(max_length=256)
    price = models.IntegerField()
```


### 動作確認
```shell
# マイグレーション
$ python manage.py makemigrations polymorphic_associations
$ python manage.py migrate polymorphic_associations
$ python manage.py shell


# データ作成
>>> from polymorphic_associations.models import Employee, Product
>>>
>>> employee = Employee(name='John', email='test@example.com')
>>> employee.save()
>>> employee.pictures.create(file_name='employee.jpg')
<Picture: Picture object (1)>
>>>
>>> product = Product(name='Desk', price=1000)
>>> product.save()
>>> product.pictures.create(file_name='product.jpg')
<Picture: Picture object (2)>


# データ取得
>>> employee.pictures.all()
<QuerySet [<Picture: Picture object (1)>]>
>>> employee.pictures.first().file_name
'employee.jpg'
>>>
>>> product.pictures.all()
<QuerySet [<Picture: Picture object (2)>]>
>>> product.pictures.first().file_name
'product.jpg'


# SQL確認
>>> str(employee.pictures.all().query)
'SELECT
    "polymorphic_associations_picture"."id",
    "polymorphic_associations_picture"."object_id",
    "polymorphic_associations_picture"."content_type_id",
    "polymorphic_associations_picture"."file_name"
FROM
    "polymorphic_associations_picture"
WHERE
    (
        "polymorphic_associations_picture"."content_type_id" = 2
    AND "polymorphic_associations_picture"."object_id" = 1
    )'
>>>
>>> str(product.pictures.all().query)
'SELECT
    "polymorphic_associations_picture"."id",
    "polymorphic_associations_picture"."object_id",
    "polymorphic_associations_picture"."content_type_id",
    "polymorphic_associations_picture"."file_name"
FROM
    "polymorphic_associations_picture"
WHERE
    (
        "polymorphic_associations_picture"."content_type_id" = 3
    AND "polymorphic_associations_picture"."object_id" = 1
    )'
```

作成されたデータは、`content_type_id`、`object_id`によってテーブル、レコードを特定できていることがわかります。
これによって画像をもつテーブルはすべて`Imageable`を継承することで素早く実装することができます。また画像に関する処理をImageableに実装することで、ロジックが各モデルやサービスに分散することを防ぐことができます。
このソースコードは[Git](https://github.com/Cohey0727/polymorphic_associations)に上げてあります。

## 参考
- https://guides.rubyonrails.org/association_basics.html#polymorphic-associations
- https://stackoverflow.com/questions/51824934/how-to-make-a-django-model-commentable-likeable-and-rateable