Scaling Django Where to put business logic
Video: Django: Where to put business logic
TLDR¶
Cherry-pick whatever makes sense to you, based on your specific context.
Separation of concerns
- [[#Models]]- define data models
- relationships between models
- model properties- for trivial DB fetches
 
- clean()validation- called with model_instance.full_clean()
 
- called with 
 
- [[#Services]]- writes to DB
- Selectors- a kind of service that just fetches from DB
- trivial DB fetches: use model properties
- can handle permissions, filtering
 
 
- [[#Views & APIs]]- YouTube: Django structure#APIs
- [[#Serializers]]: nested inside the API- Python/ORM objects <--> JSON
 
 
TLDR example¶
class CourseListApi(AuthMixin, APIView):
    class OutputSerializer(serializers.ModelSerializer):
        class Meta:
            model = Course
            fields = ('id', 'name', 'start_date', 'end_date')
    def get(self, request):
        courses = get_courses()  # service or selector
        data = self.OutputSerializer(courses, many=True)
        return Response(data)
???? Would this even work for our use case of nested serialization
[! Serializer vs ModelSerializer] Output serializer:
ModelSerializeris nice for returning listscreate/update: only use plain
Serializer, explicitly state all the fields ModelSerializer would handle the creating
class CourseCreateApi(AuthMixin, APIView):
    class InputSerializer(serializers.Serializer):
        name = serializers.CharField()
        start_date = serializers.DateField()
        end_date = serializers.DateField()
    def post(self, request):
        serializer = self.InputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        create_course(**serializer.validated_data)
    return Response(status=status.HTTP_201_CREATED)
class CourseUpdateApi(AuthMixin, APIView):
    class InputSerializer(serializers.Serializer):
        name = serializers.CharField(required=False)
        start_date = serializers.DateField(required=False)
        end_date = serializers.DateField(required=False)
    def post(self, request):
        serializer = self.InputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        update_course(course_id=course_id, **serializer.validated_data)
    return Response(status=status.HTTP_201_CREATED)
[!Why inline serializer]- changing a base serializer -> could break 5 APIs break all the tests
Notes on abstraction¶
- If you're putting something the database, use as little abstraction as possible- no DRF modelviewsets
- ?????????
 
- getting something out of database- use as much abstraction as needed
 
Boxes that Django gives us
- Models
- Views/APIs
- Templates
- Forms / Serializers
- Tasks
Models¶
- small @property- has_started
- has_finished
 
- validation logic with clean- [[Django Models#Validating objects]]
 
Why business logic doesn't belong in Models¶
- fat models aren't maintainable- God object
 
- models only for data model & relations
- clean() for additional validation
- don't modify save()for additional logic
Testing Models¶
- custom validations or properties
- tests don't hit db -> fast tests 🏃
Custom Model Managers and QuerySets¶
Django Style guide: business logic should not live in custom managers or query sets
When to use custom model managers and querysets?¶
- like model properties
- use this for simple stuff- there might be more complex logic like 3rd party API calls
- where you should move the logic to services.py
 
Custom Model Manager example¶
Custom Model Manager & Model QuerySet
get_queryset is like Post.objects.all()
QuerySet Manager example¶
Implement your own get_queryset
so that you can
Views & APIs¶
Django Rest Framework (DRF)
need to put it in the serializer
model view set???
Serializers¶
- Python/ORM objects <--> JSON
Why serializers shouldn't create objects and do business logic¶
- misleading and confusing when you have to do stuff that the serializer doesn't immediately support- update some deep abstraction
 
separation of concerns
Why nested Serializers in the view API?¶
We don't want to reuse serializers, especially for InputSerializers
- output serializer is okay sometimes
Editing a base serializer will break a ton of APIs - need to reuse serializers with great care
Adding 1 property --> breaking 5 other APIs because they all use the serializer
Services¶
What is a service¶
- simple function (w/ type hints)
- speaks the domain language
- handles- permissions
- cross-cutting concerns
- calls other services/tasks
- works mainly with models
 
def user_create(
    *,  # keyword only
    email: str,
    name: str
) -> User:
    user = User(email=email)
    user.full_clean()
    user.save()
    # calls other services
    profile_create(user=user, name=name)
    confirmation_email_send(user=user)
    return user
When to use a service¶
- every non-trivial operation that touches the database should be a service
- no ORM code in your AP
selectors.py¶
- business logic around fetching
- not always necessary handle permissions, filtering, ...
Rule of thumb¶
- if you get the N + 1 problem- If your model property starts doing queries on the model's relations
- which can't be solved with select_relatedorprefetch_related
 
- it should be a selector
example
class Lecture(models.Model):
    @property
    def not_present_students(self):
        present_ids = self.present_students.values_list('id', flat=True)
        return self.course.students.exclude(id__in=present_ids)
Testing services¶
- where the business logic is
- hit the db
- mocking