Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django ORM to win!

Django ORM to win!

Veremos como se comporta el ORM de Django con bases de datos grandes.
Para esto nos focalizaremos en como funciona el ORM de Django, describir sus componentes y funcionalidades con el fin de generar consultas mas precisas y complejas para disminuir problemas de performance.
Se explicará el funcionamiento de los QuerySet y su propiedad de ser "vagos" (laziness).
Tambien se discutirá sobre el uso de "managers" personalizados y consejos de escalabilidad con Django como el uso de multiples bases de datos.

Martin Alderete

November 19, 2017
Tweet

More Decks by Martin Alderete

Other Decks in Programming

Transcript

  1. Django ORM to win!
    Recomendaciones y otras yerbas!
    Martin Alderete
    @alderetemartin
    This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

    View full-size slide

  2. Django ORM partes interesantes
    ● Manager → Interfaz contra la base de datos (.objects)
    ● QuerySet → Clase base (filter(), all())
    ● ValuesQuerySet → Subclase que retorna dicts cuando se evalua
    ● ValuesListQuerySet → Subclase que retorna tuples cuando se evalua

    View full-size slide

  3. Django ORM query “compleja”
    MyModel.objects.filter(*args_filters, **kwargs_filters)
    MyModel.objects.filter(attr1__in=x, attr3__gt=z) # attri1 AND attr3
    from django.db.models import Q
    expression2 = Q(attr1__in=x) | Q(attr3=xxx)
    MyModel.objects.filter(expression2, attr5=x,) # (attr1 OR attr3) AND attr5

    View full-size slide

  4. QuerySet los “Vagos”
    Los “QuerySet” son instancias con la propiedad de ser “lazy” (vagos)
    No implican un “hit” a la base de datos
    Se pueden encadenar
    No realiza el “query” hasta que le preguntemos su valor
    Guardan resultado en un cache interno
    Permiten visualizar el SQL por medio del atributo “query”

    View full-size slide

  5. QuerySet
    Los QuerySet se pueden evaluar:
    ● Iteración
    ● Slicing
    ● Pickling/Caching
    ● repr()
    ● len()
    ● list()
    ● Operación Booleana

    View full-size slide

  6. QuerySet “anti-patterns”
    queryset = MyModel.objects.filter(*args_filters, **kwargs_filters)
    len(queryset) → Forzamos la evaluación e iteración para un len()
    if queryset: → Forzamos la evaluación por una operación booleana
    # code
    for obj in huge_queryset: → La memoria se puede ir por las nubes!
    #code

    View full-size slide

  7. QuerySet “anti-patterns” mejor!
    queryset = MyModel.objects.filter(**f)
    queryset.count() → Resolver las cantidades en base de datos! (COUNT())
    if queryset.exists(): → Resolver la existencia en base de datos! (“LIVIANO”)
    # code
    for obj in huge_queryset.iterator(): → Mejora el uso de memoria elimina el cache!
    #code

    View full-size slide

  8. QuerySet cache
    “Los QuerySet tienen un cache para minimizar el acceso a la base hay que
    aprovecharlo”
    plate_numbers = [car.plate_number for car in Car.objects.filter(**kwargs)] # Hit db!
    owners = [car.owner for car in Car.objects.filter(**kwargs)] # Hit db!
    qs = Car.objects.filter(**kwargs) # Lazy aun no evaluado!
    plate_numbers = [car.plate_number for car in qs] # Hit db!
    owners = [car.owner for car in qs] # Ho hay Hit usa el cache!

    View full-size slide

  9. Relaciones: “fetch”
    “Django automáticamente NO sigue la relaciones de un modelo”
    Ejemplo:
    cars_qs = Car.objects.filter(**kwargs)
    for car in cars_qs:
    print(car.owner.full_name)
    Cantidad de queries = n + 1 (n: número de cars)

    View full-size slide

  10. Relaciones: Controlar la cantidad de queries
    “Si al hacer obtener un modelo necesitamos sus relaciones debemos controlar la
    cantidad de queries”
    cars_qs = Car.objects.select_related(“owner”).filter(**kwargs)
    for car in cars_qs:
    print(car.owner.full_name) # No hay hit a la DB
    Cantidad de queries = 1

    View full-size slide

  11. Relaciones: Controlar la cantidad de queries
    “Si al hacer obtener un modelo necesitamos sus relaciones debemos controlar la
    cantidad de queries”
    cars_qs = Car.objects.prefetch_related(“vendors”).filter(**kwargs)
    for car in cars_qs:
    for vendor in car.vendors.all():
    print(vendor.name) # No hay hit a la DB
    Cantidad de queries = 2

    View full-size slide

  12. Relaciones: Controlar la cantidad de queries
    select_related() → Genera un JOIN en SQL
    ForeignKey, OneToOne
    Relaciones de Relaciones
    Car.objects.select_related(“owner__profile”)
    prefetch_related() → Una consulta por relación y hace un “JOIN” en memoria
    ManyToMany
    Relaciones de Relaciones (Agrega 1 query cada nivel)
    Company.objects.prefetch_related(“cars__vendors”)
    Reducir/Controlar la cantidad de queries nos da un BOOST de performance!

    View full-size slide

  13. Relaciones: Summary
    Debemos conocer y controlar nuestras consultas!
    select_related() Minimiza las consultas pero requiere consultas más caras en
    memoria para el motor! Generalmente es un BOOST!
    prefetch_related() Minimiza las consultas pero requiere mayor esfuerzo en Python!
    Generalmente es un BOOST para M2M!
    Evaluar los trade-off!

    View full-size slide

  14. Proyecciones: Obtener lo necesario
    SELECT FROM WHERE ;
    SELECT plate_number FROM cars;
    Manipular la “proyección” nos da un BOOST porque:
    Genera consultas más específicas en el motor
    Potencialmente no necesitamos la “traducción” del ORM
    Quiero eso en Django ❤!

    View full-size slide

  15. Proyecciones: Obtener lo necesario (dict/tuple)
    value_qs = Car.objects.filter(**kwargs).values(“plate_number”)
    ValuesQuerySet: QuerySet basado en dicts.
    valuelist_qs = Car.objects.filter(**kwargs).values_list(“plate_number”)
    ValuesListQuerySet: QuerySet basado en tuples.
    Generan consultas más específicas y al retornar dicts/tuples no necesitan crear
    instancias, no agregan ningún overhead en Python (chau ORM!)

    View full-size slide

  16. qs = Car.objects.filter(**kwargs).defer(“plate_number”)
    qs = Car.objects.filter(**kwargs).only(“plate_number”)
    Generan consultas más específicas pero deben instanciar, los fields se cargan de
    forma “lazy” dependiendo el caso. Al ser instancias podemos utilizar sus métodos.
    Tiene un leve overhead debido a que el ORM debe instanciar.
    save() ignore los fields que fueron defered
    Proyecciones: Obtener lo necesario (instancias)

    View full-size slide

  17. Ganar Performance al modificar (bulk)
    Cuando queremos hacer un “update” muchas veces hacemos “fetch” y ”save”, sin
    embargo lo que queremos es un UPDATE de SQL.
    qs = Car.objects.filter(**kwargs)
    for car in qs:
    car.status = INACTIVE
    car.save()
    OJO las Signals! pre_save/post_save
    qs = Car.objects.filter(**kwargs)
    qs.update(status=INACTIVE) F() Permiten tomar el valor de la DB!

    View full-size slide

  18. Ganar Performance al crear (bulk)
    Cuando queremos hacer un “create” muchas veces hacemos múltiples ”save”, sin
    embargo lo que queremos es un BULK INSERT de SQL.
    for d in data:
    car = Car(**d)
    car.save()
    cars = [Car(**d) for d in data]
    Car.objects.bulk_create(cars) OJO las Signals! pre_save/post_save

    View full-size slide

  19. Ganar Performance al eliminar (bulk)
    Cuando queremos hacer un “delete” muchas veces hacemos “fetch” y ”delete”, sin
    embargo lo que queremos es un DELETE de SQL.
    qs = Car.objects.filter(**kwargs)
    for car in qs:
    car.delete()
    pre_delete y post_delete se llaman!
    Car.objects.filter(**kwargs).delete()

    View full-size slide

  20. Managers: Lógica encapsulada
    Los Managers son la interface que tenemos en el ORM para interactuar con la DB.
    Todos los modelos tiene al menos un Manager (“objects”).
    Generalmente creamos Manager personalizados para:
    - Customizar el queryset inicial
    - Agregar métodos.
    - Acciones a nivel de tabla

    View full-size slide

  21. Managers: Customizar el QuerySet inicial
    class LogicalDeleteManager(models.Manager):
    def get_queryset(self):
    # queryset instance of models.QuerySet
    queryset = super().get_queryset()
    queryset = queryset.filter(removed=False)
    return queryset
    Todas las queries que hagamos usando este manager no importa de donde,
    encapsulan la misma lógica. DRY BABY!

    View full-size slide

  22. Managers: Agregar metodos ( Django < 1.7)
    class CustomManager(models.Manager):
    def available_for_user(self, user):
    condition = Q(share=True) | Q(owner=user)
    queryset = self.filter(condition)
    return queryset
    MyModel.objects.available_for_user(u)
    MyModel.objects.available_for_user(u).other_custom_method() -> AttributeError
    No permite encadenar llamadas! Para eso debemos crear un CustomQuerySet con
    la misma API que el Manager …. =(!

    View full-size slide

  23. Managers: Agregar metodos ( Django < 1.7)
    class CustomQuerySet(models.QuerySet):
    def available_for_user(self, user):
    condition = Q(share=True) | Q(owner=user)
    queryset = self.filter(condition)
    return queryset
    class CustomManager(models.Manager):
    def get_queryset(self):
    return CustomQuerySet(self.model, using=self._db)
    def available_for_user(self, user):
    queryset = self.get_queryset().available_for_user(user)
    return queryset

    View full-size slide

  24. Managers: Agregar metodos ( Django >= 1.7)
    class CustomQuerySet(models.QuerySet):
    def available_for_user(self, user):
    condition = Q(share=True) | Q(owner=user)
    queryset = self.filter(condition)
    return queryset
    class MyModel(models.Model):
    objects = managers.Manager()
    custom_manager = CustomQuerySet.as_manager()
    MyModel.objects.available_for_user(u).other_custom_method()

    View full-size slide

  25. Extra: Mysql + get_or_create() ~ Race-Condition
    MySQL por default utiliza el ISOLATION Level “REPEATABLE-READ” lo cual rompe
    el algoritmo que usa Django con “get_or_create” (READ-COMMITED) cuando
    empezamos a tener concurrencia esto puede ser un dolor de cabeza!

    View full-size slide

  26. Extra: Mysql + get_or_create() ~ Race-Condition
    REPEATABLE-READ: “Al iniciar una transacción hace un snapshot del conjunto y
    futuras lecturas del mismo conjunto darán los mismos resultados”

    View full-size slide

  27. Extra: Mysql + get_or_create() ~ Race-Condition
    La forma “más” segura de corregir esto es en el settings, asi no dependemos de la
    configuración del MOTOR.
    DATABASES = {
    ‘default’: {
    # mas cosas
    'OPTIONS' : {
    'init_command': 'SET SESSION TRANSACTION ISOLATION LEVEL READ
    COMMITTED'
    }
    }

    View full-size slide

  28. “Conclusiones”
    ● Usen PostgreSQL
    ● Conocer nuestras queries. (Django Debug Toolbar)
    ● Lamentablemente debemos saber SQL y ser capaces de analizarlo.
    ● Utilizar queryset.query para entender que estamos haciendo.
    ● Utilizar EXPLAIN para ver cómo se comporta el motor (Lea su doc).
    ● Probar estrategias de “fetch” / Optimizar las proyecciones.
    ● No solo existe filter() (managers, queryset).
    ● Aprovechar al máximo que los QuerySet son “vagos” y posponer la
    evaluación.
    ● Utilizar operaciones en bulk (disminuye el overhead).
    ● Los manager son excelentes y ayudan a NO repetir código.
    ● Los managers permiten compartir lógica por TODO django de forma segura.

    View full-size slide

  29. while not manos.dormidas:
    publico.aplaudir()
    orador.decir(‘Gracias PyconAR’)
    Muchas Gracias!
    ¿Preguntas?
    Martin Alderete
    @alderetemartin

    View full-size slide