Skip to content

Tracing source of QueriesDisabledError with nested serializers #50

@johncmacy

Description

@johncmacy

The QueriesDisabledViewMixin is great at preventing queries during serialization. Once it's raised, however, I've had difficulty tracing the root cause - mostly when working with deeply nested serializers that have the same model serialized in multiple places.

To demonstrate, I extended your Pizza/Toppings example to include several models with multiple foreign keys to the User model:

Models
class Topping(models.Model):
    name = models.CharField(max_length=100)


class Pizza(models.Model):
    name = models.CharField(max_length=100)
    toppings = models.ManyToManyField(Topping)


class Order(models.Model):
    ordered_at = models.DateTimeField(auto_now_add=True)
    pizzas = models.ManyToManyField(Pizza, through='OrderPizza', related_name='orders')
    customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders_placed')
    received_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders_received')
    delivered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders_delivered')


class OrderPizza(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='order_pizzas')
    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE, related_name='order_pizzas')
    baked_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='order_pizzas_baked')

    class SizeChoices(models.IntegerChoices):
        SMALL = 1
        MEDIUM = 2
        LARGE = 3
        FAMILY = 4

    size = models.IntegerField(choices=SizeChoices.choices)
Serializers and Viewsets
class OrderPizzaSerializer(PaperTrailMixin, serializers.ModelSerializer):
    pizza = pizza.Serializers.Summary()
    baked_by = user.Serializers.Summary()

    class Meta:
        model = OrderPizza
        fields = [
            'id',
            'pizza',
            'baked_by',
            'size',
        ]

class OrderSerializer(PaperTrailMixin, serializers.ModelSerializer):
    customer = user.Serializers.Summary()
    received_by = user.Serializers.Summary()
    delivered_by = user.Serializers.Summary()

    order_pizzas = OrderPizzaSerializer(many=True)

    class Meta:
        model = Order
        fields = [
            'id',
            'ordered_at',
            'customer',
            'received_by',
            'delivered_by',
            'order_pizzas',
        ]

class OrderViewset(QueriesDisabledViewMixin, viewsets.ModelViewSet):
    serializer_class = OrderSerializer
    queryset = Order.objects.all()

    def get_queryset(self):
        return super().get_queryset()\
            .select_related(
                'customer', 
                'received_by', 
                'delivered_by',
            )\
            .prefetch_related(
                Prefetch(
                    lookup='order_pizzas',
                    queryset=OrderPizza.objects.select_related(
                        'pizza', 
                        # 'baked_by',
                    )
                ),
            )

When I make a request to /api/v1/orders/, since I "forgot" to prefetch OrderPizza.baked_by, it raises a QueriesDisabledError. However, it just tells me that it's trying to query a User. Without digging into it, it's not readily apparent which field on which serializer is causing the issue:

QueriesDisabledError at /api/v1/orders/
SELECT "users_user"."id", "users_user"."password", "users_user"."last_login", "users_user"."is_superuser", "users_user"."username", "users_user"."first_name", "users_user"."last_name", "users_user"."email", "users_user"."is_staff", "users_user"."is_active", "users_user"."date_joined" FROM "users_user" WHERE "users_user"."id" = %s LIMIT 21

To solve this, I put together a "PaperTrailMixin" for each serializer:

PaperTrailMixin
import logging
from rest_framework.serializers import Serializer
from zen_queries import QueriesDisabledError
from collections import OrderedDict
from rest_framework.relations import PKOnlyObject
from rest_framework.fields import SkipField

INDENT_STR = '.   '

class PaperTrailMixin(Serializer):
    debug_indent = 0

    def to_representation(self, instance):
        """
        Copied from Serializer.to_representation()
        Added code between ### > ... < ###:
            Wrapped each field with try...except QueriesDisabledError
            Added logging
        """

        ret = OrderedDict()
        fields = self._readable_fields

        self.logger_prefix = f'{INDENT_STR * self.debug_indent}'
        logging.debug(f'{self.logger_prefix}{self.__class__.__name__} | {instance.__class__.__name__} | {instance}')

        for field in fields:
            ### >
            field.debug_indent = field.parent.debug_indent + 1
            if hasattr(field, 'child'): field.child.debug_indent = field.debug_indent
            field.logger_prefix = f'{INDENT_STR * field.debug_indent}{f"{field.field_name} --> "}'

            try:
            ### <

                try:
                    attribute = field.get_attribute(instance)
                except SkipField:
                    continue

                ### >
                logging.debug(f'{field.logger_prefix}{attribute.__class__.__name__} | {field.__class__.__name__} | {attribute}')
                ### <

                check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
                if check_for_none is None:
                    ret[field.field_name] = None
                else:
                    ret[field.field_name] = field.to_representation(attribute)

            ### >
            except QueriesDisabledError as e:
                logging.debug(f'{field.logger_prefix}QueriesDisabledError')
                raise e
            ### <

        return ret

When a QueriesDisabledError is raised, it logs the following:

2022-08-18 08:26:05,065 OrderSerializer | Order | 1
2022-08-18 08:26:05,067 .   id --> int | IntegerField | 1
2022-08-18 08:26:05,067 .   ordered_at --> datetime | DateTimeField | 2022-08-17 18:33:23.727706+00:00
2022-08-18 08:26:05,068 .   customer --> User | Summary | rockybalboa
2022-08-18 08:26:05,068 .   Summary | User | rockybalboa
2022-08-18 08:26:05,068 .   .   id --> int | IntegerField | 4
2022-08-18 08:26:05,069 .   .   get_full_name --> str | ReadOnlyField | Rocky Balboa
2022-08-18 08:26:05,069 .   received_by --> User | Summary | hulkhogan
2022-08-18 08:26:05,069 .   Summary | User | hulkhogan
2022-08-18 08:26:05,070 .   .   id --> int | IntegerField | 3
2022-08-18 08:26:05,070 .   .   get_full_name --> str | ReadOnlyField | Hulk Hogan
2022-08-18 08:26:05,071 .   delivered_by --> User | Summary | maverick
2022-08-18 08:26:05,072 .   Summary | User | maverick
2022-08-18 08:26:05,072 .   .   id --> int | IntegerField | 5
2022-08-18 08:26:05,072 .   .   get_full_name --> str | ReadOnlyField | Maverick Mitchell
2022-08-18 08:26:05,073 .   order_pizzas --> RelatedManager | ListSerializer | core.OrderPizza.None
2022-08-18 08:26:05,073 .   OrderPizzaSerializer | OrderPizza | 1
2022-08-18 08:26:05,074 .   .   id --> int | IntegerField | 1
2022-08-18 08:26:05,074 .   .   pizza --> Pizza | Summary | Pepperoni
2022-08-18 08:26:05,075 .   .   Summary | Pizza | Pepperoni
2022-08-18 08:26:05,076 .   .   .   id --> int | IntegerField | 2
2022-08-18 08:26:05,076 .   .   .   name --> str | CharField | Pepperoni
2022-08-18 08:26:05,078 .   .   baked_by --> QueriesDisabledError
2022-08-18 08:26:05,078 .   order_pizzas --> QueriesDisabledError
2022-08-18 08:26:05,229 Internal Server Error: /api/v1/orders/

With this it's much easier to pinpoint where I need to add prefetches or select_related's to solve the error.

  1. Is there a mechanism for this already, that I've missed somehow?
  2. If not, would you be interested in adding something like this to the package?

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions