Django ORMで ___. を正しく理解する

user_id・user.id・user__id の違いを一度で理解する

Django のコードでこうした表記を一度は見たことがあるでしょう。

Post.objects.filter(user_id=1)
Post.objects.filter(user__id=1)
post.user.id
post.user_id

アンダースコア _、ダブルアンダースコア __、ドット . が混在していると、混乱しやすいです。「同じものじゃないの?」と混ぜて使い始めると、微妙な動作差や性能問題が発生します。

この記事では、

  • Django ORM で _ / __ / .それぞれ正確に何を意味するか
  • user_id / user.id / user__id意味と性能差
  • 実務で いつどの表記を使うと良いか の基準

を一度に整理します。


1. 大まかなイメージ:三つの記号の役割



まず三つを簡単にまとめると:

  • _(アンダースコア一つ)
  • 名前の一部に過ぎません。
  • 例:created_atuser_id
  • 特に Django では ForeignKey<フィールド名>_id という実際の DB カラムを自動で作ります。

  • __(アンダースコア二つ)

  • Django ORM 専用の「ルックアップ区切り文字」です。
  • filter()exclude()order_by()values() などで使う キーワード引数名の中でのみ 意味があります。
  • 例:

    • age__gte=20(フィールド + 条件)
    • user__email='a@b.com'(リレーションをたどって別のフィールドにアクセス)
    • created_at__date=...(変換/トランスフォーム後の条件)
  • .(ドット)

  • Python の 属性アクセス演算子です。
  • obj.userobj.user.idqs.filter(...).order_by(...) など、すべてドット演算子です。
  • ORM の観点では「メモリ上にある Python オブジェクトの属性にアクセス」するだけです。(必要ならこの過程でクエリが追加で発行されることもあります – Lazy Loading)

それでは、Django ORM の観点からもう少し詳しく見ていきましょう。


2. __id:ForeignKey 周辺での意味

2.1 モデル定義の例

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

このように定義すると、DB には大体こんなカラムが作られます:

  • post テーブル
  • id(PK)
  • user_id実際の FK カラム名
  • title

Django は:

  • Python オブジェクトで post.user という 属性(User インスタンス)を作り、
  • 同時に post.user_id という 整数(または PK タイプ)属性も作ります。

つまり:

post.user    # -> User オブジェクト(必要ならクエリ 1 回追加)
post.user_id # -> User の PK 値(既に持っている値、追加クエリなし)

2.2 クエリで _id を使う

ForeignKey の user に対してクエリするときは、いくつかの表現が可能です。

# 1) FK フィールドに User オブジェクトを渡す
Post.objects.filter(user=request.user)

# 2) FK フィールドに PK 値を渡す(同じく動作)
Post.objects.filter(user=request.user.id)

# 3) _id カラム名を直接使う
Post.objects.filter(user_id=request.user.id)

これら三つは 結局同じ WHERE 条件 でコンパイルされます(単純 PK 比較の場合)。

まとめ: _id は * 「FK フィールドが実際に DB に持っているカラム名」 * ORM では そのカラムを直接使いたいときに使う名前です。


3. __ ダブルアンダースコアチェーン:Django ORM ルックアップ区切り文字



ここからは Django ORM 独自の構文です。

Django 公式ドキュメントで __「ORM lookup separator」 と呼ばれます。 フィールド名を連結してリレーションをたどったり、条件/変換を付けたりするときに使います。

3.1 基本パターン

形式は次の三つがほぼすべてです。

  1. フィールド名__lookup * 例:age__gte=20name__icontains='kim'
  2. リレーションフィールド__別フィールド * 例:user__email='a@b.com'profile__company__name='...'
  3. フィールド__transform__lookup * 例:created_at__date=date.today()created_at__year__gte=2024

例:

# 1) 単純比較
Post.objects.filter(title__icontains="django")

# 2) リレーションをたどって条件
Post.objects.filter(user__email__endswith="@example.com")

# 3) 日付フィールドの年だけを取り出して比較
Post.objects.filter(created_at__year=2025)

このとき user__email のようにリレーションをたどると、DB 側では JOIN が発生します。

コア: __ が出てくると「ORM が SQL を作るときに何かもっと複雑なことをしている(JOIN / 関数 / 条件)」ということを理解すれば十分です。


4. . ドット演算子と user.id、user_id

では user.iduser_id を一緒に見てみましょう。

4.1 Django インスタンスでの違い

post = Post.objects.first()

post.user       # User オブジェクト(必要ならこの瞬間 SELECT クエリ 1 回追加)
post.user.id    # 上で取得した User オブジェクトの PK
post.user_id    # Post テーブルに既にある FK 値

重要なポイント:

  • post.user を最初にアクセスした瞬間、該当 User がまだロードされていなければ 追加クエリ が発行されます。 (Lazy Loading)
  • post.user_id は既に post オブジェクトに入っている整数値なので 追加クエリなし
  • したがって「PK 値だけが必要なとき」は user_id を使う方が軽量です。

4.2 性能例:リストページでの違い

# views.py
posts = Post.objects.all()   # select_related は使わない

for post in posts:
    print(post.user.id)
  • posts を取得するクエリ:1 回
  • ループを回しながら post.user に最初にアクセスするときに User テーブルの検索クエリが 1 回ずつ追加 → Post が 100 個なら User クエリも最大 100 回…(典型的な N+1 問題)

対して:

for post in posts:
    print(post.user_id)
  • user_id は既に Post テーブルから取得したカラムなので 追加クエリなし

もちろん以下のように select_related を使って事前に JOIN しておけば post.user を使っても追加クエリは発生しません。

posts = Post.objects.select_related("user")

for post in posts:
    print(post.user.id)  # 追加クエリなしで使用可能

5. user_iduser.iduser__id を正確に比較

では混乱しやすい三つを一度に見てみましょう。

5.1 user_id – DB カラムを直接使う

  • どこで?
  • モデルインスタンスで:post.user_id
  • ORM クエリで:filter(user_id=1)order_by("user_id") など
  • 意味
  • post テーブルの 実際の FK カラム (user_id) をそのまま使うこと。
  • 性能
  • インスタンスで読むときは 追加クエリなし
  • クエリで使うと最終的に WHERE "post"."user_id" = 1 という条件になります。

5.2 user.id – リレーションをたどって PK にアクセス

  • どこで?
  • Python オブジェクトでのみ 使います。 (post.user.id)
  • 意味
  • まず post.user(User オブジェクト)を取得し、そのオブジェクトの id 属性を読むこと。
  • 性能
  • post.user がまだロードされていなければ その瞬間追加クエリ が発行されます。
  • select_related("user") を使っていれば同じクエリ内で事前に取得できるので追加クエリなし。

まとめ: * 「値だけが必要」なら user_id * 「User の他のフィールドも一緒に使う」なら user.フィールド

5.3 user__id – ORM ルックアップでリレーションをたどって条件を付ける

  • どこで?
  • ORM クエリメソッドのキーワード引数名でのみ登場
  • 例:Post.objects.filter(user__id=1)
  • 意味
  • user という FK をたどって User テーブルの id フィールドに条件 を付けること。
  • 動作
  • 文字通り JOIN user ON post.user_id = user.id という構造になり、WHERE user.id = 1 というクエリが生成されます。
  • 実際の SQL
  • 単純なケースでは ORM が自動で最適化して user_id = 1 に近い結果を返すこともありますが、モデルは「リレーションをたどるクエリ」 と考えるのが安全です。

6. _ vs __ が混乱しやすいパターンの整理

6.1 フィルタでよく見られる組み合わせ

# 1) FK カラム名を直接使う (_id)
Post.objects.filter(user_id=1)

# 2) FK フィールドに PK 値を渡す
Post.objects.filter(user=1)

# 3) リレーションをたどって条件を指定 (__)
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")

実務での基準は:

  • 単に FK の PK 値でフィルタ → user_id=... または user=request.user
  • 関連モデルの 他のフィールドでフィルタuser__email=...user__is_active=True のように __ を使う

6.2 _ は「名前の一部」、__ は「ORM 演算子」

見た目が似ているだけで、意味は全く異なります。

  • _(アンダースコア)
  • created_atuser_idis_active など 単なる名前
  • Python 変数、モデルフィールド、DB カラムどこでも登場

  • __(ダブルアンダースコア)

  • オリジナルの ORM クエリでのみ特殊な意味
  • filter()exclude()annotate()values() など キーワード引数名の中でのみ 文法として認識
  • 「フィールド → リレーション → 変換 → 条件」を連結する区切り文字

7. 実務で覚えておくと良いルールのまとめ

  1. FK 値だけでフィルタする場合 python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id) どちらも OK。チームのコンベンションに合わせるが、個人的には オブジェクトを渡す方(user=request.user を読みやすいと好むケースが多い。

  2. 関連モデルの他のフィールドでフィルタする場合 python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com") このときに __ チェーンが本当に力を発揮します。

  3. テンプレート/ビューで FK 値だけが必要なとき ```django {# 悪い例(追加クエリが発生する可能性) #} {{ post.user.id }}

{# より軽量(追加クエリなし) #} {{ post.user_id }} `` 特にselect_related("user")` を使っていない場合、リスト画面で N+1 クエリの原因になります。

  1. User の複数フィールドを頻繁に使う場合 python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email) select_related + post.user.フィールド の組み合わせが最もクリーンで、クエリ数も少ない。

  2. __ チェーンは「JOIN が増える可能性」を常に意識 * 特に 逆方向 FKuser__post__title など)や 複数回の filter().filter() チェーン で誤用すると、同じテーブルが何度も JOIN されて重複結果が出ることも。


8. 一行でまとめ

  • _ : 単なる名前。特に <フィールド名>_id は FK が持つ実際のカラム名。
  • __ : ORM 専用演算子。フィールド/リレーション/条件/変換を連結する ルックアップ区切り文字
  • . : Python の属性アクセス。user.id は User オブジェクトを取得してその PK を読むこと。
  • user_idPost テーブルの FK 値
  • user.idUser オブジェクトの PK 値
  • user__idORM クエリで「user リレーションをたどって User.id に条件」

これら三つを正確に区別しておけば:

  • クエリの意図がより明確になる。
  • 不要な JOIN や N+1 クエリを減らせる。
  • チーム内のコードレビューで「user_id を使う方が良い」などのフィードバックがスムーズになる。

image