最近、同僚の @qtatsu に「models.ForeignKeyのrelated_nameに +
を指定すると逆引き不可にできる」ということを教わって、そういえばこのあたりを理解してないなと思って調べた時のメモです。
目次
環境
- Django 3.0.7
models.ForeignKeyにおけるrelated_nameについて
公式ドキュメントはこちら。
ForeignKey.related_name | モデルフィールドリファレンス | Django ドキュメント | Django
ざっくり書くと、「related_nameとは、1対多の関係を持つ2つのモデルにて、1側から多側のオブジェクトを取得する時に、自分の好きな属性名で取得したいときに使うもの」です。
文章だけだと何ともな感じなので、実際に試していきます。
related_nameなし
ColorとAppleモデルがあり、Color : Apple = 1 : 多 の関係があるとします。Appleの color
属性が外部キーで Color を参照しています。
class Color(models.Model): name = models.CharField('色', max_length=30, unique=True) class Apple(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE)
この時のデータは以下とします。facotry_boy使ってデータを用意したとします。
red = ColorFactory(name='赤') yellow = ColorFactory(name='黄') AppleFactory(name='フジ', color=red) AppleFactory(name='秋映', color=red) AppleFactory(name='シナノゴールド', color=yellow) AppleFactory(name='もりのかがやき', color=yellow)
ここで、Colorから関連するAppleを取得したいとします。
ただ、ColorモデルにはAppleに関する情報はどこにもありません。
しかし、Djangoでは
リレーションシップ "反対向き” を理解する
モデルが ForeignKey を持つ場合、外部キーのモデルのインスタンスは最初のモデルのインスタンスを返す Manager にアクセスできます。デフォルトでは、この Manager は FOO_set と名付けられており、FOO には元のモデル名が小文字で入ります。
https://docs.djangoproject.com/ja/3.0/topics/db/queries/#following-relationships-backward
という機能があります。
今回の場合は、 Color.apple_set
で、ColorからAppleを参照できます。
そのため、
colors = Color.objects.all() for color in colors: for apple in color.apple_set.all(): print(f'[{color.name}] {apple.name}')
とすると、
[赤] フジ [赤] 秋映 [黄] シナノゴールド [黄] もりのかがやき
という結果が得られます。
related_nameあり
次に同じようなモデルで related_name
を用意してみます。
今回は related_name='my_apple_color'
としてみます。
# Colorモデルは同じなので略 class AppleWithRelatedName(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='my_apple_color')
同じように表示してみます。
今回は、 related_name
が変わっているので、 color.my_apple_color.all()
でアクセスすることになります。
colors = Color.objects.all() for color in colors: for apple in color.my_apple_color.all(): # my_apple_color属性へ変更 print(f'[{color.name}] {apple.name}')
結果は同じです。
[赤] フジ [赤] 秋映 [黄] シナノゴールド [黄] もりのかがやき
default_related_nameでの指定
「xxx_set
ではなくxxx_list
としたいけど、related_nameを都度指定したくない」というときは、Metaクラスの default_related_name
オプションが使えます。
https://docs.djangoproject.com/ja/3.0/ref/models/options/#django.db.models.Options.default_related_name
default_related_name
では直接名前を指定する他、以下の動的な名前も指定できます。
- '%(class)s' は、フィールドが使用されている子クラスの名前を小文字にした文字列と置換されます。
- '%(app_label)s' は、子クラスが含まれているアプリ名を小文字にした文字列と置換されます。各インストールアプリケーション名はユニークでなければならず、モデルクラスの名は各アプリ内でもユニークでなければなりません。その結果、すべての名前が異なるものとなります。
https://docs.djangoproject.com/ja/3.0/topics/db/models/#abstract-related-name
例えば
class AppleDefaultRelatedName(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE) class Meta: default_related_name = '%(app_label)s_%(class)s_list'
と %(app_label)s_%(class)s_list
を指定した場合は
colors = Color.objects.all() for color in colors: for apple in color.related_name_appledefaultrelatedname_list.all(): print(f'[{color.name}] {apple.name}')
と、Colorの属性は related_name_appledefaultrelatedname_list
になります。
結果も同じです。
[赤] フジ [赤] 秋映 [黄] シナノゴールド [黄] もりのかがやき
prefetch_related()の名前も変わる
ちなみに、 related_name
を指定した場合は、 prefetch_related()
で指定する名前も変更となります。
つまり
class AppleWithRelatedName(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='my_apple_color')
というモデルの場合は、
print('=== normal x all ===') colors = Color.objects.all() for color in colors: for apple in color.my_apple_color.all(): print(f'[{color.name}] {apple.name}') print('=== prefetch_related ===') colors = Color.objects.prefetch_related('my_apple_color') for color in colors: for apple in color.my_apple_color.all(): print(f'[{color.name}] {apple.name}')
のように、 prefetch_related('my_apple_color')
となります。
発行されるSQLを見ると、all()の場合は3回です。
(0.000) SELECT "related_name_color"."id", "related_name_color"."name" FROM "related_name_color"; args=() (0.000) SELECT "related_name_applewithrelatedname"."id", "related_name_applewithrelatedname"."name", "related_name_applewithrelatedname"."color_id" FROM "related_name_applewithrelatedname" WHERE "related_name_applewithrelatedname"."color_id" = 104; args=(104,) (0.000) SELECT "related_name_applewithrelatedname"."id", "related_name_applewithrelatedname"."name", "related_name_applewithrelatedname"."color_id" FROM "related_name_applewithrelatedname" WHERE "related_name_applewithrelatedname"."color_id" = 105; args=(105,)
一方、prefetch_related()の場合は2回で、正しく動いていることが分かります。
(0.000) SELECT "related_name_color"."id", "related_name_color"."name" FROM "related_name_color"; args=() (0.001) SELECT "related_name_applewithrelatedname"."id", "related_name_applewithrelatedname"."name", "related_name_applewithrelatedname"."color_id" FROM "related_name_applewithrelatedname" WHERE "related_name_applewithrelatedname"."color_id" IN (104, 105); args=(104, 105)
逆引きを不可にする
今回ColorからAppleを取得していますが、Djangoではこれを 逆引き
と呼ぶようです。
https://docs.djangoproject.com/ja/3.0/topics/db/models/#be-careful-with-related-name-and-related-query-name
この逆引きを不可にしたい場合も related_name
を使います。
公式ドキュメントにある通り、+
だけ、もしくは +
で終わるような名前を指定します。
If you'd prefer Django not to create a backwards relation, set related_name to '+' or end it with '+'.
https://docs.djangoproject.com/ja/3.0/ref/models/fields/#django.db.models.ForeignKey.related_name
class AppleNoReverseWithPlus(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='+') class AppleNoReverseWithEndPlus(models.Model): name = models.CharField('品種名', max_length=30, unique=True) color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='end_plus+')
出力してみます。
colors = Color.objects.all() for color in colors: print(dir(color)) for apple in color.applenoreversewithplus.all(): print(f'[{color.name}] {apple.name}')
とすると、 for apple in color.applenoreversewithplus.all():
の行でエラーになります。
AttributeError: 'Color' object has no attribute 'applenoreversewithplus'
print(dir(color))
の結果を抜粋すると
[..., 'apple_set', ..., 'my_apple_color', ..., 'related_name_appledefaultrelatedname_list', ...]
と、+
や end_plus+
とした時の属性が含まれていません。
これにより、逆引きができなくなっています。
1モデル中に、同一モデルに対する外部キー参照が複数ある場合のrelated_name
例えば、 fruit_color
と bud_color
で、Colorに対する参照を持つとします。
class AppleWith2Color(models.Model): name = models.CharField('品種名', max_length=30, unique=True) fruit_color = models.ForeignKey(Color, on_delete=models.CASCADE) bud_color = models.ForeignKey(Color, on_delete=models.CASCADE)
related_name
を指定しない場合、マイグレーションを実行するとエラーになります。
$ python manage.py makemigrations SystemCheckError: System check identified some issues: ERRORS: related_name.AppleWith2Color.bud_color: (fields.E304) Reverse accessor for 'AppleWith2Color.bud_color' clashes with reverse accessor for 'AppleWith2Color.fruit_color'. HINT: Add or change a related_name argument to the definition for 'AppleWith2Color.bud_color' or 'AppleWith2Color.fruit_color'. related_name.AppleWith2Color.fruit_color: (fields.E304) Reverse accessor for 'AppleWith2Color.fruit_color' clashes with reverse accessor for 'AppleWith2Color.bud_color'. HINT: Add or change a related_name argument to the definition for 'AppleWith2Color.fruit_color' or 'AppleWith2Color.bud_color'.
そのため、明示的に related_name
を指定します。これにより、マイグレーションができるようになります。
class AppleWith2Color(models.Model): name = models.CharField('品種名', max_length=30, unique=True) fruit_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='fruits') bud_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='buds')
Abstractなモデルにて外部キーを使っている場合のrelated_name
Djangoアプリを書く中で、共通な項目をまとめて定義したいことがあります。
その場合
- 共通で使う項目はAbstractなモデルに定義
- 実際のクラスは、Abstractなモデルを継承して定義
とするかもしれません。
例えば、 bud_color
属性を共通で持たせ、明示的な名前を付けたいとします。
その場合、Abstractなクラスに
class ModelBase(models.Model): bud_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='bud_colors') class Meta: abstract = True
と定義します。
そしてこのクラスを
class Fruit(ModelBase): name = models.CharField('品種名', max_length=30, unique=True) class Potato(ModelBase): name = models.CharField('品種名', max_length=30, unique=True)
のように使うとします。
しかし、これでは makemigrations時に失敗します。
$ python manage.py makemigrations SystemCheckError: System check identified some issues: ERRORS: related_name.Fruit.bud_color: (fields.E304) Reverse accessor for 'Fruit.bud_color' clashes with reverse accessor for 'Potato.bud_color'. HINT: Add or change a related_name argument to the definition for 'Fruit.bud_color' or 'Potato.bud_color'. related_name.Fruit.bud_color: (fields.E305) Reverse query name for 'Fruit.bud_color' clashes with reverse query name for 'Potato.bud_color'. HINT: Add or change a related_name argument to the definition for 'Fruit.bud_color' or 'Potato.bud_color'. related_name.Potato.bud_color: (fields.E304) Reverse accessor for 'Potato.bud_color' clashes with reverse accessor for 'Fruit.bud_color'. HINT: Add or change a related_name argument to the definition for 'Potato.bud_color' or 'Fruit.bud_color'. related_name.Potato.bud_color: (fields.E305) Reverse query name for 'Potato.bud_color' clashes with reverse query name for 'Fruit.bud_color'. HINT: Add or change a related_name argument to the definition for 'Potato.bud_color' or 'Fruit.bud_color'.
原因は、公式ドキュメントのこちらです。
related_name と related_query_name の利用に関して注意するべき点
ForeignKey もしくは ManyToManyField に対して related_name または related_query_name を使う場合、そのフィールドに対して 一意の 逆引き名およびクエリ名を常に定義しなければなりません。これは抽象基底クラスにおいて、フィールドが継承した子クラスそれぞれに含まれ、継承される毎にその属性値が完全に同じ値 (related_name と related_query_name も含む) となるため、通常問題となります。
そのため、Abstractなモデルを変更します。
class ModelBase(models.Model): bud_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='%(class)s_bud_colors') class Meta: abstract = True
これでマイグレーションが実行できます。
動作確認です。
pink = ColorFactory(name='ピンク') purple = ColorFactory(name='紫') FruitFactory(name='フジ', leaf_color=green, bud_color=pink) FruitFactory(name='秋映', leaf_color=green, bud_color=pink) PotatoFactory(name='男爵', leaf_color=green, bud_color=purple) PotatoFactory(name='メークイン', leaf_color=green, bud_color=purple) print('=== fruit ===') fruits_color = Color.objects.prefetch_related('fruit_bud_colors') for color in fruits_color: for fruit in color.related_name_fruit_buds.all(): print(f'[{color.name}] {fruit.name}') print('=== potato ===') potato_colors = Color.objects.prefetch_related('potato_bud_colors') for color in potato_colors: for potato in color.related_name_potato_buds.all(): print(f'[{color.name}] {potato.name}')
を実行すると、
=== fruit === [ピンク] フジ [ピンク] 秋映 === potato === [紫] 男爵 [紫] メークイン
となりました。
related_query_nameについて
公式ドキュメントはこちら
https://docs.djangoproject.com/ja/3.0/ref/models/fields/#django.db.models.ForeignKey.related_query_name
DjangoのQuerySet APIでは、1側から多側の項目に対してfilter()やorder_by()を使うこともできます。
その場合、引数に指定する名前は
- default_related_name
- related_name
です。
ただ、それらとは名前を別にしたい場合は、 related_query_name
を使います。
例えば、
class AppleWith3Color(models.Model): name = models.CharField('品種名', max_length=30, unique=True) bud_color = models.ForeignKey(Color, on_delete=models.CASCADE) leaf_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='leaf_colors') fruit_color = models.ForeignKey(Color, on_delete=models.CASCADE, related_name='fruit_colors', related_query_name='my_fruit_colors') class Meta: default_related_name = 'default_colors'
というモデルがあったとします。
このモデルに対しQuerySetを使う場合、こんな感じに名前を指定します。
項目名 | default_related_name | related_name | related_query_name | 結果 |
---|---|---|---|---|
bud_color | あり | 無し | 無し | default_related_nameを使う |
leaf_color | あり | あり | 無し | related_nameを使う |
fruit_color | あり | あり | あり | related_query_nameを使う |
実際に試してみます。
まずはデータです。filterで確認しやすいよう、「金のりんご」という除外してほしいデータを用意しました。
red = ColorFactory(name='赤') yellow = ColorFactory(name='黄') pink = ColorFactory(name='ピンク') green = ColorFactory(name='緑') gold = ColorFactory(name='金') AppleWith3ColorFactory(name='フジ', fruit_color=red, bud_color=pink, leaf_color=green) AppleWith3ColorFactory(name='秋映', fruit_color=red, bud_color=pink, leaf_color=green) AppleWith3ColorFactory(name='シナノゴールド', fruit_color=yellow, bud_color=pink, leaf_color=green) AppleWith3ColorFactory(name='もりのかがやき', fruit_color=yellow, bud_color=pink, leaf_color=green) AppleWith3ColorFactory(name='ピンクレディ', fruit_color=pink, bud_color=pink, leaf_color=green) AppleWith3ColorFactory(name='金のりんご', fruit_color=gold, bud_color=gold, leaf_color=gold)
まずは、 default_related_name
のみ定義してある場合のfilterです。
default_colors = Color.objects.filter(default_colors__name='フジ') for color in default_colors: for apple in color.default_colors.all(): print(f'[{color.name}] {apple.name}') # => # [ピンク] フジ # [ピンク] 秋映 # [ピンク] シナノゴールド # [ピンク] もりのかがやき # [ピンク] ピンクレディ
フジと同じ bud_color
を持つリンゴだけが抽出されました。
続いて、 default_related_name
と related_name
を定義してある場合のfilterです。
bud_colors = Color.objects.filter(leaf_colors__name='フジ') for color in bud_colors: for apple in color.leaf_colors.all(): print(f'[{color.name}] {apple.name}') # => # [緑] フジ [緑] 秋映 [緑] シナノゴールド [緑] もりのかがやき [緑] ピンクレディ
フジと同じ leaf_color
を持つリンゴだけが抽出されました。
最後に、 related_query_name
も定義してある場合のfilterです。
まずは、 related_name
で指定してある fruit_colors
を使用してみますが、エラーとなります。
fruit_colors = Color.objects.filter(fruit_colors__name='フジ') # => # django.core.exceptions.FieldError: Cannot resolve keyword 'fruit_colors' into field. Choices are: apple, buds, default_colors, fruit_bud_colors, fruits, id, leaf_colors, my_apple_color, my_fruit_colors, name, potato_bud_colors, related_name_appledefaultrelatedname_list
続いて、 related_query_name
で指定してある my_fruit_colors
を使うと動作しました。
[赤] フジ [赤] 秋映
フジと同じ fruit_color
を持つリンゴだけが抽出されました。
ちなみに、order_byでもrelated_nameが有効になっています。
print('=== order_by asc ===') order_by_name = Color.objects.order_by('my_fruit_colors__name') for color in order_by_name: print(color.name) print('=== order_by desc ===') order_by_name = Color.objects.order_by('-my_fruit_colors__name') for color in order_by_name: print(color.name) # => === order_by asc === 緑 黄 黄 ピンク 赤 赤 金 === order_by desc === 金 赤 赤 ピンク 黄 黄 緑
ソースコード
GitHubに上げました。 related_name
アプリが今回のファイルです。
https://github.com/thinkAmi-sandbox/django_30-sample