この記事は JSL (日本システム技研) Advent Calendar 2018 - Qiita 14日目の記事です。
今年のDjango Congress のセッションでManyToManyFieldにthrough属性なるものがあることを知りました。不勉強ですんません。
manytomanyfieldにthrough属性てのを使うと良いらしい #djangocongress
— Kouichi Nishizawa(19) (@koty) 2018年5月19日
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を使わず自力で定義するよりはマシってとこでしょうか。
今回のサンプルコードはこちら。