Djangoのmodels.ForeignKeyにおけるrelated_nameとrelated_query_nameについて調べてみた

最近、同僚の @qtatsu に「models.ForeignKeyのrelated_nameに + を指定すると逆引き不可にできる」ということを教わって、そういえばこのあたりを理解してないなと思って調べた時のメモです。

 
目次

 

環境

 

models.ForeignKeyにおけるrelated_nameについて

公式ドキュメントはこちら。
ForeignKey.related_name | モデルフィールドリファレンス | Django ドキュメント | Django

ざっくり書くと、「related_nameとは、1対多の関係を持つ2つのモデルにて、1側から多側のオブジェクトを取得する時に、自分の好きな属性名で取得したいときに使うもの」です。

文章だけだと何ともな感じなので、実際に試していきます。

 

ColorとAppleモデルがあり、Color : Apple = 1 : 多 の関係があるとします。Applecolor 属性が外部キーで 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='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}')

 
結果は同じです。

[赤] フジ
[赤] 秋映
[黄] シナノゴールド
[黄] もりのかがやき

 

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_colorbud_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 も含む) となるため、通常問題となります。

https://docs.djangoproject.com/ja/3.0/topics/db/models/#be-careful-with-related-name-and-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 ===
[紫] 男爵
[紫] メークイン

となりました。

 

公式ドキュメントはこちら
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_namerelated_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