В 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

Но он не работает или делает много запросов к БД.

Как запросить проект и его потоки с их последним сообщением в одном вызове?

Вернуться на верх