Djangoメモ(28) : QuerySet API(count, annotate)で個数のカウント
A Complete Beginner's Guide to Djangoのチュートリアルを参考にQuerySet APIを使ってみる。
対話型シェルで確認
チュートリアルに沿って掲示板アプリを作成しているが、今回はQuerySet APIを使用してPost、Topicの個数、最後のPostに関する情報を表示してみる。
最初に、オブジェクトをわかりやすく表示するためにboards/models.py
の各モデルに__str__
を定義しておく。
Truncator
は文字列を切り詰めることができるクラス。
from django.db import models from django.utils.text import Truncator class Board(models.Model): # ... def __str__(self): return self.name class Topic(models.Model): # ... def __str__(self): return self.subject class Post(models.Model): # ... def __str__(self): truncated_message = Truncator(self.message) return truncated_message.chars(30)
以降、対話型シェルで確認を進めていくが、データが多い方がわかりやすいのでTopic、Postに適当にデータを追加しておく。
対話型シェルはpython manage.py shell
で起動できるが、import文などが不要なshell_plus
を使用して確認する。
shell_plus
の使い方については下記記事を参照。
board.topics.count()
で"Django" Boardに属するTopicの個数を取得している。
Post.objects.count()
でPostの個数を取得しているが、これは全体の個数で"Django" Boardに属しているPostの個数ではない。
"Django" Boardに属しているPostの個数を取得するにはPost.objects.filter(topic__board=board).count()
のようにする。
このようにアンダースコアを2つ繋げる(__
)ことでリレーションを辿ることができる。
最後のPostの情報を取得するにはorder_by('-created_at')
で降順(-
が降順の意味)にソートしてfirst()
で先頭のオブジェクトを取得する。
モデル、テンプレートの実装
データの取得方法がわかったので実装する。
boards/models.py
にget_posts_count(self)
とget_last_post(self)
を追加。
from django.db import models class Board(models.Model): name = models.CharField(max_length=30, unique=True) description = models.CharField(max_length=100) def __str__(self): return self.name def get_posts_count(self): return Post.objects.filter(topic__board=self).count() def get_last_post(self): return Post.objects.filter(topic__board=self).order_by('-created_at').first()
関数の処理は対話型シェルで確認したもので、self
にはBoardインスタンスが入る。
次にtemplates/home.html
テンプレートを編集する。
{% extends 'base.html' %} {% block breadcrumb %} <li class="breadcrumb-item active">Boards</li> {% endblock %} {% block content %} <table class="table"> <thead class="thead-dark"> <tr> <th>Board</th> <th>Posts</th> <th>Topics</th> <th>Last Post</th> </tr> </thead> <tbody> {% for board in boards %} <tr> <td> <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a> <small class="text-muted d-block">{{ board.description }}</small> </td> <td class="align-middle"> {{ board.get_posts_count }} </td> <td class="align-middle"> {{ board.topics.count }} </td> <td class="align-middle"> {% with post=board.get_last_post %} <small> <a href="{% url 'topic_posts' board.pk post.topic.pk %}"> By {{ post.created_by.username }} at {{ post.created_at }} </a> </small> {% endwith %} </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
{{ board.get_posts_count }}
のようにモデルで定義した関数を呼び出している。
withタグは複雑な表現の変数の値をキャッシュし、簡単な名前で参照できるようにするタグ(board.get_last_post
がpost
で参照できるようになる)。
これで図のようにPost、Topicの個数、最後のPostに関する情報が表示される。
ただし、上記の実装だとPostが何もない場合にテストがエラーになる。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). .......................................................EEE...................... ====================================================================== ... FAILED (errors=3) Destroying test database for alias 'default'...
エラーが出ないようにtemplates/home.html
を編集する。
{% with post=board.get_last_post %} {% if post %} <small> <a href="{% url 'topic_posts' board.pk post.topic.pk %}"> By {{ post.created_by.username }} at {{ post.created_at }} </a> </small> {% else %} <small class="text-muted"> <em>No posts yet.</em> </small> {% endif %} {% endwith %}
動作確認用にPostのないBoardを追加して問題ないことを確認する。
annotate()
次にTopic一覧ページでも同様に個数をカウントするがannotate()を使用してみる。
annotate()
は即席のカラムが追加されるイメージで、annotate(replies=Count('posts')
のようにするとtopic.replies
で個数を取得できる(-1
としているのはTopic作成時のPostを除外するため)。
Count()
部分にはAvg()
、Max()
といった集計関数を指定できる。
annotate()
の使い方がわかったのでTopic一覧ページのboards/views.py
を編集する。
from django.db.models import Count from django.shortcuts import get_object_or_404, render from .models import Board def board_topics(request, pk): board = get_object_or_404(Board, pk=pk) topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1) return render(request, 'topics.html', {'board': board, 'topics': topics})
次にtemplates/topics.html
テンプレートを編集する。
{% for topic in topics %} <tr> <td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td> <td>{{ topic.starter.username }}</td> <td>{{ topic.replies }}</td> <td>0</td> <td>{{ topic.last_updated }}</td> </tr> {% endfor %}
これで返信数が表示されるようになる。
まとめ
Truncator
は文字列を切り詰めることができるクラスtopic__board
のように__
でリレーションを辿れる- 降順ソートは
order_by('-created_at')
のように-
を付ける annotate(replies=Count('posts'))
でtopic.replies
で個数を取得できるannotate()
では集計関数を指定できる