django-virtual-models

repository·main·Indexed 41 days ago

https://github.com/vintasoftware/django-virtual-models

A declarative prefetching layer for Django and Django REST Framework that allows developers to define complex query logic—including joins, prefetches, and annotations—within reusable Virtual Model classes. It prevents N+1 performance issues and ensures data consistency between serializers and querysets by providing specialized components like VirtualModelSerializer and VirtualModelListAPIView.

Tokens
9.4K
Snippets
28
Records
41
Agent score
100%

What's inside django-virtual-models

  1. What are Virtual Models and why use them?

    main

    Django Virtual Models introduces a "prefetching layer" to help manage complex read logic in Django and Django REST Framework (DRF) projects.

    Instead of manually managing complex chains of select_related, prefetch_related, and annotate calls in your view querysets—which is error-prone and often leads to N+1 query problems—you can declare all nesting, annotations, and joins in a single, reusable, and declarative VirtualModel class.

    Key benefits include:

    • Performance: Prevents N+1 select problems by ensuring all necessary prefetches are included.
    • Maintainability: Keeps complex query logic bundled with the data structure it represents.
    • Safety: When used with DRF, the library validates that your VirtualModel covers all fields required by your VirtualModelSerializer. If a field is missing, it raises a MissingVirtualModelFieldException to guide you toward the correct implementation.
  2. Understand the Change Amplification problem in Django

    main

    In standard Django and Django REST Framework (DRF) projects, read logic (serializers) is often decoupled from the database optimization logic (prefetching and annotations). This leads to change amplification: when you add a field to a serializer that requires an annotation or a prefetch, you must manually find and update every single View/QuerySet that uses that serializer (or its parent serializers) to include the necessary .annotate() or .prefetch_related() calls.

    Failure to do this results in:

    1. N+1 Selects Problem: Performance degradation due to excessive database queries.
    2. Brittle Codebase: Coordinated changes across different modules (Views vs. Serializers) are difficult to maintain and easy to miss during code reviews.
  3. How DRF serializer integration works

    main

    The integration between DRF and django-virtual-models is handled by the LookupFinder class.

    1. LookupFinder automatically discovers which virtual fields are required by a serializer.
    2. It raises exceptions if a required field is missing from the virtual model associated with the serializer.
    3. It uses recursively_find_lookup_list to generate the lookup_list required for optimization.
    4. This logic is automatically triggered in views using GenericVirtualModelViewMixin via its get_queryset method.

    Developers can customize this logic by overriding the behavior in their own view classes.

  4. Access Request Context (User and Serializer Context) in Virtual Models

    main

    Virtual Models can access dynamic data passed through the request or serializer context. This allows for user-specific annotations or filtered prefetches.

    Accessing the Current User

    In v.Annotation, the user argument is automatically injected from self.context["request"].user. In get_prefetch_queryset, the user argument is also available.

    class VirtualMovie(v.VirtualModel):
        user_rating = v.Annotation(
            lambda qs, user, **kwargs: qs.annotate(
                Subquery(
                    UserMovieRating.objects.filter(
                        movie=OuterRef("pk"),
                        user=user
                    ).values("rating")[:1]
                )
            )
        )
    
        class Meta:
            model = Movie

    Accessing Serializer Context

    You can pass custom data from a view to the serializer via get_serializer_context, and then access it in a v.Annotation using the serializer_context argument.

    # In the View
    def get_serializer_context(self):
        context = super().get_serializer_context()
        return {**context, 'vote_value': 5}
    
    # In the Virtual Model
    class VirtualMovie(v.VirtualModel):
        voting_count = v.Annotation(
            lambda qs, user, serializer_context, **kwargs: qs.annotate(
                voting_count=Count("ratings", filter=Q(ratings__rating=serializer_context['vote_value']))
            )
        )
    
        class Meta:
            model = Movie
    class VirtualMovie(v.VirtualModel):
        user_rating = v.Annotation(
            lambda qs, user, **kwargs: qs.annotate(
                Subquery(
                    UserMovieRating.objects.filter(
                        movie=OuterRef("pk"),
                        user=user
                    ).values("rating")[:1]
                )
            )
        )
    
        class Meta:
            model = Movie
  5. Define Virtual Models for complex read logic

    main

    To define a Virtual Model, inherit from django_virtual_models.VirtualModel. You can declare nested Virtual Models using the same syntax as DRF serializers and define annotations using v.Annotation.

    • Use class Meta: model = YourModel to specify the base model.
    • Use v.Annotation to define queryset-level computations.
    • Nest other Virtual Models to represent relationships, using the lookup argument to specify the related name/field.
    import django_virtual_models as v
    
    class VirtualAward(v.VirtualModel):
        class Meta:
            model = Nomination
    
        def get_prefetch_queryset(self, **kwargs):
            return Nomination.objects.filter(is_winner=True)
    
    
    class VirtualPerson(v.VirtualModel):
        # Nesting a Virtual Model with a lookup
        awards = VirtualAward(lookup="nominations")
        
        # Defining an annotation
        nomination_count = v.Annotation(
            lambda qs, **kwargs: qs.annotate(
                nomination_count=Count("nominations")
            ).distinct()
        )
    
        class Meta:
            model = Person
    
    
    class VirtualMovie(v.VirtualModel):
        # Nesting another Virtual Model
        directors = VirtualPerson()
    
        class Meta:
            model = Movie
  6. Explore the django-virtual-models architecture in the example

    main

    The example project is structured to demonstrate how to define relationships and implement virtual models. To understand the implementation, examine the following files:

    • Model Relationships: See example/movies/models.py to understand how standard Django models are structured.
    • Virtual Model Implementation: See example/movies/virtual_models.py to see how django-virtual-models is used to create virtual representations.
    • Data Consumption: See example/movies/views.py to see how the virtual models are accessed within a Django view.
  7. Define Virtual Models for Django REST Framework

    main

    To prevent N+1 queries and manage complex querysets, use VirtualModel and VirtualModelSerializer to create a layer that defines exactly which fields (annotations, prefetches, etc.) a serializer requires.

    1. Define a VirtualModel: Create a class inheriting from v.VirtualModel and specify the target model in class Meta.
    2. Define a VirtualModelSerializer: Use v.VirtualModelSerializer and link it to your virtual model using the virtual_model attribute in class Meta.
    3. Use Virtual View Classes: Use v.VirtualModelListAPIView or v.VirtualModelRetrieveAPIView instead of standard DRF views to ensure the optimized queryset is automatically applied.

    Note: If you override get_queryset in your view, you must call super().get_queryset() to maintain integration. If using custom base views, use GenericVirtualModelViewMixin.

    import django_virtual_models as v
    
    class VirtualMovie(v.VirtualModel):
        class Meta:
            model = Movie
    
    class MovieSerializer(v.VirtualModelSerializer):
        directors = PersonSerializer(many=True)
    
        class Meta:
            model = Movie
            virtual_model = VirtualMovie
            fields = ["name", "directors"]
    
    class MovieListView(v.VirtualModelListAPIView):
        serializer_class = MovieSerializer
        queryset = Movie.objects.all()
  8. Run the django-virtual-models example project

    main

    To run the demonstration project, follow these steps to set up the database, load the initial data, and start the development server:

    1. Run migrations to set up the database schema.
    2. Load the initial dataset using the provided YAML file.
    3. Start the Django development server.

    Once running, you can view the data returned by the view at http://localhost:8000/movies/.

    python manage.py migrate
    python manage.py loaddata movies_initial.yaml
    python manage.py runserver
  9. Prefetch model properties and methods

    main

    If your DRF serializer includes model properties or methods in its Meta.fields, you must apply the same hints decorators directly to the model definition to ensure the necessary virtual fields are prefetched when the serializer is used.

    Note: For properties, the decorator must be placed after the @property decorator.

    from django_virtual_models import hints
    
    class Person(models.Model):
        name = models.CharField(max_length=255)
    
        @property
        @hints.no_deferred_fields()
        def name_title_case(self):
            return self.name.title()
    
        def has_won_any_award(self: Annotated[Person, hints.Virtual("awards")]):
            return len(self.awards) > 0
    
    class PersonSerializer(v.VirtualModelSerializer):
        class Meta:
            model = Person
            virtual_model = VirtualPerson
            fields = ["name_title_case", "has_won_any_award"]
  10. Integrate Virtual Models with Django REST Framework

    main

    To use Virtual Models with DRF, you must switch from standard DRF classes to the specialized classes provided by django_virtual_models:

    1. Serializer: Inherit from v.VirtualModelSerializer and specify the virtual_model in the Meta class.
    2. View: Inherit from v.VirtualModelListAPIView (or other appropriate view classes) to ensure the queryset is automatically optimized based on the serializer's requirements.

    This integration automatically handles the necessary prefetch_related and annotate calls for you.

    import django_virtual_models as v
    
    class MovieSerializer(v.VirtualModelSerializer):
        ...
    
        class Meta:
            ...
            virtual_model = VirtualMovie
    
    class MovieList(v.VirtualModelListAPIView):
        queryset = Movie.objects.all()
        serializer_class = MovieSerializer
        ...