Django入门与实践-第20章:QuerySets(查询结果集)

现在我们花点时间来探索关于模型的 API。首先,我们来改进主页:

5-13.png

有3个任务:

  • 显示每个板块的总主题数
  • 显示每个板块的总回复数
  • 显示每个板块的最后发布者和日期

在实现这些功能前,我们先使用Python终端

因为我们要在Python终端尝试,所以,把所有的 models 定义一个 __str__ 方法是个好主意

boards/models.py(完整代码)

  1. from django.db import models
  2. from django.utils.text import Truncator
  3. class Board(models.Model):
  4. # ...
  5. def __str__(self):
  6. return self.name
  7. class Topic(models.Model):
  8. # ...
  9. def __str__(self):
  10. return self.subject
  11. class Post(models.Model):
  12. # ...
  13. def __str__(self):
  14. truncated_message = Truncator(self.message)
  15. return truncated_message.chars(30)

在 Post 模型中,使用了 Truncator 工具类,这是将一个长字符串截取为任意长度字符的简便方法(这里我们使用30个字符)

现在打开 Python shell

  1. python manage.py shell
  2. from boards.models import Board
  3. # First get a board instance from the database
  4. board = Board.objects.get(name='Django')

这三个任务中最简单的一个就是获取当前版块的总主题数,因为 Topic 和 Baoard 是直接关联的。

  1. board.topics.all()
  2. <QuerySet [<Topic: Hello everyone!>, <Topic: Test>, <Topic: Testing a new post>, <Topic: Hi>]>
  3. board.topics.count()
  4. 4

就这样子。

现在统计一个版块下面的回复数量有点麻烦,因为回复并没有和 Board 直接关联

  1. from boards.models import Post
  2. Post.objects.all()
  3. <QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: Hi everyone!>,
  4. <Post: New test here!>, <Post: Testing the new reply feature!>, <Post: Lorem ipsum dolor sit amet,...>,
  5. <Post: hi there>, <Post: test>, <Post: Testing..>, <Post: some reply>, <Post: Random random.>
  6. ]>
  7. Post.objects.count()
  8. 11

这里一共11个回复,但是它并不全部属于 “Django” 这个版块的。

我们可以这样来过滤

  1. from boards.models import Board, Post
  2. board = Board.objects.get(name='Django')
  3. Post.objects.filter(topic__board=board)
  4. <QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: hi there>,
  5. <Post: Hi everyone!>, <Post: Lorem ipsum dolor sit amet,...>, <Post: New test here!>,
  6. <Post: Testing the new reply feature!>
  7. ]>
  8. Post.objects.filter(topic__board=board).count()
  9. 7

双下划线的topic__board用于通过模型关系来定位,在内部,Django 在 Board-Topic-Post之间构建了桥梁,构建SQL查询来获取属于指定版块下面的帖子回复。

最后一个任务是标识版块下面的最后一条回复

  1. # order by the `created_at` field, getting the most recent first
  2. Post.objects.filter(topic__board=board).order_by('-created_at')
  3. <QuerySet [<Post: testing>, <Post: new post>, <Post: hi there>, <Post: Lorem ipsum dolor sit amet,...>,
  4. <Post: Testing the new reply feature!>, <Post: New test here!>, <Post: Hi everyone!>,
  5. <Post: test.>, <Post: This is my first topic.. :-)>
  6. ]>
  7. # we can use the `first()` method to just grab the result that interest us
  8. Post.objects.filter(topic__board=board).order_by('-created_at').first()
  9. <Post: testing>

太棒了,现在我们来实现它

boards/models.py (完整代码)

  1. from django.db import models
  2. class Board(models.Model):
  3. name = models.CharField(max_length=30, unique=True)
  4. description = models.CharField(max_length=100)
  5. def __str__(self):
  6. return self.name
  7. def get_posts_count(self):
  8. return Post.objects.filter(topic__board=self).count()
  9. def get_last_post(self):
  10. return Post.objects.filter(topic__board=self).order_by('-created_at').first()

注意,我们使用的是self,因为这是Board的一个实例方法,所以我们就用这个Board实例来过滤这个 QuerySet

现在我们可以改进主页的HTML模板来显示这些新的信息

templates/home.html

  1. {% extends 'base.html' %}
  2. {% block breadcrumb %}
  3. <li class="breadcrumb-item active">Boards</li>
  4. {% endblock %}
  5. {% block content %}
  6. <table class="table">
  7. <thead class="thead-inverse">
  8. <tr>
  9. <th>Board</th>
  10. <th>Posts</th>
  11. <th>Topics</th>
  12. <th>Last Post</th>
  13. </tr>
  14. </thead>
  15. <tbody>
  16. {% for board in boards %}
  17. <tr>
  18. <td>
  19. <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
  20. <small class="text-muted d-block">{{ board.description }}</small>
  21. </td>
  22. <td class="align-middle">
  23. {{ board.get_posts_count }}
  24. </td>
  25. <td class="align-middle">
  26. {{ board.topics.count }}
  27. </td>
  28. <td class="align-middle">
  29. {% with post=board.get_last_post %}
  30. <small>
  31. <a href="{% url 'topic_posts' board.pk post.topic.pk %}">
  32. By {{ post.created_by.username }} at {{ post.created_at }}
  33. </a>
  34. </small>
  35. {% endwith %}
  36. </td>
  37. </tr>
  38. {% endfor %}
  39. </tbody>
  40. </table>
  41. {% endblock %}

现在是这样的效果

5-14.png

运行测试:

  1. python manage.py test
  1. Creating test database for alias 'default'...
  2. System check identified no issues (0 silenced).
  3. .......................................................EEE......................
  4. ======================================================================
  5. ERROR: test_home_url_resolves_home_view (boards.tests.test_view_home.HomeTests)
  6. ----------------------------------------------------------------------
  7. django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
  8. ======================================================================
  9. ERROR: test_home_view_contains_link_to_topics_page (boards.tests.test_view_home.HomeTests)
  10. ----------------------------------------------------------------------
  11. django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
  12. ======================================================================
  13. ERROR: test_home_view_status_code (boards.tests.test_view_home.HomeTests)
  14. ----------------------------------------------------------------------
  15. django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']
  16. ----------------------------------------------------------------------
  17. Ran 80 tests in 5.663s
  18. FAILED (errors=3)
  19. Destroying test database for alias 'default'...

看起来好像有问题,如果没有回复的时候程序会崩溃

templates/home.html

  1. {% with post=board.get_last_post %}
  2. {% if post %}
  3. <small>
  4. <a href="{% url 'topic_posts' board.pk post.topic.pk %}">
  5. By {{ post.created_by.username }} at {{ post.created_at }}
  6. </a>
  7. </small>
  8. {% else %}
  9. <small class="text-muted">
  10. <em>No posts yet.</em>
  11. </small>
  12. {% endif %}
  13. {% endwith %}

再次运行测试:

  1. python manage.py test
  1. Creating test database for alias 'default'...
  2. System check identified no issues (0 silenced).
  3. ................................................................................
  4. ----------------------------------------------------------------------
  5. Ran 80 tests in 5.630s
  6. OK
  7. Destroying test database for alias 'default'...

我添加一个没有任何消息的版块,用于检查这个”空消息”

5-15.png

现在是时候来改进回复列表页面了。

5-16.png

现在,我将告诉你另外一种方法来统计回复的数量,用一种更高效的方式

和之前一样,首先在Python shell 中尝试

  1. python manage.py shell
  1. from django.db.models import Count
  2. from boards.models import Board
  3. board = Board.objects.get(name='Django')
  4. topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts'))
  5. for topic in topics:
  6. print(topic.replies)
  7. 2
  8. 4
  9. 2
  10. 1

这里我们使用annotate ,QuerySet将即时生成一个新的列,这个新的列,将被翻译成一个属性,可通过 topic.replies来访问,它包含了指定主题下的回复数。

我们来做一个小小的修复,因为回复里面不应该包括发起者的帖子

  1. topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
  2. for topic in topics:
  3. print(topic.replies)
  4. 1
  5. 3
  6. 1
  7. 0

很酷,对不对?

boards/views.py (完整代码)

  1. from django.db.models import Count
  2. from django.shortcuts import get_object_or_404, render
  3. from .models import Board
  4. def board_topics(request, pk):
  5. board = get_object_or_404(Board, pk=pk)
  6. topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
  7. return render(request, 'topics.html', {'board': board, 'topics': topics})

templates/topics.html(完整代码)

  1. {% for topic in topics %}
  2. <tr>
  3. <td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td>
  4. <td>{{ topic.starter.username }}</td>
  5. <td>{{ topic.replies }}</td>
  6. <td>0</td>
  7. <td>{{ topic.last_updated }}</td>
  8. </tr>
  9. {% endfor %}

topics-5-15.png

下一步是修复主题的查看次数,但是,现在我们需要添加一个新的字段