5

簡単な RPG を作成して Django を試しています。鎧のアップグレードがあります。鎧にはさまざまなカテゴリがあります (頭と胴体など)。各カテゴリには多くの鎧があります。たとえば、「頭」カテゴリには、「ドラゴン ヘルム」、「ダック ヘルム」、「ニードル ヘルム」などがあります。

ユーザーがカテゴリで利用可能な防具を表示するには、まずそのカテゴリの防具の少なくとも 1 つへのアクセスを許可する必要があります。その時点で、まだ購入できないアーマーを含め、そのカテゴリのすべてのアーマーを表示できます。

問題

ユーザーがアクセスを許可されている鎧の部分を同時にメモしながら、カテゴリのすべての鎧についてデータベースに効率的にクエリを実行しようとしています。ある程度は機能していますが、完全ではありません。

関連コード

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

問題の詳細

私は「頭」カテゴリを作成し、この質問の最初の段落で示した 3 つの防具を付けました。上にリストしたのと同じ順序で鎧を作成しました。次に、2 つのユーザー プロファイルを作成しました。

最初のユーザー プロファイルに「Duck Helm」アーマーへのアクセスを許可しました。次に、最初のユーザー プロファイルで /upgrades/head/ にアクセスし、forループから次の出力を取得しました。

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

これは予想される出力でした。次に、2 番目のユーザー プロファイルに "Dragon Helm" アーマーへのアクセスを許可しました。2 番目のユーザー プロファイルで同じ 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は、がアクセスできるか、もアクセスできない鎧のアイテムに一致します。これが、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)

これらのJOIN2 つのテーブルの 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. コードに関するその他のコメント

  1. count()クエリセットのメソッドを使用して、プロファイルがカテゴリ内の任意のタイプのアーマーにアクセスできるかどうかをテストします。

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

    しかし、プロファイルがアクセスできるカテゴリ内の防具の種類のは実際には気にしないので、 があるexists()どうかだけで、代わりに メソッドを使用する必要があります。

  2. せっかくモデルに を設定したのに、使ってみませんかManyToManyFieldArmorつまり、次のArmorようにモデルをクエリする代わりに:

    Q(profilearmor__profile = ...)
    

    次のように同じクエリを実行して、入力を節約できます。

    Q(profile = ...)
    

7. あとがき

これは素晴らしい質問でした。多くの場合、Django の質問ではモデルの詳細が十分に提供されていないため、誰もが自信を持って答えることができません。

于 2013-03-31T13:45:56.337 に答える