web-dev-qa-db-ja.com

django RESTフレームワーク-prefetch_relatedを回避するための逆シリアル化

ItemItemGroupの2つのモデルがあります。

class ItemGroup(models.Model):
   group_name = models.CharField(max_length=50)
   # fields..

class Item(models.Model):
   item_name = models.CharField(max_length=50)
   item_group = models.ForeignKey(ItemGroup, on_delete=models.CASCADE)
   # other fields..

ネストされた配列としてアイテムリストを含むすべてのアイテムグループをフェッチするシリアライザを記述したいと思います。

だから私はこの出力が欲しい:

[ {group_name: "item group name", "items": [... list of items ..] }, ... ]

私が見るように、私はこれをDjango RESTフレームワークで書く必要があります:

class ItemGroupSerializer(serializers.ModelSerializer):
   class Meta:
      model = ItemGroup
      fields = ('item_set', 'group_name') 

つまり、ItemGroupItemではなく)のシリアライザを作成する必要があります。多くのクエリを回避するために、このクエリセットを渡します。

ItemGroup.objects.filter(**filters).prefetch_related('item_set')

私が見る問題は、大規模なデータセットの場合、prefetch_relatedを実行すると、非常に大きなsql IN句を含む追加のクエリが発生します。これは、代わりにItemオブジェクトのクエリで回避できます。

Item.objects.filter(**filters).select_related('item_group')

どちらがより良いJOINになります。

Itemの代わりにItemGroupをクエリし、それでも同じシリアル化出力を取得することは可能ですか?

14
user3599803

基本から始めましょう

シリアライザは、与えられたデータのみを処理できます

つまり、ネストされた表現でItemGroupおよびItemオブジェクトのリストをシリアル化できるシリアライザを取得するには、最初にそのリストを指定する必要があります。これまでに、prefetch_relatedを呼び出して関連するItemGroupオブジェクトを取得するItemモデルのクエリを使用して、これを達成しました。また、prefetch_relatedが2番目のクエリをトリガーしてそれらの関連オブジェクトを取得することも確認しましたが、これでは不十分です。

prefetch_relatedは、複数の関連オブジェクトを取得するために使用されます

これはどういう意味ですか?単一のItemGroupのような単一のオブジェクトをクエリする場合は、prefetch_relatedを使用して、逆方向外部キー(1対多)または定義された多対多の関係など、複数の関連オブジェクトを含む関係を取得します。 Djangoいくつかの理由により、意図的に2番目のクエリを使用してこれらのオブジェクトを取得します

  1. select_relatedで必要とされる結合は、2番目のテーブルに対して結合を強制する場合、多くの場合パフォーマンスが低下します。これは、ItemGroupを含まないItemオブジェクトが失われないようにするために、右外部結合が必要になるためです。
  2. prefetch_relatedが使用するクエリは、インデックス付きの主キーフィールドINであり、これは、最もパフォーマンスの高いクエリの1つです。
  3. クエリは、存在することがわかっているItemオブジェクトのIDのみを要求するため、追加のサブクエリを実行しなくても、重複(多対多の関係の場合)を効率的に処理できます。

これはすべて言い方です。prefetch_relatedは、本来あるべきことを正確に行っており、理由のためにそうしています。

しかし、とにかくselect_relatedでこれをやりたい

わかった、わかった。それが求められていることなので、何ができるか見てみましょう。

これを実現する方法はいくつかありますが、そのすべてに長所と短所があり、手動で「ステッチ」を行わないと最終的には機能しません。私は、組み込みのViewSetまたはDRFによって提供される汎用ビューを使用していないことを前提としていますが、その場合は、組み込みのフィルタリングを機能させるために、filter_querysetメソッドでステッチを行う必要があります。ああ、それはおそらくページネーションを壊すか、ほとんど役に立たないものにします。

元のフィルターを保持する

元のフィルターセットがItemGroupオブジェクトに適用されています。そして、これはAPIで使用されているため、おそらく動的であり、それらを失いたくないでしょう。したがって、次の2つの方法のいずれかでフィルターを適用する必要があります。

  1. フィルターを生成し、関連する名前をプレフィックスとして付けます。

    したがって、通常のfoo=barフィルターを生成し、それらをfilter()に渡す前にプレフィックスを付けて、related__foo=barにします。これで、関係全体をフィルタリングしているため、パフォーマンスに影響を与える可能性があります。

  2. 元のサブクエリを生成し、それをItemクエリに直接渡します

    prefetch_relatedと同等のパフォーマンスを持つINクエリを生成する場合を除いて、これはおそらく「最もクリーンな」ソリューションです。代わりに、これはキャッシュできないサブクエリとして扱われるため、パフォーマンスが低下します。

シリアライザが機能するようにItemオブジェクトとItemGroupオブジェクトを「フリップアンドスティッチ」できるようにしたいので、これらの両方を実装することは現実的にこの質問の範囲外です。

Itemクエリを反転して、ItemGroupオブジェクトのリストを取得する

select_relatedItemGroupオブジェクトと一緒にすべてのItemオブジェクトを取得するために使用されている元の質問で与えられたクエリを取ると、Itemオブジェクトでいっぱいのクエリセットが返されます。 ItemGroupを使用しているので、実際にはItemGroupSerializerオブジェクトのリストが必要なので、「反転」する必要があります。

from collections import defaultdict

items = Item.objects.filter(**filters).select_related('item_group')

item_groups_to_items = defaultdict(list)
item_groups_by_id = {}

for item in items:
    item_group = item.item_group

    item_groups_by_id[item_group.id] = item_group
    item_group_to_items[item_group.id].append(item)

ほとんどのDjangoモデルは不変ではなく、時々人々はハッシュ法を主キー以外のものにオーバーライドするので、私は辞書のキーとしてidItemGroupを意図的に使用しています。

これにより、ItemGroupオブジェクトから関連するItemオブジェクトへのマッピングが得られます。これは、最終的にそれらを再度「ステッチ」するために必要なものです。

ItemGroupオブジェクトをそれらの関連するItemオブジェクトとステッチする

この部分は、関連するすべてのオブジェクトが既にあるので、実際に行うことは難しくありません。

for item_group_id, item_group_items in item_group_to_items.items():
    item_group = item_groups_by_id[item_group_id]

    item_group.item_set = item_group_items

item_groups = item_groups_by_id.values()

これにより、要求されたすべてのItemGroupオブジェクトが取得され、item_groups変数にlistとして格納されます。各ItemGroupオブジェクトには、item_set属性で設定された関連するItemオブジェクトのリストがあります。自動的に生成された同じ名前の逆外部キーと競合しないように、名前を変更したい場合があります。

ここから、ItemGroupSerializerで通常使用するように使用でき、シリアル化で機能するはずです。

ボーナス:「フリップとスティッチ」の一般的な方法

他の同様のシナリオで使用するために、このジェネリック(および判読不能)をかなりすばやく作成できます。

def flip_and_stitch(itmes, group_from_item, store_in):
    from collections import defaultdict

    item_groups_to_items = defaultdict(list)
    item_groups_by_id = {}

    for item in items:
        item_group = getattr(item, group_from_item)

        item_groups_by_id[item_group.id] = item_group
        item_group_to_items[item_group.id].append(item)

    for item_group_id, item_group_items in item_group_to_items.items():
        item_group = item_groups_by_id[item_group_id]

        setattr(item_group, store_in, item_group_items)

    return item_groups_by_id.values()

そして、あなたはこれを

item_groups = flip_and_stitch(items, 'item_group', 'item_set')

どこ:

  • itemsは、select_related呼び出しがすでに適用されている、最初に要求したアイテムのクエリセットです。
  • item_groupは、関連するItemが格納されているItemGroupオブジェクトの属性です。
  • item_setは、関連するItemGroupオブジェクトのリストが格納されるItemオブジェクトの属性です。
1
Kevin Brown

_prefetch_related_を使用すると、2つのクエリと大きなIN句の問題が発生しますが、実証済みで移植性があります。

私はあなたのフィールド名に基づいて、より多くの例である解決策を与えます。 _select_related_ Itemを使用してquerysetのシリアライザーから変換する関数を作成します。これは、ビューのリスト関数をオーバーライドし、1つのシリアライザーデータから必要な表現を提供する他のシリアライザーデータに変換します。 1つのクエリのみを使用し、結果の解析はO(n)で行われるため、高速である必要があります。

結果にフィールドを追加するには、_get_data_のリファクタリングが必要になる場合があります。

_class ItemSerializer(serializers.ModelSerializer):
    group_name = serializers.CharField(source='item_group.group_name')

    class Meta:
        model = Item
        fields = ('item_name', 'group_name')

class ItemGSerializer(serializers.Serializer):
    group_name = serializers.CharField(max_length=50)
    items = serializers.ListField(child=serializers.CharField(max_length=50))
_

ビューで:

_class ItemGroupViewSet(viewsets.ModelViewSet):
    model = models.Item
    serializer_class = serializers.ItemSerializer
    queryset = models.Item.objects.select_related('item_group').all()

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            data = self.get_data(serializer.data)
            s = serializers.ItemGSerializer(data, many=True)
            return self.get_paginated_response(s.data)

        serializer = self.get_serializer(queryset, many=True)
        data = self.get_data(serializer.data)
        s = serializers.ItemGSerializer(data, many=True)
        return Response(s.data)

    @staticmethod
    def get_data(data):
        result, current_group = [], None
        for elem in data:
            if current_group is None:
                current_group = {'group_name': elem['group_name'], 'items': [elem['item_name']]}
            else:
                if elem['group_name'] == current_group['group_name']:
                    current_group['items'].append(elem['item_name'])
                else:
                    result.append(current_group)
                    current_group = {'group_name': elem['group_name'], 'items': [elem['item_name']]}

        if current_group is not None:
            result.append(current_group)
        return result
_

これが私の偽のデータでの私の結果です:

_[{
    "group_name": "group #2",
    "items": [
        "first item",
        "2 item",
        "3 item"
    ]
},
{
    "group_name": "group #1",
    "items": [
        "g1 #1",
        "g1 #2",
        "g1 #3"
    ]
}]
_
7
edilio