17
18

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 3 years have passed since last update.

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

Last updated at Posted at 2020-03-21

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

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

Djangoにおける多対多の書き方

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

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

できること

# 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_friendshipsobjectsのような振る舞いをします。例えば、以下の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つ宣言しておく方が良さそうです。

17
18
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
17
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?