ORM

Django ORMのカスタムマネージャー入門|スコープを定義する方法

Django ORM

Django ORMのカスタムマネージャー入門
スコープを定義する方法

Django ORMのカスタムマネージャーとカスタムQuerySetを使って、よく使うクエリ条件をメソッドとして定義する方法を解説します。

こんな人向けの記事です

  • よく使うクエリ条件を再利用可能にしたい人
  • カスタムマネージャーの作り方を知りたい人
  • QuerySetメソッドのチェーンを実現したい人

Step 1カスタムマネージャーとは

Djangoのマネージャー(Manager)はモデルに対するデータベースクエリのインターフェースです。objectsがデフォルトのマネージャーで、all()filter()を提供しています。カスタムマネージャーを作ると、よく使うクエリ条件をメソッドとして定義できます。

Python
# カスタムマネージャーなしの場合(毎回同じfilterを書く)
# views.py のあちこちで...
active_products = Product.objects.filter(is_active=True)
active_products = Product.objects.filter(is_active=True)  # 重複!

# カスタムマネージャーありの場合(1行で呼べる)
active_products = Product.objects.active()  # スッキリ!

Step 2基本的なカスタムマネージャー

models.Managerを継承してカスタムマネージャーを作成します。

Python
# models.py
from django.db import models
from django.utils import timezone

class ProductManager(models.Manager):
    """Productモデル用のカスタムマネージャー"""

    def active(self):
        """アクティブな商品のみ取得"""
        return self.filter(is_active=True)

    def expensive(self, threshold=10000):
        """指定価格以上の商品を取得"""
        return self.filter(price__gte=threshold)

    def recently_added(self, days=7):
        """直近n日以内に追加された商品を取得"""
        since = timezone.now() - timezone.timedelta(days=days)
        return self.filter(created_at__gte=since)

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    objects = ProductManager()  # カスタムマネージャーを設定

    def __str__(self):
        return self.name
Python
# 使い方
active = Product.objects.active()
print(f"アクティブ: {active.count()}件")

expensive = Product.objects.expensive(5000)
print(f"5000円以上: {expensive.count()}件")

recent = Product.objects.recently_added(30)
print(f"直近30日: {recent.count()}件")
実行結果
アクティブ: 15件
5000円以上: 8件
直近30日: 3件

Step 3カスタムQuerySetの定義

カスタムマネージャーのメソッドはチェーンできません。チェーン可能にするにはカスタムQuerySetを定義します。

Python
# models.py
from django.db import models

class ProductQuerySet(models.QuerySet):
    """チェーン可能なカスタムQuerySet"""

    def active(self):
        return self.filter(is_active=True)

    def expensive(self, threshold=10000):
        return self.filter(price__gte=threshold)

    def in_stock(self):
        return self.filter(stock__gt=0)

    def by_category(self, category_name):
        return self.filter(category__name=category_name)

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    stock = models.IntegerField(default=0)
    is_active = models.BooleanField(default=True)
    category = models.ForeignKey("Category", on_delete=models.CASCADE)

    # QuerySetをマネージャーとして使う
    objects = ProductQuerySet.as_manager()

    def __str__(self):
        return self.name
Python
# チェーンが可能!
products = Product.objects.active().expensive(5000).in_stock()
print(f"アクティブで5000円以上かつ在庫あり: {products.count()}件")

# filter()やorder_by()とも組み合わせ可能
products = Product.objects.active().by_category(
    "電子機器"
).order_by("-price")[:5]
as_manager()の仕組み
QuerySet.as_manager()は、カスタムQuerySetのメソッドをマネージャーから直接呼べるようにします。これによりProduct.objects.active()のようにマネージャーからもチェーンからも同じメソッドが使えます。

Step 4QuerySetのチェーン

カスタムマネージャーとカスタムQuerySetを両方使いたい場合の書き方です。

Python
# models.py
class ProductQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)

    def in_stock(self):
        return self.filter(stock__gt=0)

class ProductManager(models.Manager):
    def get_queryset(self):
        return ProductQuerySet(self.model, using=self._db)

    def active(self):
        return self.get_queryset().active()

    def in_stock(self):
        return self.get_queryset().in_stock()

    def sales_report(self):
        """マネージャー専用の集計メソッド"""
        from django.db.models import Sum, Avg
        return self.get_queryset().active().aggregate(
            total_price=Sum("price"),
            avg_price=Avg("price"),
        )

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    stock = models.IntegerField(default=0)
    is_active = models.BooleanField(default=True)

    objects = ProductManager()

    def __str__(self):
        return self.name
Python
# マネージャーからもQuerySetからもメソッドが使える
products = Product.objects.active().in_stock()

# マネージャー専用メソッド
report = Product.objects.sales_report()
print(f"合計: {report['total_price']}円, 平均: {report['avg_price']}円")

Step 5実践的な活用例

Python
# models.py - 実務でよく使うパターン
class ArticleQuerySet(models.QuerySet):
    def published(self):
        """公開済みの記事"""
        return self.filter(
            status="published",
            published_at__lte=timezone.now()
        )

    def draft(self):
        """下書きの記事"""
        return self.filter(status="draft")

    def by_author(self, user):
        """特定ユーザーの記事"""
        return self.filter(author=user)

    def popular(self, min_views=100):
        """人気記事"""
        return self.filter(view_count__gte=min_views)

    def search(self, keyword):
        """タイトルまたは本文でキーワード検索"""
        from django.db.models import Q
        return self.filter(
            Q(title__icontains=keyword) |
            Q(content__icontains=keyword)
        )

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    status = models.CharField(max_length=20, default="draft")
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    view_count = models.IntegerField(default=0)
    published_at = models.DateTimeField(null=True, blank=True)

    objects = ArticleQuerySet.as_manager()

# views.pyでの使用例
def article_list(request):
    articles = Article.objects.published().popular().order_by("-published_at")

    keyword = request.GET.get("q")
    if keyword:
        articles = articles.search(keyword)

    return render(request, "article/list.html", {"articles": articles})

まとめ

  • カスタムマネージャーでよく使うクエリ条件をメソッドとして定義できる
  • カスタムQuerySetを使えばメソッドチェーンが可能になる
  • QuerySet.as_manager()で簡単にマネージャーとして登録できる
  • ビューのコードがシンプルになり、クエリ条件の再利用性が向上する
  • ビジネスロジックに合った命名で可読性も上がる