HackSoftware / Django-Styleguide

Django styleguide used in HackSoft projects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to use selectors in APIs

sumit4613 opened this issue · comments

Your style guide suggests that model properties should be written as selectors when properties spans multiple relations.
My query is that model properties can be easily written to fields in model serializers but how can I use these selectors with API views or serializers.

An example snippet of the possible scenario can help here.

Thanks!

Hi, @sumit4613 !

Let's say we have these models:

from django.db import models


class School(models.Model):
    name = models.CharField()


class Course(models.Model):
    name = models.CharField()
    school = models.ForeignKey(School, related_name='courses', on_delete=models.CASCADE)


class Student(models.Model):
    name = models.CharField()
    school = models.ForeignKey(School, related_name='students', on_delete=models.CASCADE)


class StudentCourseEnrollment(models.Model):
    student = models.ForeignKey(Student, related_name='course_enrollments', on_delete=models.CASCADE)
    course = models.ForeignKey(Course, related_name='student_enrollments', on_delete=models.CASCADE)

We have a School, in which we can add many Students and Courses. Each Student can be enrolled in a certain Course via the StudentCourseEnrollment model.


Now, it depends on your exact use case.

If you just want to serialize spanning relations with no extra data (e.g. annotations/aggregations), you can directly write them in the serializer:

class SchoolSerializer(serializers.Serializer):
    name = serializers.CharField()
    students = inline_serializer(many=True, fields={
        'name': serializers.CharField(),
        'course_enrollments': inline_serializer(many=True, fields={
            'course': inline_serializer(fields={
                'name': serializers.CharField()
            })
        })
    })

These are all valid field names and will be serialized with no issues.


But if you want to serialize more custom data that you've dynamically added or used annotations/aggregations in the query, you should use a selector.

Let's say you want to display the count of enrollments for each student:

class SchoolSerializer(serializers.Serializer):
    name = serializers.CharField()
    students = inline_serializer(many=True, fields={
        'name': serializers.CharField(),
        'course_enrollments_count': serializers.IntegerField()  # You cannot get this directly!
    })

In order to achieve this, you need to annotate this (preferably in a selector):

# selectors.py

from typing import Iterable

from django.db.models import Count

from .models import School


def get_schools_with_annotated_course_enrollments_count_for_students() -> Iterable[School]:
    qs = School.objects.prefetch_related('students')

    for school in qs:
        school._students = school.students.annotate(course_enrollments_count=Count('course_enrollments'))
   
    return qs

What we do in the selector is actually attach a dynamic property to each School entry in the queryset, which is called _students. This property contains the actual students relation of the School entry, but with annotated course_enrollments_count.

Now, with a little tweak in the serializer you can render this data successfully:

class SchoolSerializer(serializers.Serializer):
    name = serializers.CharField()
    students = inline_serializer(source='_students', many=True, fields={
        # _______________________^
        'name': serializers.CharField(),
        'course_enrollments_count': serializers.IntegerField()  # We now have this annotated!
    })

Hope this answers your question and gives you a good example of the selector usage.

Cheers! 🙌

Leaving this open since it's a nice example to include in the styleguide

@wencakisa Thanks, for a well-explained example. There's one more thing I need to ask, can we achieve the same thing with model serializers???

@sumit4613 Yes, you can use ModelSerializer & specify the "special" fields explicitly (in the case with the annotations for example):

class StudentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Student
        fields = ('name', 'course_enrollments_count')
        # Note: You need to make sure `course_enrollments_count` is annotated before using this serializer,
        # otherwise it'll throw an exception for missing field.
        # That's why we recommend the usage of `inline_serializer`.


class SchoolSerializer(serializers.ModelSerializer):
    students = StudentSerializer(source='_students', many=True)

    class Meta:
        model = School
        fields = ('name', 'students')

I'm closing this for now 🙌