NARITO BLOG

Djangoで、カスタムマネージャーを使う

Python, Django,

概要

Djangoでは、モデル名.objectsのようにすると様々なことができます。このobjectsはマネージャーと呼ばれるものです。

このマネージャーは自分でカスタマイズすることもできます。

初期クエリを変更したくなる例

例えばカテゴリと記事のモデルがあり、カテゴリの一覧を表示する際に紐づいた記事数も一緒に表示したいとします。

これはDjangoで、集計処理でも紹介したannotate()で実装することができます。

次のようなモデルの例です。

from django.db import models


class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

    def __str__(self):
        return self.title

例えば、カテゴリの一覧表示をするビューは次のようになるでしょう。

from django.db.models import Count
from django.views import generic
from .models import *


class CaegorytList(generic.ListView):
    model = Category

    def get_queryset(self):
        queryset = super().get_queryset()
        # カテゴリを、紐づいた記事数と一緒に取得し、その記事数順に並び替え
        return queryset.annotate(post_count=Count('post')).order_by('-post_count')

そして、category_list.htmlです。

{% extends 'app/base.html' %}

{% block content %}
    <!-- Bulmaで書いています-->
    <div class="section">
        <div class="container">
            {% for category in category_list %}
                <p>{{ category }}({{ category.post_count }})</p>
            {% endfor %}
        </div>
    </div>
{% endblock %}

これは問題なく動きます。

ちゃんと記事数も一緒に表示される

今のところ、ビューのget_queryset()を上書きするだけで済みます。

しかしこの表示は便利なので、admin管理画面でもやりたくなるかもしれません。また、記事検索フォームなんかを作っていて、そのカテゴリ欄でも同様に表示したくなるかもしれません。

こうなってくると、カテゴリを表示しそうな箇所全てを↑のようなコードにする必要があります。

マネージャーのカスタマイズ

そこで、マネージャーのカスタマイズです。models.pyを編集します。

# 増えた
class CategoryManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().annotate(
            post_count=models.Count('post')
        ).order_by('-post_count')


class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)
    objects = CategoryManager()  # 足した

    def __str__(self):
        return self.name

Categoryのobjects属性を、CategoryManagerというものに変更しました。`

モデルの標準ではobjects = models.Manager()のようになっていて、Category.objects.all()のような処理をすると、マネージャークラスのget_queryset()が呼ばれます。今回のコードはそれを上書きしている形ですね。

初期クエリの段階で記事数も一緒に返すようになったので、__str__()での表示名も一緒に変更しておくと捗るでしょう。

class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)
    objects = CategoryManager()

    def __str__(self):
        if hasattr(self, 'post_count'):
            return f'{self.name}({self.post_count})'
        else:
            return self.name

すると、管理画面でも記事数がパッと確認できますね。テンプレートでも、{{ category }}だけでよくなります。

管理画面でも記事数が表示された

デフォルトマネージャーも使えるようにする

上の方法は、Category.objects.all()は全てannotate()されたデータが取得できます。それが不要なケースもあるでしょう。

デフォルトのマネージャーも使いつつ、場合によってannotate()での集計済みデータも使えるようにしたいと思います。

class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)
    objects = models.Manager()
    counted = CategoryManager()  # 足した

Category.counted.all()で集計済みデータが、Category.objects...で通常の処理をすることができます。これ以外にも、好きなだけマネージャーを追加できます。

このマネージャーを上手く使うことで、ビュー等にある似たようなfilter()order_by()を無くすことができます。

Post.public.all()Post.private.all()Post.recent.all()のようにして、よく使いそうなクエリセットの一覧を定義しておくと捗ります。