5

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

为了让用户看到a 类别中可用的任何盔甲,他们必须首先被授予访问该类别中至少一件盔甲的权限。那时,他们可以查看该类别中的所有盔甲——包括他们还不能购买的盔甲。

问题

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

相关代码

模型.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)

网址.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”盔甲。然后我使用第一个用户配置文件访问 /upgrades/head/ 并从for循环中获得以下输出:

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

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

Dragon Helm: 1
Needle Helm: 0

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

Duck Helm: 1
Needle Helm: 0

现在“龙盔”盔甲不见了。

有任何想法吗?

4

1 回答 1

25

1.快速解释

None当您在 Django查询集中测试多对多关系时,如下所示:

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 并发出原始 SQL 查询。像这样:

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连接简介

我相信您知道,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(如果我们想看到结果中出现的所有盔甲类别,无论它们在另一个表中是否有匹配的行,我们都必须运行所谓的连接:特别是 a 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)您将从该联接中获取所有行。

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

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. 对您的代码的其他注释

  1. 您可以使用查询集上的方法测试配置文件是否可以访问某个类别中的任何类型的盔甲count()

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

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

  2. ManyToManyField费尽心思在模型上设置了一个Armor,为什么不使用它呢?也就是说,而不是Armor像这样查询模型:

    Q(profilearmor__profile = ...)
    

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

    Q(profile = ...)
    

7. 后记

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

于 2013-03-31T13:45:56.337 回答