[Django]-Truncate the length of model serializer with many=True in Django

-1👍

Everybody was right about the messages = serializers.SerializerMethodField(). One thing that needs to be mentioned is ordering the list to be the last messages of the array.

For my implementation, I need the last 50 messages sorted in ascending timestamp (getting later) so this is what I did.

MESSAGE_COUNT = 50
class ChatSerializer(serializers.ModelSerializer):
    messages = serializers.SerializerMethodField()
    class Meta:
        model = Deal
        fields = '__all__'

    def get_messages(self, chat):
        qs = Message.objects.filter(deal=deal).order_by('timestamp')
        if len(qs) > MESSAGE_COUNT:
            qs = qs[len(qs)-MESSAGE_COUNT:]
        return MessageSerializer(instance=qs, many=True).data

4👍

You can use a SerializerMethodField within ChatSerializer as follows:

messages = serializers.SerializerMethodField()

def get_messages(self, chat):
    qs = Message.objects.filter(chat=chat).order_by('-date')[:50]
    return MessageSerializer(instance=qs, many=True).data

This runs a separate query for every Chat instance, but it only fetches the required number of rows. You’d have to customize the field names (chat, date) as applicable.


The alternative syntax @spiritsree is aiming at results in the same SQL, using implicit rather than explicit filtering:

qs = chat.messages.order_by('-date')[:50]

One thing to avoid is using prefetch_related('messages') in the queryset of the ViewSet that returns the Chat list as that prefetch won’t be used at all and would haul in all messages from the database only to be discarded unused.


The subquery alternative dismissed in another answer as slow is in fact quite interesting. It saves you as many roundtrips to the database as there are Chats. In exchange however, the database has to execute twice as many queries internally.

The excess queries being quite lightweight (selecting a small number of messages by id and ordering them), the saved roundtrips may easily make up for them. In my quick tests this method was more than 10x as fast as using SerializerMethodField. It may depend to some extent on the data; test it for yourself:

from rest_framework import viewsets
from django.db.models import Prefetch, Subquery, OuterRef

class ChatViewSet(viewsets.ModelViewSet):
    prefetch = Prefetch(
        'messages',
        queryset=Message.objects
            .filter(id__in=Subquery(Message.objects
                .filter(chat=OuterRef('chat_id'))
                .order_by('-date')
                .values_list('id', flat=True)[:4]))
            .order_by('-date')
    )

    queryset = Chat.objects.prefetch_related(prefetch)

2👍

You could use SerializerMethodField to do that.

MESSAGE_COUNT = 50
class ChatSerializer(serializers.ModelSerializer):
    messages = serializers.SerializerMethodField()
    class Meta:
        model = Chat
        fields = '__all__'

    def get_messages(self, obj):
        return MessageSerializer(obj.messages[:MESSAGE_COUNT ], many=True).data

0👍

The answer by @Tobey is unrelated to nested limiting and should be disregarded.

The answer by @spritsree would force the limiting to be performed in Python instead of at the Database level, where you want it to be done.

The details you are looking for are not implemented at the serializer level.

When you instantiate your serializer, you have to pass it the data that you wish to serialize. In your case, you should perform this reverse relationship limiting when gathering the data from the database.

I can only assume that you a 1-N relationship on Chat-Message. Therefore, you would WISH that you could do something like the following when writing your QuerySet:

QuerySet = Chat.objects.all().prefetch_related(
    Prefetch(
        "messages", queryset=Message.objects.all().order_by("-created_at")[:50]
    )
)

However, Django does not support slicing in prefetch related query sets. There is a work around described, but you need to perform an IN query, and this is the slowest way of solving this issue.

Instead, you should separate the message gathering into a separate query:

# assuming that you are only interested in a single chat
chat = Chat.objects.latest("created_at")
messages = Message.objects.filter(chat=chat).order_by("created_at")[:50]

# instantiate serializer
serializer = ChatSerializer(data={"chat": chat, "messages": messages, ...})
serializer.data
...

Leave a comment