В Django + REST Framework, какой правильный способ получения префетишированных данных в сложных сериализаторах?
Мы делаем веб-приложение, очень похожее на форум (по концепции). Пользователи могут публиковать сообщения в темах, которые организованы во вложенные категории, принадлежащие проекту. Проект - это группа пользователей и их контент/активность внутри него.
Мы используем Django + DRF для бэкенда и Vue для фронтенда.
Я взял на себя бэкенд и установил панель инструментов отладки. Я понимаю, что наш бэкенд - это беспорядок, который делает много запросов, поэтому я пытаюсь справиться с этим. Я нашел много руководств по prefetch_related
и select_related
на простых примерах, но в моем случае я изо всех сил пытаюсь заставить это работать.
Что бы я сделал на данный момент: получить текущий проект и 12 последних активных тем с их последним сообщением в одном запросе.
Вот мои модели:
# projects/models.py
class Project(models.Model):
name = models.CharField(max_length=500, verbose_name=_('Name'))
slug = models.SlugField(unique=True)
image = models.ImageField(
blank=True, null=True,
upload_to=project_directory_path,
max_length=500,
verbose_name=_("Project image"),
validators=[validate_file_extension]) # not working. Bug ?
image_token = models.CharField(blank=True, max_length=50, verbose_name=_('image token'))
preferences = models.TextField(blank=True,verbose_name=_('Preferences'))
# ... some other methods ....
Вот мой вид (я стараюсь хранить последние потоки в методе initial
, потому что это единственный способ, который я нашел, чтобы не вызывать этот запрос много раз):
# projects/views.py
class ProjectsViewSet(BaseProjectsViewSet):
lookup_field = 'slug'
serializer_class = ProjectSerializer
action_permission_classes = {
'update': [ProjectAdminPermission],
'destroy': [ProjectAdminPermission]
}
def initial(self, request, *args, **kwargs):
user = request.user
self.permissions = {}
if user and user.is_superuser:
self.permissions['superuser'] = list(
Permission.objects.filter(content_type__model='project').values_list("codename", flat=True)
)
else:
permissions = UserObjectPermission.objects.select_related('permissions').filter(
user=user,
content_type__model='project',
).values('object_pk', 'permission__codename')
for permission in permissions:
pk = int(permission['object_pk'])
codename = permission['permission__codename']
if pk in self.permissions:
self.permissions[pk].add(codename)
else:
self.permissions[pk] = {codename}
if 'last_threads' in self.request.GET.get("extra_fields", "").split(","):
self.last_threads = self.get_last_threads()
return super().initial(request, *args, **kwargs)
def get_last_threads(self):
queryset = Thread.objects.filter(
category__project=self.get_object(),
category__status="published",
messages_number__gt=0,
status="published").select_related('category').prefetch_related('message_set', 'category__child_categories')
return queryset
def get_serializer_context(self):
context = super().get_serializer_context()
context["extra_fields"] = self.request.GET.get("extra_fields", "").split(",")
context["permissions"] = self.permissions
if 'last_threads' in self.request.GET.get("extra_fields", "").split(","):
context["last_threads"] = self.last_threads
return context
# ... some other methods ....
Вот мои сериализаторы:
# projects/serializer.py
class ProjectSerializer(serializers.ModelSerializer):
image = serializers.ImageField(max_length=None, allow_empty_file=True, allow_null=True, required=False)
permissions = serializers.SerializerMethodField('project_permissions')
users = serializers.SerializerMethodField('get_users')
last_threads = serializers.SerializerMethodField('get_last_threads')
last_medias = serializers.SerializerMethodField('get_last_medias')
last_events = serializers.SerializerMethodField('get_last_events')
last_polls = serializers.SerializerMethodField('get_last_polls')
posts = serializers.SerializerMethodField('get_posts')
subscriptions = serializers.SerializerMethodField('get_subscriptions')
thumbnail = serializers.SerializerMethodField('get_thumbnail')
class Meta:
model = Project
fields = ['name', 'slug', 'pk', 'permissions', 'users', 'image', 'thumbnail', 'preferences', 'last_threads', 'last_medias', 'last_events', 'last_polls', 'posts', 'subscriptions']
lookup_field = 'slug'
read_only_fields = ['pk', 'slug', 'permissions', 'users', 'thumbnail', 'last_threads', 'last_medias', 'last_events', 'last_polls', 'posts', 'subscriptions']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
extra_fields = self.context.get("extra_fields", None) or []
EXTRAS = [ 'last_threads', 'last_medias', 'last_events', 'last_polls', 'posts', 'users', 'subscriptions']
for extra in EXTRAS:
if extra not in extra_fields:
del self.fields[extra]
def get_last_threads(self, obj):
if 'last_threads' in self.context:
threads = self.context['last_threads']
else :
threads = []
serializer = ThreadSerializer(threads, many=True, context={'request': self.context.get("request"),'project': obj, 'user': self.get_current_user()})
return serializer.data
# ... some other methods ....
# messaging/serializer.py
class ThreadSerializer(serializers.ModelSerializer):
synthesis_user_update = UserSerializer(many=False, read_only=True)
unread_messages_number = serializers.SerializerMethodField('get_unread_messages_number')
last_message = serializers.SerializerMethodField()
category = CategorySerializer(many=False)
dates = SimpleDateSerializer(many=True, read_only=True)
class Meta:
model = Thread
fields = [
'pk', 'title', 'category', 'status', 'synthesis',
'update', 'messages_update', 'messages_number', 'modules'
]
lookup_field = 'pk'
read_only_fields = [
'pk', 'category',
'update', 'messages_update', 'messages_number',
]
def get_unread_messages_number(self, obj):
return obj.get_unread_messages_number(self.context.get("user"))
def get_last_message(self, obj):
# Here it is : I don't know how to properly get a Message object properly...
Старый код был:
def get_last_message(self, obj):
return MessageSerializer(obj.get_last_message(), many=False, context={'request': self.context.get("request"), 'thread_access_date':None, 'hello': 'coucou'}).data
Я возился с этим:
def get_last_message(self, obj):
# this is for test purpose, and doesn't add a query
for thread in self.instance:
for message in thread.message_set.all():
print(message.text)
print()
# End of the test
thread = self.instance.filter(thread_pk=obj.pk, status='sent').last()
return MessageSerializer(thread, many=False, context={'request': self.context.get("request"), 'thread_access_date':None}).data
Но он не работает или делает много запросов к БД.
Как запросить проект и его потоки с их последним сообщением в одном вызове?