0

我使用nplusone来检测 N+1 个查询。

我有一个序列化实例的serpy序列化程序。OrderAnOrder有一个cartOrderComponent实例组成的,如下所示。代码简化:

class Order(models.Model):
    objects = OrderManager()
    listings = models.ManyToManyField(to=Listing, through="OrderComponent", related_name="orders")


class OrderComponent(models.Model):
    listing = models.ForeignKey(to=Listing, on_delete=models.PROTECT, related_name="order_components")
    order = models.ForeignKey(to=Order, on_delete=models.PROTECT, related_name="cart")
    nb_units = models.PositiveIntegerField()

Order通过以下方式获取序列化实例列表OrderListView

class OrderListView(SimplePaginatedListView):  # Custom base class
    model = Order
    serializer_class = OrderSerializer
    deserializer_class = OrderDeserializer

    def get_queryset(self):
        return super().get_queryset().select_related(
            "fulfillment",  # Used for order.is_fulfilled()
            "cancellation",  # Used for order.is_cancelled()
        ).prefetch_related(
            "cart",  # I don't believe the first two are necessary, but added for testing purposes
            "cart__listing",
            "cart__listing__product",
            "cart__listing__service",
        )

    @http_get(required_permissions=ShopRolePermission.MANAGE_ORDERS)
    def get(self, *args, **kwargs):
        return super().get(*args, **kwargs)

    @http_post(required_permissions=ShopRolePermission.MANAGE_ORDERS)
    def post(self, *args, **kwargs):
        return super().post(*args, **kwargs)

OrderSerializer定义如下。nplusone没有说明在哪里检测到 N+1 查询,所以我已经注释掉了所有可能的罪魁祸首并找到了真正的罪魁祸首。我已经在评论中指出它们在哪里。

class OrderSerializer(BaseSerializer):
    class SimpleOrderComponentSerializer(BaseSerializer):
        id = serpy.IntField()
        listing_id = serpy.IntField()
        product_name = serpy.MethodField(required=False)  # No N+1
        service_name = serpy.MethodField(required=False)  # No N+1
        nb_units = serpy.IntField()

        def __init__(self, instance=None, many=False, data=None, context=None, **kwargs):
            # Should this be necessary, since I prefetched in get_queryset?
            instance = instance.select_related("listing", "listing__product", "listing__service")
            super().__init__(instance, many, data, context, **kwargs)

        @staticmethod
        def get_product_name(_serializer, obj: OrderComponent):
            # No N+1
            return obj.listing.product.product_name if obj.listing.product else None

        @staticmethod
        def get_service_name(_serializer, obj: OrderComponent):
            # No N+1
            return obj.listing.service.service_name if obj.listing.service else None

    id = serpy.IntField()
    cancelled = serpy.BoolField(attr="is_cancelled")  # Checks if Cancellation instance exists, no N+1
    fulfilled = serpy.BoolField(attr="is_fulfilled")  # Checks if Fulfillment instance exists, no N+1
    cart_products = serpy.MethodField(required=False)  # N+1 !!!
    cart_services = serpy.MethodField(required=False)  # N+1 !!!

    def get_cart_products(self, obj: Order):
        # N+1 detected here.
        return self.SimpleOrderComponentSerializer(obj.cart.filter(listing__product__isnull=False), many=True).data

    def get_cart_services(self, obj: Order):
        # N+1 detected here. Curiously, the number of N+1 queries detected differs between the two.
        return self.SimpleOrderComponentSerializer(obj.cart.filter(listing__service__isnull=False), many=True).data

我一生都无法弄清楚为什么我的预取在这里不起作用。Django Debug Toolbar 确认它不是误报:

SELECT ••• FROM "shop_ordercomponent" INNER JOIN "shop_listing" ON ("shop_ordercomponent"."listing_id" = "shop_listing"."id") INNER JOIN "shop_product" ON ("shop_listing"."product_id" = "shop_product"."id") LEFT OUTER JOIN "shop_service" ON ("shop_listing"."service_id" = "shop_service"."id") WHERE ("shop_ordercomponent"."order_id" = 2 AND "shop_listing"."product_id" IS NOT NULL)
  25 similar queries. 

如果我objget_cart_servicesor中检查get_cart_products,我看到那obj._prefetched_objects_cache["cart"]QuerySetof OrderComponent。例如,如果我obj在中进行检查,我在 中看不到任何东西,这是我没有预料到的,因为and是-ed,而不是-ed。SimpleOrderComponentSerializer.get_product_nameobj._prefetched_objects_cache["cart"]listing.productlisting.serviceselect_relatedprefetch_related

我承认我不完全理解它是如何工作的,但我假设select_related在一个查询中贪婪地填充一对一的关系,而不是懒惰地等待相关对象被请求。

从外观上看,我的初始值prefetch_related不会“延续”到内部序列化程序的MethodField处理程序中。nplusone日志:

Potential n+1 query detected on `Order.cart`
Potential n+1 query detected on `Order.cart`
Potential n+1 query detected on `Order.cart`
...  # Repeated many times. Multiple warnings printed for each object.

是因为cart是“反向”关系吗?任何帮助理解为什么这不起作用将不胜感激。

4

1 回答 1

1

我通过在其中注释并获取所需信息来解决问题get_queryset。这样,预取的信息将存储在每个实例中。

于 2020-11-25T14:27:38.653 回答