使用Count进行注释不工作我的期望

时间:2013-03-29 07:00:24

标签: django

我正在通过构建一个简单的RPG来试验Django。它有装甲升级。有不同类别的护甲(例如头部和身体)。每个类别都有许多盔甲。例如,“Head”类别可能包含“Dragon Helm”,“Duck Helm”,“Needle Helm”等。

为了让用户看到一个类别中可用的装甲的任何,他们必须首先被授予对该类别中至少一件装甲的访问权限。此时,他们可以查看该类别中的所有护甲 - 包括他们尚未购买的护甲。

问题

我正在尝试有效地查询数据库中所有类别的装甲,同时记录用户被授予访问权限的装甲。我有点工作,但不完全。

相关代码

models.py

from django.contrib.auth.models import User
from django.db import models


class Armor(models.Model):
    armor_category = models.ForeignKey('ArmorCategory')
    name = models.CharField(max_length=100)
    profile = models.ManyToManyField('Profile', through='ProfileArmor')

    def __unicode__(self):
        return self.name


class ArmorCategory(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(blank=True)

    class Meta:
        verbose_name_plural = 'Armor categories'

    def __unicode__(self):
        return self.name


class Profile(models.Model):
    user = models.OneToOneField(User)
    dob = models.DateField('Date of Birth')

    def __unicode__(self):
        return self.user.get_full_name()


class ProfileArmor(models.Model):
    profile = models.ForeignKey(Profile)
    armor = models.ForeignKey(Armor)
    date_created = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ('-date_created',)

    def __unicode__(self):
        return '%s: %s' % (self.profile.user.get_full_name(), self.armor.name)

urls.py

from django.conf.urls import patterns, url
from core import views

urlpatterns = patterns('',
    url(r'^upgrades/(?P<armor_category_slug>[-\w]+)/$', views.Upgrades.as_view(), name='upgrades'),
)

views.py (此文件存在问题)

from django.db.models import Count, Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from .models import ArmorCategory


class Upgrades(TemplateView):
    template_name = 'core/upgrades.html'

    def get(self, request, *args, **kwargs):
        # Make sure the slug is valid.

        self.armor_category = get_object_or_404(ArmorCategory, slug=kwargs['armor_category_slug'])

        # Make sure the user has been granted access to at least one item in
        # this category, otherwise there is no point for the user to even be
        # here.

        if self.armor_category.armor_set.filter(
            profilearmor__profile=self.request.user.profile
        ).count() == 0:
            return HttpResponseRedirect('/')

        return super(Upgrades, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        # THIS IS WHERE THE PROBLEM IS.

        # Get all of the armor in this category, but also take note of which
        # armor this user has been granted access to.

        armor = self.armor_category.armor_set.filter(
            Q(profilearmor__profile=self.request.user.profile) |
            Q(profilearmor__profile=None)
        ).annotate(profile_armor_count=Count('profilearmor__id'))

        print armor.query

        for armor_item in armor:
            print '%s: %s' % (armor_item.name, armor_item.profile_armor_count)

        return {
            'armor_category': self.armor_category,
            'armor': armor,
        }

问题的更多细节

我创造了“头部”类别并给了它在这个问题的第一段中指出的三件盔甲。我按照上面列出的顺序创建了护甲。然后我创建了两个用户配置文件。

我给第一个用户配置文件访问了“Duck Helm”装甲。然后我使用第一个用户配置文件访问/升级/ head /并从for循环获得此输出:

Dragon Helm: 0
Duck Helm: 1
Needle Helm: 0

这是预期的产出。接下来,我给第二个用户配置文件访问了“Dragon Helm”装甲。当我使用第二个用户配置文件访问相同的URL时,我得到了这个输出:

Dragon Helm: 1
Needle Helm: 0

为什么不列出“鸭头盔”护甲?我决定再次使用第一个用户配置文件返回相同的URL,以确保它仍然有效。当我这样做时,我得到了这个输出:

Duck Helm: 1
Needle Helm: 0

现在“龙头盔”盔甲消失了。

有什么想法吗?

1 个答案:

答案 0 :(得分:25)

1。快速解释

当您在Django查询集中测试与None的多对多关系时,就像在此处一样:

Q(profilearmor__profile=None)

匹配多对多关系中 no 对应行的行。所以你的查询

self.armor_category.armor_set.filter(
    Q(profilearmor__profile=self.request.user.profile) |
    Q(profilearmor__profile=None))

匹配self.request.user有权访问的装甲项目,或者 no-one 可以访问的装甲项目。这就是为什么你对第二个用户的查询未能为Duck Helm返回一行的原因:因为某人(即第一个用户)有权访问它。

2。该怎么做

您要运行的查询在SQL中如下所示:

SELECT `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`,
       COUNT(`myapp_profilearmor`.`id`) AS `profile_armor_count`
FROM `myapp_armor`
LEFT OUTER JOIN `myapp_profilearmor`
             ON `myapp_armor`.`id` = `myapp_profilearmor`.`armor_id`
                AND `myapp_profilearmor`.`profile_id` = %s
WHERE `myapp_armor`.`armor_category_id` = %s
GROUP BY `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`

不幸的是,Django的对象关系映射系统似乎没有提供表达此查询的方法。但您始终可以绕过ORM并发出raw SQL query。像这样:

sql = '''
    SELECT `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`,
           COUNT(`myapp_profilearmor`.`id`) AS `profile_armor_count`
    FROM `myapp_armor`
    LEFT OUTER JOIN `myapp_profilearmor`
                 ON `myapp_armor`.`id` = `myapp_profilearmor`.`armor_id`
                    AND `myapp_profilearmor`.`profile_id` = %s
    WHERE `myapp_armor`.`armor_category_id` = %s
    GROUP BY `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`
'''
armor = Armor.objects.raw(sql, [self.request.user.profile.id, self.armor_category.id])
for armor_item in armor:
    print('{:14}{}'.format(armor_item.name, armor_item.profile_armor_count))

例如:

>>> helmets = ArmorCategory.objects.get(id=1)
>>> profile = Profile.objects.get(id=1)
>>> armor = Armor.objects.raw(sql, [profile.id, helmets.id])
>>> for armor_item in armor:
...     print('{:14}{}'.format(armor_item.name, armor_item.profile_armor_count))
... 
Dragon Helm   0
Duck Helm     1
Needle Helm   0

3。你怎么能为自己想出这个

这是令人讨厌的Django查询:

armor = self.armor_category.armor_set.filter(
    Q(profilearmor__profile=self.request.user.profile) |
    Q(profilearmor__profile=None)
).annotate(profile_armor_count=Count('profilearmor__id'))

当您不理解查询产生错误结果的原因时,总是值得查看实际的SQL,您可以通过获取查询集的query属性并将其转换为字符串来实现:

>>> from django.db.models import Q
>>> helmets = ArmorCategory.objects.get(name='Helmets')
>>> profile = Profile.objects.get(id=1)
>>> print(helmets.armor_set.filter(Q(profilearmor__profile=profile) |
...                                Q(profilearmor__profile=None)
... ).annotate(profile_armor_count=Count('profilearmor__id')).query)
SELECT `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`,
       COUNT(`myapp_profilearmor`.`id`) AS `profile_armor_count`
FROM `myapp_armor`
LEFT OUTER JOIN `myapp_profilearmor`
             ON (`myapp_armor`.`id` = `myapp_profilearmor`.`armor_id`) 
LEFT OUTER JOIN `myapp_profile`
             ON (`myapp_profilearmor`.`profile_id` = `myapp_profile`.`id`) 
WHERE (`myapp_armor`.`armor_category_id` = 1
       AND (`myapp_profilearmor`.`profile_id` = 1
            OR `myapp_profile`.`id` IS NULL))
GROUP BY `myapp_armor`.`id`, `myapp_armor`.`armor_category_id`, `myapp_armor`.`name`
ORDER BY NULL

这是什么意思?如果您了解SQL连接的所有内容,则可以跳到第5节。否则,请继续阅读。

4。 SQL连接简介

我相信您知道,join of tables in SQL由这些表中的行组合组成(取决于您在查询中指定的某些条件)。例如,如果你有两种护甲和六种护甲:

mysql> SELECT * FROM myapp_armorcategory;
+----+---------+---------+
| id | name    | slug    |
+----+---------+---------+
|  1 | Helmets | helmets |
|  2 | Suits   | suits   |
+----+---------+---------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM myapp_armor;
+----+-------------------+-------------+
| id | armor_category_id | name        |
+----+-------------------+-------------+
|  1 |                 1 | Dragon Helm |
|  2 |                 1 | Duck Helm   |
|  3 |                 1 | Needle Helm |
|  4 |                 2 | Spiky Suit  |
|  5 |                 2 | Flower Suit |
|  6 |                 2 | Battle Suit |
+----+-------------------+-------------+
6 rows in set (0.00 sec)

然后这两个表中的JOIN将包含第一个表中的2行和第二个表的6行的所有12个组合:

mysql> SELECT * FROM myapp_armorcategory JOIN myapp_armor;
+----+---------+---------+----+-------------------+-------------+
| id | name    | slug    | id | armor_category_id | name        |
+----+---------+---------+----+-------------------+-------------+
|  1 | Helmets | helmets |  1 |                 1 | Dragon Helm |
|  2 | Suits   | suits   |  1 |                 1 | Dragon Helm |
|  1 | Helmets | helmets |  2 |                 1 | Duck Helm   |
|  2 | Suits   | suits   |  2 |                 1 | Duck Helm   |
|  1 | Helmets | helmets |  3 |                 1 | Needle Helm |
|  2 | Suits   | suits   |  3 |                 1 | Needle Helm |
|  1 | Helmets | helmets |  4 |                 2 | Spiky Suit  |
|  2 | Suits   | suits   |  4 |                 2 | Spiky Suit  |
|  1 | Helmets | helmets |  5 |                 2 | Flower Suit |
|  2 | Suits   | suits   |  5 |                 2 | Flower Suit |
|  1 | Helmets | helmets |  6 |                 2 | Battle Suit |
|  2 | Suits   | suits   |  6 |                 2 | Battle Suit |
+----+---------+---------+----+-------------------+-------------+
12 rows in set (0.00 sec)

通常会为连接添加条件以限制返回的行,以便它们有意义。例如,当使用护甲表加入护甲类别表时,我们只对护甲属于护甲类别的组合感兴趣:

mysql> SELECT * FROM myapp_armorcategory JOIN myapp_armor
           ON myapp_armorcategory.id = myapp_armor.armor_category_id;
+----+---------+---------+----+-------------------+-------------+
| id | name    | slug    | id | armor_category_id | name        |
+----+---------+---------+----+-------------------+-------------+
|  1 | Helmets | helmets |  1 |                 1 | Dragon Helm |
|  1 | Helmets | helmets |  2 |                 1 | Duck Helm   |
|  1 | Helmets | helmets |  3 |                 1 | Needle Helm |
|  2 | Suits   | suits   |  4 |                 2 | Spiky Suit  |
|  2 | Suits   | suits   |  5 |                 2 | Flower Suit |
|  2 | Suits   | suits   |  6 |                 2 | Battle Suit |
+----+---------+---------+----+-------------------+-------------+
6 rows in set (0.08 sec)

这一切都很简单。但是,如果一个表中的记录在另一个表中没有匹配项,则会出现问题。让我们添加一个新的护甲类别,没有相应的护甲物品:

mysql> INSERT INTO myapp_armorcategory (name, slug) VALUES ('Arm Guards', 'armguards');
Query OK, 1 row affected (0.00 sec)

如果我们重新运行上面的查询(SELECT * FROM myapp_armorcategory JOIN myapp_armor ON myapp_armorcategory.id = myapp_armor.armor_category_id;),我们会得到相同的结果:新护甲类别在护甲表中没有匹配的记录,因此它不会出现在{{1}中}。如果我们想要查看结果中显示的所有护甲类别,无论它们是否在另一个表中都有匹配的行,我们都必须运行所谓的outer join:特别是JOIN:< / p>

LEFT OUTER JOIN

对于左侧表中右侧表中没有匹配行的每一行,左外部联接包含右侧每列mysql> SELECT * FROM myapp_armorcategory LEFT OUTER JOIN myapp_armor ON myapp_armorcategory.id = myapp_armor.armor_category_id; +----+------------+-----------+------+-------------------+-------------+ | id | name | slug | id | armor_category_id | name | +----+------------+-----------+------+-------------------+-------------+ | 1 | Helmets | helmets | 1 | 1 | Dragon Helm | | 1 | Helmets | helmets | 2 | 1 | Duck Helm | | 1 | Helmets | helmets | 3 | 1 | Needle Helm | | 2 | Suits | suits | 4 | 2 | Spiky Suit | | 2 | Suits | suits | 5 | 2 | Flower Suit | | 2 | Suits | suits | 6 | 2 | Battle Suit | | 3 | Arm Guards | armguards | NULL | NULL | NULL | +----+------------+-----------+------+-------------------+-------------+ 7 rows in set (0.00 sec) 的行。

5。解释查询出错的原因

让我们创建一个可以访问Duck Helm的个人资料:

NULL

然后让我们运行查询的简化版本,试图了解它在做什么:

mysql> INSERT INTO myapp_profile (name) VALUES ('user1');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO myapp_profilearmor (profile_id, armor_id) VALUES (1, 2);
Query OK, 1 row affected (0.00 sec)

因此,当您添加条件mysql> SELECT `myapp_armor`.*, `myapp_profilearmor`.*, T5.* FROM `myapp_armor` LEFT OUTER JOIN `myapp_profilearmor` ON (`myapp_armor`.`id` = `myapp_profilearmor`.`armor_id`) LEFT OUTER JOIN `myapp_profile` T5 ON (`myapp_profilearmor`.`profile_id` = T5.`id`) WHERE `myapp_armor`.`armor_category_id` = 1; +----+-------------------+-------------+------+------------+----------+------+-------+ | id | armor_category_id | name | id | profile_id | armor_id | id | name | +----+-------------------+-------------+------+------------+----------+------+-------+ | 1 | 1 | Dragon Helm | NULL | NULL | NULL | NULL | NULL | | 2 | 1 | Duck Helm | 1 | 1 | 1 | 1 | user1 | | 3 | 1 | Needle Helm | NULL | NULL | NULL | NULL | NULL | +----+-------------------+-------------+------+------------+----------+------+-------+ 3 rows in set (0.04 sec) 时,您将获得此联接中的所有行。

现在让我们创建一个可以访问Dragon Helm的第二个配置文件:

(myapp_profilearmor.profile_id = 1 OR T5.id IS NULL)

重新开始加入:

mysql> INSERT INTO myapp_profile (name) VALUES ('user2');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO myapp_profilearmor (profile_id, armor_id) VALUES (2, 1);
Query OK, 1 row affected, 1 warning (0.09 sec)

现在您可以看到,当您运行第二个用户配置文件的查询时,连接上的额外条件将为mysql> SELECT `myapp_armor`.*, `myapp_profilearmor`.*, T5.* FROM `myapp_armor` LEFT OUTER JOIN `myapp_profilearmor` ON (`myapp_armor`.`id` = `myapp_profilearmor`.`armor_id`) LEFT OUTER JOIN `myapp_profile` T5 ON (`myapp_profilearmor`.`profile_id` = T5.`id`) WHERE `myapp_armor`.`armor_category_id` = 1; +----+-------------------+-------------+------+------------+----------+------+-------+ | id | armor_category_id | name | id | profile_id | armor_id | id | name | +----+-------------------+-------------+------+------------+----------+------+-------+ | 1 | 1 | Dragon Helm | 2 | 2 | 1 | 2 | user2 | | 2 | 1 | Duck Helm | 1 | 1 | 2 | 1 | user1 | | 3 | 1 | Needle Helm | NULL | NULL | NULL | NULL | NULL | +----+-------------------+-------------+------+------------+----------+------+-------+ 3 rows in set (0.00 sec) ,这将仅选择第1行和第3行。第2行将丢失。

因此,您可以看到原始过滤器(myapp_profilearmor.profile_id = 2 OR T5.id IS NULL)成为子条款Q(profilearmor__profile=None),这只会选择{{>> 条目(对于任何配置文件)的行{{1} 1}}关系。

6。对您的代码的其他评论

  1. 您使用查询集上的count()方法测试个人资料是否可以访问某个类别中的任何类型的护甲:

    T5.id IS NULL

    但由于您实际上并不关心配置文件可以访问的类别中的数量类型的盔甲,只要是否有任何,您应该使用而是exists()方法。

  2. ProfileArmor模型上设置if self.armor_category.armor_set.filter( profilearmor__profile=self.request.user.profile ).count() == 0: 时遇到了麻烦,为什么不使用它?也就是说,而不是像这样查询ManyToManyField模型:

    Armor

    您可以像这样运行相同的查询并保存一些输入:

    Armor
  3. 7。后记

    这是一个很好的问题。很多时候,Django的问题没有提供足够的模型细节,任何人都能够自信地回答。