@kotyのブログ

PythonとかAWSとか勉強会のこととかを、田舎者SEがつづります。記事のライセンスは"CC BY"でお願いします。

Django ManyToManyFieldのthrough属性を調べた

この記事は JSL (日本システム技研) Advent Calendar 2018 - Qiita 14日目の記事です。

今年のDjango Congress のセッションでManyToManyFieldにthrough属性なるものがあることを知りました。不勉強ですんません。

through属性を使うと独自に定義した中間モデルを使えます。中間モデルに有効日from-toや有効フラグなどを指定できるようになるわけです。存在を知っただけで試して見なかったので、アドベントカレンダーのネタづくりも兼ねて試してみます。

以下のようにモデルを作りました。djangoのバージョンは2.1.4、Python 3.6.3 です。

class MyUser(models.Model):
    username = models.CharField(max_length=100)
    user_avatar = models.ForeignKey(
        'Avatar',
        null=True,
        on_delete=models.PROTECT
    )


class Team(models.Model):
    name = models.CharField(max_length=100)
    members = models.ManyToManyField(MyUser, through='TeamAssign')


class TeamAssign(models.Model):
    user = models.ForeignKey(MyUser, on_delete=models.CASCADE)
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    enable_from = models.DateField()
    enable_to = models.DateField(null=True)

    class Meta:
        unique_together = (('user', 'team', ), )

通常のManyToManyFieldであれば models.ManyToManyField(MyUser) としますが、中間モデルを明示的に定義し through 属性で指定しています。 とりあえず モデルを作っていきます。

teamA = Team.objects.create(name='A Team')

scott = MyUser.objects.create(username='scott')

tiger = MyUser.objects.create(username='tiger')

次に普通にモデルを追加してみます。

teamA.members.add(scott)

すると

AttributeError: Cannot use add() on a ManyToManyField which specifies an intermediary model. Use api.TeamAssign's Manager instead.

ダメでした。そこで

from datetime import datetime as dt
TeamAssign.objects.create(team=teamA, user=scott, enable_from=dt(2018,12, 16))
TeamAssign.objects.create(team=teamA, user=tiger, enable_from=dt(2018,12, 17))

で登録できました。中間モデルを明示的にcreateする必要があるようです。仕方ないといえば仕方ない。

次にデータを取得してみます。

teamA.members.all()

すると

<QuerySet [<MyUser: MyUser object (1)>, <MyUser: MyUser object (2)>]>

2件取れました。通常のManyToManyFieldと同じ使い勝手です。 enable_from <= 今日 を満たすmembersを取ってみます。

teamA.members.filter(enable_from__gt=dt(2018, 12, 16))

すると

FieldError: Cannot resolve keyword 'enable_from' into field. Choices are: custompks, id, profile, team, teamassign, user_avatar, user_avatar_id, username

無理だった。そりゃそうか。。。all()で全部取ってきてからフィルタをかけるしかないっぽい。

超便利ってほどではないけど、ManyToManyFieldを使わず自力で定義するよりはマシってとこでしょうか。

今回のサンプルコードはこちら