最近Djangoで改めてフォロー機能のためのモデルを作った際、through
引数を使ってみたところ書きやすいモデルが作れたので記事にしてみました。
特別に高度な書き方をしているわけではありませんが、フォロー機能について検索してみたところこの通りのモデルがなかなか見当たらなかったので、近い機能を作る方の参考になれば幸いです。
Djangoにおける多対多の書き方
Djangoでは多対多を表すモデルを作成する際、書き方が大きく分けて3つあります。
-
ManyToManyField
のみ使用する- 基本的な方法です。この場合、中間テーブルは自動生成されます。
- 中間モデルを作成して、
ManyToManyField
を使用しない- データ同士の繋がり以外の情報(
created_at
など)を持たせるために用います。
- データ同士の繋がり以外の情報(
- 中間モデルを作成して、
ManyToManyField
のthrough
引数に渡す- 今回紹介する方法です。
できること
# user1のフォロワー/フォロイー全部の取得
user1.followers.all()
user1.followees.all()
# user1がフォローしているユーザーのアイテム一覧(Twitterで言うところのタイムライン)取得
Item.objects.filter(user__followers=user1)
# user1がuser2をフォローする
user1.followee_friendships.create(followee=user2)
みたいな書き方が出来るようになります。
実装
中間モデル
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行は同じ意味になります。
Friendship.objects.create(follower=user1, followee=user2)
user1.followee_friendships.create(followee=user2)
これらはuser1
からuser2
へのフォローを表します。
unique_together
組み合わせが一意であるという制約を設けるためのものです。
フォロワーに対するフォロイーの組み合わせは必ず一通りであり、複数作成されることは異常な動作であるためこれを設定しておきます。
なお、フォローとフォロイーがそれぞれ逆になったもの(いわゆる相互フォロー)が存在してもがエラーになることは無いようです。
ユーザーモデル
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つで済みます。
class User(AbstractUser):
followers = models.ManyToManyField(
'User', verbose_name='フォローされているユーザー', through='FriendShip',
related_name='followees', through_fields=('followee', 'follower')
)
ただしverbose_name
を指定できなくなるなど不便が出てきそうなので、2つ宣言しておく方が良さそうです。