1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DjangoのORM — EloquentとDjango ORMを比較した

1
Posted at

はじめに

DjangoとDRFを触ってきて、ORM部分をもっと深掘りしたくなった。

Django ORMはDjango入門の記事で基本的なCRUDは触れたが、JOIN・集計・サブクエリ・N+1問題あたりの実践的な使い方を整理できていなかった。LaravelのEloquentと比較しながらまとめる。


モデルのリレーション定義

まず今回使うモデルを定義する。

# models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=50)

    class Meta:
        db_table = "categories"

    def __str__(self):
        return self.name


class Post(models.Model):
    title    = models.CharField(max_length=200)
    body     = models.TextField()
    author   = models.ForeignKey(
        "Author",
        on_delete=models.CASCADE,
        related_name="posts",
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="posts",
    )
    tags     = models.ManyToManyField("Tag", related_name="posts")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "posts"


class Author(models.Model):
    name  = models.CharField(max_length=50)
    email = models.EmailField(unique=True)

    class Meta:
        db_table = "authors"


class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)

    class Meta:
        db_table = "tags"

PHPのEloquentと並べると:

<?php
// Laravel Eloquent
class Post extends Model
{
    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }
}

Laravelはモデルにリレーションメソッドを書くが、DjangoはForeignKeyやManyToManyFieldをフィールドとして定義する。related_nameでリバースリレーションの名前を指定する(LaravelのhasManyに相当)。


基本的なCRUD

作成

<?php
// Laravel
$post = Post::create([
    "title" => "Pythonの学習記録",
    "body"  => "...",
]);
# Django
post = Post.objects.create(
    title="Pythonの学習記録",
    body="...",
    author=author,
)

# または2ステップで
post = Post(title="Pythonの学習記録", body="...")
post.author = author
post.save()

読み取り

<?php
// Laravel
$posts = Post::all();
$post  = Post::find(1);
$post  = Post::findOrFail(1);
$posts = Post::where("title", "like", "%Python%")->get();
# Django
posts = Post.objects.all()
post  = Post.objects.get(id=1)          # 見つからなければDoesNotExist
post  = Post.objects.filter(id=1).first()  # 見つからなければNone
posts = Post.objects.filter(title__contains="Python")

更新

<?php
// Laravel
Post::where("id", 1)->update(["title" => "新しいタイトル"]);
$post->update(["title" => "新しいタイトル"]);
# Django
Post.objects.filter(id=1).update(title="新しいタイトル")

# インスタンス経由
post.title = "新しいタイトル"
post.save()

# 特定フィールドだけ保存(update_fields)
post.title = "新しいタイトル"
post.save(update_fields=["title"])  # titleカラムだけUPDATE

update_fieldsを使うと必要なカラムだけUPDATEするSQLが発行される。Laravelにはない細かい制御ができる。

削除

Post.objects.filter(id=1).delete()

post = Post.objects.get(id=1)
post.delete()

クエリのルックアップ記法

Django ORMの独特な部分。__(ダブルアンダースコア)でフィールド名とルックアップを繋ぐ。

# 数値の比較
Post.objects.filter(id__lt=10)          # id < 10
Post.objects.filter(id__lte=10)         # id <= 10
Post.objects.filter(id__gt=5)           # id > 5
Post.objects.filter(id__gte=5)          # id >= 5
Post.objects.filter(id__in=[1, 2, 3])   # IN

# 文字列
Post.objects.filter(title__exact="Python")      # = 'Python'
Post.objects.filter(title__iexact="python")     # 大文字小文字無視
Post.objects.filter(title__contains="Python")   # LIKE '%Python%'
Post.objects.filter(title__icontains="python")  # 大文字小文字無視
Post.objects.filter(title__startswith="P")      # LIKE 'P%'
Post.objects.filter(title__endswith="n")        # LIKE '%n'

# NULL
Post.objects.filter(category__isnull=True)   # IS NULL
Post.objects.filter(category__isnull=False)  # IS NOT NULL

# 日付
Post.objects.filter(created_at__date=date.today())
Post.objects.filter(created_at__year=2024)
Post.objects.filter(created_at__month=4)

Laravelのwhere("title", "like", "%Python%")filter(title__contains="Python")になる。最初は記法が独特だが、型ヒントなしでも補完が効くエディタが多い。


AND / OR / NOT

<?php
// Laravel
Post::where("is_published", true)
    ->where("author_id", 1)
    ->get();

Post::where("category_id", 1)
    ->orWhere("category_id", 2)
    ->get();
from django.db.models import Q

# AND(filter()を繋ぐかカンマ区切り)
Post.objects.filter(is_published=True, author_id=1)
Post.objects.filter(is_published=True).filter(author_id=1)

# OR(Qオブジェクト)
Post.objects.filter(Q(category_id=1) | Q(category_id=2))

# NOT
Post.objects.filter(~Q(is_published=True))
# または
Post.objects.exclude(is_published=True)

# 複合条件
Post.objects.filter(
    Q(title__contains="Python") | Q(title__contains="Django"),
    is_published=True,  # AND条件はカンマで追加
)

QオブジェクトがLaravelのorWhereに相当する。最初は「なんでQオブジェクト?」と思ったが、OR条件とAND条件を組み合わせた複雑なクエリが書きやすい設計だとわかった。


リレーションのクエリ

# ForeignKeyのリバースリレーション
author = Author.objects.get(id=1)
posts  = author.posts.all()           # related_name="posts"で定義

# 関連モデルをまたいだフィルタ(JOINが自動で発行)
posts = Post.objects.filter(author__name="田中")
posts = Post.objects.filter(author__email__contains="example.com")

# ManyToManyのフィルタ
posts = Post.objects.filter(tags__name="Python")

# 逆方向のフィルタ
authors = Author.objects.filter(posts__created_at__year=2024)

__でリレーションを跨ぐのがDjango ORMの独特な書き方。LaravelのwhereHas()に相当する操作がフィルタ記法で書ける。

<?php
// Laravel
$posts = Post::whereHas("author", fn($q) => $q->where("name", "田中"))->get();
# Django
posts = Post.objects.filter(author__name="田中")

Djangoのほうが短く書ける。


N+1問題と対処

N+1問題はDjango ORMでも発生する。

# 悪い例 — N+1が発生する
posts = Post.objects.all()
for post in posts:
    print(post.author.name)  # 各postごとにSELECTが発行される
# select_related — INNER JOIN(ForeignKey/OneToOne用)
posts = Post.objects.select_related("author", "category").all()
for post in posts:
    print(post.author.name)  # 追加のSELECTは発生しない

# prefetch_related — 別途SELECTしてPythonでマージ(ManyToMany/逆ForeignKey用)
posts = Post.objects.prefetch_related("tags").all()
for post in posts:
    print([tag.name for tag in post.tags.all()])

PHPのEloquentとの比較:

<?php
// Laravel
Post::with(["author", "category"])->get();  // eager loading
Post::with("tags")->get();
# Django
Post.objects.select_related("author", "category")  # JOIN
Post.objects.prefetch_related("tags")               # 別クエリ

LaravelはEager LoadingをすべてEager Loadingで統一しているが、DjangoはJOINするか別クエリかを使い分ける。一般的に:

  • ForeignKey・OneToOneselect_related()(JOIN)
  • ManyToMany・逆ForeignKeyprefetch_related()(別クエリ)

集計クエリ

from django.db.models import Count, Sum, Avg, Max, Min, F

# 全体集計(Laravelのaggregate()に相当)
total  = Post.objects.count()
avg_id = Post.objects.aggregate(avg=Avg("id"))["avg"]

# GROUP BY(annotateを使う)
from django.db.models import Count

authors = Author.objects.annotate(
    post_count=Count("posts")
).order_by("-post_count")

for author in authors:
    print(f"{author.name}: {author.post_count}")
<?php
// Laravel
Author::withCount("posts")->orderByDesc("posts_count")->get();

LaravelのwithCount()に相当するのがDjangoのannotate(Count(...))

# HAVINGに相当(annotate後にfilter)
prolific_authors = Author.objects.annotate(
    post_count=Count("posts")
).filter(post_count__gte=5)

# 複数の集計を同時に
from django.db.models import Count, Avg

stats = Post.objects.aggregate(
    total=Count("id"),
    avg_length=Avg("body__length"),
)

Fオブジェクト — フィールド同士の比較

from django.db.models import F

# フィールド同士の比較(SQLでカラム同士を比較する)
posts = Post.objects.filter(updated_at__gt=F("created_at"))

# フィールドの値を使った更新(Laravelのincrement/decrementに相当)
Post.objects.filter(id=1).update(view_count=F("view_count") + 1)

# race conditionを避けられる(SELECT + UPDATEではなく単一UPDATE)

Laravelのincrement()に相当するのがupdate(field=F("field") + 1)。一度取得して加算してsaveするのと違い、SQLで直接加算するので競合が起きない。


サブクエリ

from django.db.models import OuterRef, Subquery

# 各著者の最新記事タイトルを取得
latest_post = Post.objects.filter(
    author=OuterRef("pk")
).order_by("-created_at").values("title")[:1]

authors = Author.objects.annotate(
    latest_post_title=Subquery(latest_post)
)

for author in authors:
    print(f"{author.name}: {author.latest_post_title}")

LaravelのサブクエリよりDjangoのほうが書きやすいと感じた。OuterRefで外側のクエリを参照できる。


トランザクション

<?php
// Laravel
DB::transaction(function () {
    Post::create([...]);
    Author::where("id", 1)->increment("post_count");
});
from django.db import transaction

# デコレータ
@transaction.atomic
def create_post_with_count(data):
    post = Post.objects.create(**data)
    Author.objects.filter(id=data["author_id"]).update(
        post_count=F("post_count") + 1
    )
    return post

# コンテキストマネージャ
def create_post(data):
    with transaction.atomic():
        post = Post.objects.create(**data)
        Author.objects.filter(id=data["author_id"]).update(
            post_count=F("post_count") + 1
        )
    return post

@transaction.atomicデコレータとwith transaction.atomic():の両方が使える。withブロックの中で例外が発生するとロールバックされる。


発行されるSQLを確認する

# クエリセットにstrをかけるとSQLが見える
qs = Post.objects.select_related("author").filter(is_published=True)
print(str(qs.query))
# SELECT "posts"."id", "posts"."title", "authors"."name"
# FROM "posts"
# INNER JOIN "authors" ON ("posts"."author_id" = "authors"."id")
# WHERE "posts"."is_published" = True

# Django Debug Toolbarを使うとブラウザ上で確認できる
pip install django-debug-toolbar

発行されるSQLを確認できるのはN+1問題を発見するときに便利。LaravelのDB::listen()やdebugbarに相当する。


EloquentとDjango ORMの対応表

操作 Laravel Eloquent Django ORM
全件取得 Post::all() Post.objects.all()
ID検索 Post::find(1) Post.objects.get(id=1)
条件検索 Post::where(...) Post.objects.filter(...)
OR条件 ->orWhere(...) Q(...)
NOT条件 ->whereNot(...) ~Q(...) / .exclude()
Eager Loading Post::with("author") select_related() / prefetch_related()
件数 ->count() .count()
GROUP BY ->groupBy() + withCount() .annotate()
HAVING ->having() .annotate().filter()
サブクエリ ->whereIn(subquery) Subquery() + OuterRef()
increment ->increment("count") .update(count=F("count")+1)
トランザクション DB::transaction() transaction.atomic()

まとめ

  • __ルックアップ記法でJOINもフィルタも書ける
  • N+1はForeignKey系はselect_related()、ManyToMany系はprefetch_related()で解決
  • QオブジェクトでOR/NOT条件を表現する
  • annotate()でGROUP BY、F()でフィールド参照・更新
  • transaction.atomic()でトランザクション管理

Eloquent慣れだと最初は__記法やQオブジェクトに戸惑うが、慣れるとJOINを意識せずにリレーションをまたいだクエリが書けるのは便利。FとQの使い方を覚えてからは複雑なクエリも書けるようになってきた。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?