mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-28 21:47:34 +00:00
Perf: batch CustomField lookups when validating a document's custom_fields
DocumentSerializer.custom_fields validates each item's field id via a plain PrimaryKeyRelatedField, which issues one SELECT per custom field per validation pass (discussion #13690). Batch-resolve all field ids in one query and cache them on the field instance so per-item validation is free instead of re-querying.
This commit is contained in:
committed by
Trenton H
parent
e24db7023a
commit
f8e5aea8dd
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
@@ -876,8 +877,50 @@ def validate_documentlink_targets(user, doc_ids):
|
||||
)
|
||||
|
||||
|
||||
class _CachingCustomFieldPrimaryKeyField(serializers.PrimaryKeyRelatedField):
|
||||
"""
|
||||
A document's custom_fields are validated as a list; the default
|
||||
PrimaryKeyRelatedField issues one SELECT per item. CustomFieldInstanceListSerializer
|
||||
below resolves all of an incoming list's field ids in a single query up
|
||||
front and caches them here, keyed by id, so per-item validation is free
|
||||
instead of re-querying. The cache lives on this field instance, which
|
||||
DRF constructs fresh for each request -- no state persists between
|
||||
requests.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._cache: dict[int, CustomField] = {}
|
||||
|
||||
def prefetch(self, ids: Iterable[int]) -> None:
|
||||
missing = {i for i in ids if i not in self._cache}
|
||||
if missing:
|
||||
for obj in self.get_queryset().filter(pk__in=missing):
|
||||
self._cache[obj.pk] = obj
|
||||
|
||||
def to_internal_value(self, data: int) -> CustomField:
|
||||
if data in self._cache:
|
||||
return self._cache[data]
|
||||
obj: CustomField = super().to_internal_value(data)
|
||||
self._cache[obj.pk] = obj
|
||||
return obj
|
||||
|
||||
|
||||
class CustomFieldInstanceListSerializer(serializers.ListSerializer):
|
||||
def to_internal_value(self, data: Any) -> list[Any]:
|
||||
if isinstance(data, list):
|
||||
field_ids = {
|
||||
item["field"]
|
||||
for item in data
|
||||
if isinstance(item, dict) and "field" in item
|
||||
}
|
||||
if field_ids:
|
||||
self.child.fields["field"].prefetch(field_ids)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class CustomFieldInstanceSerializer(serializers.ModelSerializer[CustomFieldInstance]):
|
||||
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
|
||||
field = _CachingCustomFieldPrimaryKeyField(queryset=CustomField.objects.all())
|
||||
value = ReadWriteSerializerMethodField(allow_null=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -978,6 +1021,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer[CustomFieldInsta
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldInstance
|
||||
list_serializer_class = CustomFieldInstanceListSerializer
|
||||
fields = [
|
||||
"value",
|
||||
"field",
|
||||
|
||||
Reference in New Issue
Block a user