gpt4 book ai didi

django - 用Count注释无法正常工作

转载 作者:行者123 更新时间:2023-12-04 16:20:22 25 4
gpt4 key购买 nike

我正在通过构建一个简单的RPG来尝试使用Django。它有装甲升级。装甲有不同类别(例如头部和 body )。每个类别都有许多装甲。例如,“头部”类别可能包含“龙盔”,“鸭盔”,“针盔”等。

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

问题

我试图有效地查询数据库中某个类别的所有装甲,同时注意用户已被授予访问权限的那些装甲。我有某种工作方式,但不是全部。

相关代码

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,
}

问题的更多详细信息

我创建了“头部”类别,并给了该问题第一段中所述的三套盔甲。我按照上面列出的顺序创建了盔甲。然后,我创建了两个用户配置文件。

我为第一个用户个人资料授予了“鸭盔”装甲的访问权限。然后,我使用第一个用户配置文件访问/upgrades/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.快速说明

当您在Django查询集中针对None测试多对多关系时,请按以下步骤操作:

Q(profilearmor__profile=None)

匹配多对多关系中没有对应行的行。所以你的查询
self.armor_category.armor_set.filter(
Q(profilearmor__profile=self.request.user.profile) |
Q(profilearmor__profile=None))

匹配 self.request.user可以访问或没有人可以访问的装甲项目。这就是为什么您对第二个用户的查询未能返回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,您可以通过获取queryset的 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;),我们将得到相同的结果:新的盔甲类别在盔甲表中没有匹配的记录,因此它不会出现在 JOIN中。如果我们想查看所有装甲类别都出现在结果中,而不管它们是否在另一个表中有匹配的行,则必须运行一个所谓的 outer join:特别是 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)

对于左侧表中没有匹配行的左侧表中的每一行,左外部联接包含右侧每一行带有 NULL的行。

5.解释查询为什么出错

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

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)

因此,当您添加条件 (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)

现在您可以看到,当您运行第二个用户配置文件的查询时,联接上的额外条件将是 (myapp_profilearmor.profile_id = 2 OR T5.id IS NULL),这将仅选择第1行和第3行。第2行将丢失。

因此,您可以看到原来的过滤器 Q(profilearmor__profile=None)成为了子句 T5.id IS NULL,这仅选择 ProfileArmor关系中没有条目(对于任何配置文件)的行。

6.关于您的代码的其他注释
  • 您可以使用queryset上的 count() 方法测试配置文件是否可以访问类别中的任何类型的装甲:
    if self.armor_category.armor_set.filter(
    profilearmor__profile=self.request.user.profile
    ).count() == 0:

    但是由于您实际上并不关心配置文件可以访问的类别中的装甲类型数量,因此,无论是否存在,您都应该使用 exists() 方法。
  • 麻烦在ManyToManyField模型上设置Armor,为什么不使用它呢?也就是说,不是像这样查询Armor模型:
    Q(profilearmor__profile = ...)

    您可以像这样运行相同的查询并保存一些输入内容:
    Q(profile = ...)

  • 7.后记

    这是一个很好的问题。 Django问题经常无法提供足够详细的模型细节,任何人都可以自信地回答。

    关于django - 用Count注释无法正常工作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15698866/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com