DRF is taking too much time to return nested serialized data

258 views Asked by At

We are having too many models which are related, While returning queryset serializing the data is too slow(serializer.data). Below are our models and serializer.

Why django nested serializer is taking too long to return JSON response. What are we doing wrong here?

models.py

class Doctor(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    first_name = models.CharField(_("first_name"), max_length=255, null=False)
    last_name = models.CharField(_("last_name"), max_length=255, null=False)
    full_name = property(lambda self: "{} {}".format(self.first_name, self.last_name))
    email = models.EmailField(_("email"), max_length=255, unique=True, null=False)
    password = models.CharField(_("password"), max_length=128, null=False)
    country_code = models.CharField(
        _("country_code"), max_length=10, null=False, blank=False
    )
    phone_number = models.CharField(
        _("phone_number"), max_length=50, null=False, blank=False
    )
    full_phone_number = models.CharField(
        _("full_phone_number"), max_length=60, unique=True, null=False
    )
    is_otp_verified = models.BooleanField(_("is_otp_verified"), default=False)
    gender = models.CharField(
        _("gender"), max_length=50, choices=choices.gender_choices, blank=True, null=False
    )
    image = encrypt(models.ImageField(upload_to=userProfilePictureDirectoryPath, default=None, null=True, blank=True))
    is_active = models.BooleanField(_("is_active"), default=True)
    is_verified = models.BooleanField(_("is_verified"), default=False)
    national_medical_id = models.CharField(max_length=100, null=True, unique=True)
    numeric_id = models.IntegerField(blank=True, null=True)
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True, blank=True, null=True)


    username = None
    USERNAME_FIELD = "full_phone_number"
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    class Meta:
        unique_together = (
            "country_code",
            "phone_number",
        )

    def __str__(self) -> str:
        return self.phone_number

class Specialties(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_en = models.CharField(_("name_en"), max_length=255, blank=True, null=True)
    name_ro = models.CharField(_("name_ro"), max_length=255, blank=True, null=True)
    name_ar = models.CharField(_("name_ar"), max_length=255, blank=True, null=True)
    priority = models.IntegerField(_("priority"), default=settings.DEFAULT_SPECIALITY_PRIORTY)
    icon = encrypt(models.ImageField(upload_to=userSpecialtiesDirectoryPath, blank=True))
    is_active = models.BooleanField(_("is_active"), default=False)
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True, blank=True, null=True)
    
    class Meta:
        ordering = ['priority']


class Profile(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    doctor = models.OneToOneField(to=Doctor, on_delete=models.CASCADE)
    bio = models.TextField(max_length=5000, blank=True)
    spoken_languages = models.ManyToManyField(to=Language, name=_("language"))
    graduation_year = models.CharField(max_length=4, blank=True)
    app_language = models.CharField(max_length=255, choices=choices.language_choices, default=choices.language_choices[0][0])
    date_of_birth = models.DateField(blank=True, null=True)
    reviews_average = models.FloatField(default=0.0)
    number_of_bookings = models.IntegerField(default=0)
    number_of_reviews = models.IntegerField(default=0)
    number_of_visitors = models.IntegerField(default=0)
    image = encrypt(models.ImageField(upload_to=userProfilePictureDirectoryPath, default=None, null=True, blank=True))
    lowest_price = models.FloatField(null=True, blank=True)
    specialties = models.ManyToManyField(to=Specialties, name="specialties")
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True, blank=True, null=True)

class Achievements(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
    description = models.TextField(
        _("description"), max_length=1024, blank=True, null=True
    )
    name = models.CharField(blank=True, null=True, max_length=255)
    image = encrypt(models.ImageField(upload_to=userAchivementsDirectoryPath, blank=True))
    is_active = models.BooleanField(_("is_active"), default=False)
    issued_at = models.DateTimeField(_("issued_at"), blank=True, null=True)
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True, blank=True, null=True)

class PlaceOfWork(models.Model):
    id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, unique=True, editable=False
    )
    doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
    title = models.CharField(max_length=255, blank=False, null=False)
    name = models.CharField(max_length=255, blank=False, null=False)
    still_working = models.BooleanField(default=True)
    started_at = models.DateField(blank=False, null=False)
    finished_at = models.DateField(blank=True, null=True)
    created_at = models.DateTimeField(_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(_("updated_at"), auto_now=True, blank=True, null=True)

views.py

class DoctorBySpecialityView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        doctor = request.user
        specialties = Specialties.objects
        active_doctor = get_all_active_doctors().count()
        search = request.query_params.get(constants.SEARCH, None)
        context = {"me": doctor, "search": search}
        serialized_specialties = SpecialitiesWithListOfDoctors(
            specialties, many=True, context=context
        )
        data = {
            "specialties": serialized_specialties.data,
            "active_doctor": active_doctor,
        }

        response = get_response(
            status=status.HTTP_200_OK,
            status_code=ResponseStatus.success.value,
            data=data,
        )
        return Response(response, status=status.HTTP_200_OK)

serializers.py

class SpecialtiesBaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Specialties
        read_only_fields = ("id",)


class SpecialtiesSerializer(SpecialtiesBaseSerializer):
    class Meta(SpecialtiesBaseSerializer.Meta):
        fields = "__all__"

class SpecialitiesWithListOfDoctors(SpecialtiesSerializer):
    doctors = serializers.SerializerMethodField()
    doctor_count = serializers.SerializerMethodField()

    def get_doctors(self, speciality):
        from doctor.serializers import DoctorProfileSerializer

        me = self.context["me"]
        search = self.context["search"]

        query = speciality.profile_set.filter(~Q(doctor=me)).all()

        if not search:
            query = query.filter(
                Q(doctor__first_name__icontains=search)
                | Q(doctor__last_name__icontains=search)
            )
        serialized_doctor_profile = DoctorProfileSerializer(
            query, many=True, context=self.context
        )
        self.context["doctor_count"] = query.count()
        return serialized_doctor_profile.data

    def get_doctor_count(self, speciality):
        return self.context["doctor_count"]


class DoctorProfileSerializer(ProfileSerializer):
    doctor = serializers.SerializerMethodField()
    place_of_work = serializers.SerializerMethodField()
    achievements = serializers.SerializerMethodField()
    chat_access = serializers.SerializerMethodField()

    def get_doctor(self, profile):
        doctor = profile.doctor
        return DoctorSerializer(doctor).data

    def get_place_of_work(self, profile):
        place_of_work = profile.doctor.placeofwork_set.all()
        return PlaceOfWorkSerializer(place_of_work, many=True).data

    def get_achievements(self, profile):
        achievements = profile.doctor.achievements_set.all()
        return AchievementsSerializer(achievements, many=True).data


class DoctorBaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Doctor


class DoctorSerializer(DoctorBaseSerializer):
    need_support_help = serializers.SerializerMethodField()

    def create(self, validated_data):
        validated_data["password"] = make_password(validated_data["password"])
        validated_data["numeric_id"] = get_numeric_id()

        return super(DoctorSerializer, self).create(validated_data)

    class Meta(DoctorBaseSerializer.Meta):
        read_only_fields = ("uuid",)
        exclude = [
            "numeric_id",
        ]
        extra_kwargs = {
            "password": {"write_only": True},
            "is_superuser": {"write_only": True},
            "is_staff": {"write_only": True},
            "date_joined": {"write_only": True},
            "groups": {"write_only": True},
            "user_permissions": {"write_only": True},
            "last_login": {"write_only": True},
        }

    def get_need_support_help(self, doctor):
        #using some other models 
        support_help, created = SupportHelp.objects.get_or_create(doctor=doctor)
        need_help = doctor.supporthelp.need_help
        return need_help


class PlaceOfWorkBaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = PlaceOfWork
        read_only_fields = ("id",)

    def validate(self, data):
        if self.partial:
            return super().validate(data)
        if data.get("finished_at"):
            data["still_working"] = False
            if data["finished_at"] < data["started_at"]:
                raise serializers.ValidationError(
                    {"finished_at": "finished_at should be greater than started_at"}
                )
        still_working = data.get("still_working", True)
        if not still_working and not data.get("finished_at"):
            raise serializers.ValidationError(
                {"finished_at": "finished_at is required if still_working is false"}
            )
        return super().validate(data)


class PlaceOfWorkSerializer(PlaceOfWorkBaseSerializer):
    class Meta(PlaceOfWorkBaseSerializer.Meta):
        fields = "__all__"


class AchievementsBaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Achievements
        read_only_fields = ("id",)

    def validate_image(self, image):
        if not validate_image_size(image):
            raise serializers.ValidationError(
                f"image should be between {settings.MIN_IMAGE_MB}MB and {settings.MAX_IMAGE_MB}MB"
            )
        return image


class AchievementsSerializer(AchievementsBaseSerializer):
    class Meta(AchievementsBaseSerializer.Meta):
        fields = "__all__"

Note: It is one the example. we have too many like this. In this example i want to get all specialities with the list of doctors and doctor it self contain its achievements, place of works and many other models

1

There are 1 answers

0
Ernesto González On

Serialisation time is normally not the problem with nested serialisers, but the number of queries being executed is.

You need to pass an optimised query set to your serialised using select_related and prefetch_related when possible.

Start by seeing the number of queries executed for each request, I recommend Django Debug Toolbar