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.
  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
  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
  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”
  5. QuerySet Los QuerySet se pueden evaluar: • Iteración • Slicing

    • Pickling/Caching • repr() • len() • list() • Operación Booleana
  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
  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
  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!
  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)
  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
  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
  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!
  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!
  14. Proyecciones: Obtener lo necesario SELECT <projection> FROM <table> WHERE <conditions>;

    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 ❤!
  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!)
  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)
  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!
  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
  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()
  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
  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!
  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 …. =(!
  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
  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()
  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!
  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”
  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' } }
  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.