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. 对您的代码的其他注释
您可以使用查询集上的方法测试配置文件是否可以访问某个类别中的任何类型的盔甲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 问题并没有提供足够的模型细节,让任何人都能够自信地回答。