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.
- Is there a mechanism for this already, that I've missed somehow?
- If not, would you be interested in adding something like this to the package?
Thanks!
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
Serializers and Viewsets
When I make a request to
/api/v1/orders/, since I "forgot" to prefetchOrderPizza.baked_by, it raises aQueriesDisabledError. 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:To solve this, I put together a "PaperTrailMixin" for each serializer:
PaperTrailMixin
When a QueriesDisabledError is raised, it logs the following:
With this it's much easier to pinpoint where I need to add prefetches or select_related's to solve the error.
Thanks!