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
は、がアクセスできるか、誰もアクセスできない鎧のアイテムに一致します。これが、2 番目のユーザーのクエリが 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'))
クエリが間違った結果を生成する理由がわからない場合は、クエリセットのquery
属性を取得して文字列に変換することで、実際の SQL を確認することをお勧めします。
>>> 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 でのテーブルの結合は、それらのテーブルの行の組み合わせで構成されます (クエリで指定した条件に従います)。たとえば、2 つのカテゴリの防具と 6 つの防具アイテムがある場合:
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 つのテーブルの 1 つには、最初のテーブルの 2 行と 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
. 他のテーブルに一致する行があるかどうかに関係なく、すべてのアーマー カテゴリが結果に表示されるようにするには、いわゆる外部結合を実行する必要があり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 にアクセスできる 2 番目のプロファイルを作成しましょう。
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)
2 番目のユーザー プロファイルのクエリを実行すると、結合に追加の条件が追加され、行 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 の質問ではモデルの詳細が十分に提供されていないため、誰もが自信を持って答えることができません。