Item
とItemGroup
の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')
つまり、ItemGroup
(Item
ではなく)のシリアライザを作成する必要があります。多くのクエリを回避するために、このクエリセットを渡します。
ItemGroup.objects.filter(**filters).prefetch_related('item_set')
私が見る問題は、大規模なデータセットの場合、prefetch_related
を実行すると、非常に大きなsql IN
句を含む追加のクエリが発生します。これは、代わりにItemオブジェクトのクエリで回避できます。
Item.objects.filter(**filters).select_related('item_group')
どちらがより良いJOINになります。
Item
の代わりにItemGroup
をクエリし、それでも同じシリアル化出力を取得することは可能ですか?
基本から始めましょう
つまり、ネストされた表現でItemGroup
およびItem
オブジェクトのリストをシリアル化できるシリアライザを取得するには、最初にそのリストを指定する必要があります。これまでに、prefetch_related
を呼び出して関連するItemGroup
オブジェクトを取得するItem
モデルのクエリを使用して、これを達成しました。また、prefetch_related
が2番目のクエリをトリガーしてそれらの関連オブジェクトを取得することも確認しましたが、これでは不十分です。
prefetch_related
は、複数の関連オブジェクトを取得するために使用されますこれはどういう意味ですか?単一のItemGroup
のような単一のオブジェクトをクエリする場合は、prefetch_related
を使用して、逆方向外部キー(1対多)または定義された多対多の関係など、複数の関連オブジェクトを含む関係を取得します。 Djangoいくつかの理由により、意図的に2番目のクエリを使用してこれらのオブジェクトを取得します
select_related
で必要とされる結合は、2番目のテーブルに対して結合を強制する場合、多くの場合パフォーマンスが低下します。これは、ItemGroup
を含まないItem
オブジェクトが失われないようにするために、右外部結合が必要になるためです。prefetch_related
が使用するクエリは、インデックス付きの主キーフィールドのIN
であり、これは、最もパフォーマンスの高いクエリの1つです。Item
オブジェクトのIDのみを要求するため、追加のサブクエリを実行しなくても、重複(多対多の関係の場合)を効率的に処理できます。これはすべて言い方です。prefetch_related
は、本来あるべきことを正確に行っており、理由のためにそうしています。
select_related
でこれをやりたいわかった、わかった。それが求められていることなので、何ができるか見てみましょう。
これを実現する方法はいくつかありますが、そのすべてに長所と短所があり、手動で「ステッチ」を行わないと最終的には機能しません。私は、組み込みのViewSetまたはDRFによって提供される汎用ビューを使用していないことを前提としていますが、その場合は、組み込みのフィルタリングを機能させるために、filter_queryset
メソッドでステッチを行う必要があります。ああ、それはおそらくページネーションを壊すか、ほとんど役に立たないものにします。
元のフィルターセットがItemGroup
オブジェクトに適用されています。そして、これはAPIで使用されているため、おそらく動的であり、それらを失いたくないでしょう。したがって、次の2つの方法のいずれかでフィルターを適用する必要があります。
フィルターを生成し、関連する名前をプレフィックスとして付けます。
したがって、通常のfoo=bar
フィルターを生成し、それらをfilter()
に渡す前にプレフィックスを付けて、related__foo=bar
にします。これで、関係全体をフィルタリングしているため、パフォーマンスに影響を与える可能性があります。
元のサブクエリを生成し、それをItem
クエリに直接渡します
prefetch_related
と同等のパフォーマンスを持つIN
クエリを生成する場合を除いて、これはおそらく「最もクリーンな」ソリューションです。代わりに、これはキャッシュできないサブクエリとして扱われるため、パフォーマンスが低下します。
シリアライザが機能するようにItem
オブジェクトとItemGroup
オブジェクトを「フリップアンドスティッチ」できるようにしたいので、これらの両方を実装することは現実的にこの質問の範囲外です。
Item
クエリを反転して、ItemGroup
オブジェクトのリストを取得するselect_related
がItemGroup
オブジェクトと一緒にすべての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モデルは不変ではなく、時々人々はハッシュ法を主キー以外のものにオーバーライドするので、私は辞書のキーとしてid
のItemGroup
を意図的に使用しています。
これにより、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
オブジェクトの属性です。_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"
]
}]
_