ManyToManyFieldのthroughを使ってDjangoで扱いやすいフォローモデルを作る

最近Djangoで改めてフォロー機能のためのモデルを作った際、`through`引数を使ってみたところ書きやすいモデルが作れたので記事にしてみました。

特別に高度な書き方をしているわけではありませんが、フォロー機能について検索してみたところこの通りのモデルがなかなか見当たらなかったので、近い機能を作る方の参考になれば幸いです。

## Djangoにおける多対多の書き方
Djangoでは多対多を表すモデルを作成する際、書き方が大きく分けて3つあります。

* `ManyToManyField`のみ使用する
  * 基本的な方法です。この場合、中間テーブルは自動生成されます。
* 中間モデルを作成して、`ManyToManyField`を使用しない
  * データ同士の繋がり以外の情報(`created_at`など)を持たせるために用います。
* 中間モデルを作成して、`ManyToManyField`の`through`引数に渡す
  * 今回紹介する方法です。

## できること
```py
# user1のフォロワー/フォロイー全部の取得
user1.followers.all()
user1.followees.all()

# user1がフォローしているユーザーのアイテム一覧(Twitterで言うところのタイムライン)取得
Item.objects.filter(user__followers=user1)

# user1がuser2をフォローする
user1.followee_friendships.create(followee=user2)
```

みたいな書き方が出来るようになります。

## 実装
### 中間モデル
```py
class FriendShip(models.Model):
    follower = models.ForeignKey('User', on_delete=models.CASCADE, related_name='followee_friendships')
    followee = models.ForeignKey('User', on_delete=models.CASCADE, related_name='follower_friendships')

    class Meta:
        unique_together = ('follower', 'followee')
```
フォローの関係性を表す中間モデルです。`User`モデルに対してフォロー、フォロイーとしてそれぞれに`Foreignkey`を向けています。

ユーザーが削除されるとこれらの関係も削除されるべきなので、`on_delete`引数は`models.CASCADE`です。これによって`Foreignkey`の対象データが削除されると、自動でこのデータも削除されます。

#### related_name
同じクラスに対して複数`Foreignkey`を使用する場合は`related_name`を設定しないと`manage.py makemigrations`コマンドの時点でエラーが出ます。これを設定することで外部キーの接続先からこのモデルに対して、つまり逆向きの参照が出来るようになります。

ここで`follower_friendships`は`objects`のような振る舞いをします。例えば、以下の2行は同じ意味になります。

```py
Friendship.objects.create(follower=user1, followee=user2)
user1.followee_friendships.create(followee=user2)
```

これらは`user1`から`user2`へのフォローを表します。

#### unique_together
組み合わせが一意であるという制約を設けるためのものです。

フォロワーに対するフォロイーの組み合わせは必ず一通りであり、複数作成されることは異常な動作であるためこれを設定しておきます。

なお、フォローとフォロイーがそれぞれ逆になったもの(いわゆる相互フォロー)が存在してもがエラーになることは無いようです。

### ユーザーモデル
```py
class User(AbstractUser):
    followees = models.ManyToManyField(
        'User', verbose_name='フォロー中のユーザー', through='FriendShip',
        related_name='+', through_fields=('follower', 'followee')
    )
    followers = models.ManyToManyField(
        'User', verbose_name='フォローされているユーザー', through='FriendShip', 
        related_name='+', through_fields=('followee', 'follower')
    )
```

#### AbstractUser
`AbstractUser`はデフォルトのUserを拡張するためのものです。`User`がすでに持っているフィールドを上書きしたり、新たに追加する時に用います。

これはより良い情報がすでにいくつかあるので、詳細は解説記事または公式ドキュメントを参照のこと。

#### ManyToManyField
`through`引数に`FriendShip`モデルを指定しています。これで`followees`, `followers`を呼び出すだけで、中間モデルを通したデータが得られるようになります。

#### related_name
`+`を指定することで逆参照が不要であることを示します。ドキュメントには以下のように書かれています。

> If you’d prefer Django not to create a backwards relation, set related_name to '+' or end it with '+'. For example, this will ensure that the User model won’t have a backwards relation to this model:
> 
> https://docs.djangoproject.com/en/3.0/ref/models/fields/#django.db.models.ForeignKey.related_name

#### through_fields
`ManyToManyField`においてあるデータとこれに紐づく別のデータをソースとターゲットとするとき、`through_fields`引数には`(ソース, ターゲット)`を渡します。

`user.followee`はあるユーザーのフォロイーを取得したいので`('follower', 'followee')`を指定し、`user.follower`はその逆を指定しています。

### 余談
`related_name`で逆参照を指定すれば、`ManyToManyField`は1つで済みます。

```py
class User(AbstractUser):
    followers = models.ManyToManyField(
        'User', verbose_name='フォローされているユーザー', through='FriendShip', 
        related_name='followees', through_fields=('followee', 'follower')
    )
```

ただし`verbose_name`を指定できなくなるなど不便が出てきそうなので、2つ宣言しておく方が良さそうです。