Django, el framework para perfeccionistas con deadlines …

Entradas

django-testing

Testing de modelos en Django, buenas prácticas.

El testing es una de las partes más importantes de cualquier proyecto de software, ya que aporta calidad y seguridad a nuestro código.  En esta entrada voy a tratar de exponer algunas prácticas muy recomendables para testear modelos en Django de forma eficiente y segura.

Leer más

django-users

Cómo extender el User de Django

El sistema de usuarios y autenticación integrado en Django es muy completo,  lo podemos adaptar a la mayoria de los casos de uso, pero hay situaciones en las que no podemos utilizarlo “out-of-the-box”. Por ejemplo, si nuestro proyecto tuviese carácter social es probable que necesitásemos almacenar una pequeña biografía, la localización del usuario y alguna otra información de interés. En esta entrada veremos algunas estrategias para extender el User Model que nos proporciona Django según nos convenga.

Leer más

Django-rest

Optimizando Django REST Framework

Django REST Framework es una aplicación Django que permite desarrollar de forma simple APIs muy robustas. Sin embargo, los serializadores propios de la aplicación pueden ser un “kill performance” para nuestro sistema. La raíz del problema es el denominado “N+1 selects problem”, este se da al acceder a un objeto concreto y sus a N objetos relacionados, normalmente a través de claves foráneas. Con pocos datos, este problema no produce un deterioro significativo del rendimiento, pero cuando el número de objetos relacionados crece, el rendimiento del sistema cae en picado.
Para mejorar el rendimiento de DRF haremos uso de los infrautilizados métodos del ORM de Django, “select_related” y “prefetch_related”, lo que nos permitirá realizar un “eager loading” (carga ansiosa, o evaluación ansiosa) de la información.

Descripción del problema

Cuando construimos una vista de DRF a menudo incluimos los datos de más de una tabla relacionada, desafortunadamente desde que hacemos uso del serializador de DRF para dicha vista, corremos el riesgo de aplastar el rendimiento de nuestra API.

Veamos un ejemplo:

class CustomerSerializer(serializers.ModelSerializer):
    order_descriptions = serializers.StringRelatedField(many=True) 
    orders = OrderSerializer(many=True, read_only=True)

 

El serializador anterior hace lo siguiente:

  1. Recupera todos los “customers”, realiza una consulta a la base de datos.
  2. Para el primer “customer” obtenido, recupera sus “orders”. Realiza otra consulta a la base de datos.
  3. Para el segundo “customer” obtenido, recupera sus “orders”. Vuelve a realizar otra consulta a la base de datos.
  4. Se realizarán tantas consultas como “customers” tenga en la base de datos.

Desde que “OrderSerializer” tenga otra relación anidada las consultas aumentarán drásticamente. Para que nos hagamos una idea, una sitio web pequeño empieza a tener problemas a partir de las 50 peticiones a la base de datos.

Planteamiento de la solución

Nuestro enfoque para solucionar este problema se denomina evaluación ansiosa (eager loading). Esencialmente, lo que hacemos es indicar a Django que vamos a realizar la consulta repetidamente.

queryset = queryset.prefetch_related('orders')

 

Ahora el serializador anterior simplemente haría los siguiente:

  1. Obtendría todos los “customers”, solo haría dos peticiones a la base de datos, uno para recuperar los customers y otro para recuperar todos los orders relacionados.
  2. En esta ocasión para cada uno de los customers obtenidos no tendría que realizar consultas extras para obtener cada uno de sus orders relacionados.

Vamos a establecer un patrón común, para  ello cada vez que construyamos un serializador le añadiremos un método estático llamado “setup_eager_loading”:

class CustomerSerializer(serializers.ModelSerializer):
    orders = OrderSerializer(many=True, read_only=True)

    def setup_eager_loading(cls, queryset):
        queryset = queryset.prefetch_related('orders')
        return queryset

 

Cada vez que vayamos a usar el serializador llamaremos al metodo “setup_eager_loading” en el “Queryset” antes de evaluar el serializador:

customers = Customers.objects.all()
customer_qs = CustomerSerializer.setup_eager_loading(customers)  # Set up eager loading to avoid N+1 selects
post_data = CustomerSerializer(customer_qs, many=True).data

 

O, si tenemos un APIView o un ViewSet, lo podremos llamar en el metodo “get_queryset”:

def get_queryset(self):
    queryset = Customers.objects.all()
    # Set up eager loading to avoid N+1 selects
    queryset = self.get_serializer_class().setup_eager_loading(queryset)  
    return queryset

 

En resumen, el ORM de Django evalua ansiosamente los datos requeridos para obtener los “customers” y los “orders” relacionados y los ha almacena en caché. Acceder a los datos de dicha caché es prácticamente instantáneo en comparación con realizar una petición a la base de datos.

Veamos otro ejemplo, vamos a optimizar los problemas de rendimiento de un sitio web de organización de eventos. La estructura de datos sería la siguiente:

from django.contrib.auth.models import User

class Event:
    """ A single occasion that has many `attendees` from a number of organizations."""
    creator = models.ForeignKey(User)
    name = models.TextField()
    event_date = models.DateTimeField()

class Attendee:
    """ A party-goer who (usually) represents an `organization`, who may attend many `events`."""
    events = models.ManyToManyField(Event, related_name='attendees')
    organization = models.ForeignKey(Organization, null=True)

class Organization:
    name = models.TextField()

 

Nuestro serializador de eventos quedaría tal que así:

class EventSerializer(serializers.ModelSerializer):
    creator = serializers.StringRelatedField()
    attendees = AttendeeSerializer(many=True)
    unaffiliated_attendees = AttendeeSerializer(many=True)

    @staticmethod
    def setup_eager_loading(queryset):
        """ Perform necessary eager loading of data. """
        # select_related for "to-one" relationships
        queryset = queryset.select_related('creator')

        # prefetch_related for "to-many" relationships
        queryset = queryset.prefetch_related(
            'attendees',
            'attendees__organization')

        # Prefetch for subsets of relationships
        queryset = queryset.prefetch_related(
            Prefetch('unaffiliated_attendees', 
                queryset=Attendee.objects.filter(organization__isnull=True))
            )
        return queryset

 

Llamar a “setup_eager_loading” antes de usar “EventSerializer”, nos garantiza que sólo tendremos dos grandes consultas en lugar de N + 1 consultas pequeñas, con lo cual el rendimiento será drásticamente mejor.

Referencias: