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

2018-12-27 / PythonDjango

概要

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="category">
            {% 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

すると、管理画面でも記事数がパッと確認できますね。
管理画面でも記事数が表示された

よく使うfilter処理をマネージャーに定義する

例えば記事に未来の日付を設定することができて、通常は現在時間までの記事、一部の状況では未来も含めたすべての記事を取得する、といったケースがあります。こういった場合は、次のようなマネージャーを定義しておくと捗ります。

from django.db import models
from django.utils import timezone


class PostQuerySet(models.QuerySet):

    def published(self):
        return self.filter(created_at__lte=timezone.now())


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    objects = PostQuerySet.as_manager()

    def __str__(self):
        return self.title

この例はget_queryset()を上書きしていません。その代わり、published()という独自のメソッドを定義しています。処理内容を見ればわかるように、これは現在時間までの記事だけ取得します。これにより、Post.objects.all()で全ての記事が取得でき、Post.objects.published()で公開記事だけを取得できるのです。ビュー等にfilter()で絞り込みをする必要はなくなり、直感的になりました。

この記事の関連記事

Djangoで、集計処理

2018-11-20 / PythonDjango

- Djangoで、データの平均値を求めたり、何個のデータが紐づくか、といった集計処理のサンプルを紹介しています。

Djangoで、月別アーカイブ欄を作る

2019-08-16 / PythonDjango

- Djangoで、ブログや日記によくある月別アーカイブ欄を作成していきます。

コメント欄

記事にコメントする

まだコメントはありません。