Django ORM to win!

Django ORM to win!

This presentation shows how the Django ORM behave mainly when you have big databases, in order to do that we are going to focus on how the ORM works, describe its main components and features to generate better and complex queries.
Also custom Manager will be showed.

93eb065dded558598bd4562c122e9048?s=128

Martin Alderete

November 13, 2015
Tweet

Transcript

  1. Django ORM at Scale to win! Recomendaciones y otras yerbas!

    Martin Alderete @alderetemartin martin.a@devsar.com
  2. 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 Permiten visualizar el SQL por medio del atributo “query”
  3. QuerySet Los QuerySet se pueden evaluar: • Iteración • Slicing

    • Pickling/Caching • repr() • len() • list() • Operación Booleana
  4. QuerySet • QuerySet → Clase base (filter(), all()) • ValuesQuerySet

    → Subclase que retorna dicts cuando se evalua • ValuesListQuerySet → Subclase que retorna tuples cuando se evalua
  5. QuerySet “anti-patterns” queryset = MyModel.objects.filter(**f) len(queryset) → Forzamos la evaluación

    e iteración para un len() if queryset: → Forzamos la evaluacion por una operacion booleana # code for obj in huge_queryset: → La memoria se va por las nubes! #code
  6. 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
  7. 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 = O(n) + 1 (n: número de cars)
  8. 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
  9. 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
  10. Relaciones: Controlar la cantidad de queries select_related() → Genera un

    JOIN en SQL ForeignKey, OneToOne Relaciones de Relaciones 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!
  11. 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!
  12. 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!
  13. 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 instanciar , no agregan ningun overhead en Python (chau ORM!)
  14. Proyecciones: Obtener lo necesario (instancias) qs = Car.objects.filter(**kwargs).only(“plate_number”) qs =

    Car.objects.filter(**kwargs).defer(“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.
  15. 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() qs = Car.objects.filter(**kwargs) qs.update(status=INACTIVE) F() Permiten tomar el valor de la DB!
  16. 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!
  17. Managers: Logica encapsulada Los Managers son la interface que tenemos

    en el ORM para interactuar con la DB. Todos los modelos tiene al menos un Manager. Podemos crear Manager personalizados para: Customizar el queryset inicial Agregar métodos
  18. Managers: Customizar el QuerySet class LogicalDeleteManager(models.Manager): def get_queryset(self): # queryset

    instance of models.QuerySet queryset = super(LogicalDeleteManager, self).get_queryset() queryset = queryset.filter(removed=False) return queryset Todas las queries que hagamos usando este manager no importa de donde encapsulan la misma logica!! DRY BABY!
  19. 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) No permite encadenar llamadas! Para eso debemos crear un CustomQuerySet con la misma API que el Manager…. =(!
  20. 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
  21. 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 = CustomQuerySet.as_manager() MyModel.objects.available_for_user(u).other_custom_method()
  22. 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!
  23. Extra: Mysql + get_or_create() ~ Race-Condition REPEATABLE-READ: “Al iniciar una

    transaccion hace un snapshot del conjunto y futuras lecturas del mismo conjunto darán los mismos resultados”
  24. Extra: Mysql + get_or_create() ~ Race-Condition La forma “más” segura

    de corregir esto es en el settings, asi no dependemos de la configuracion del MOTOR. DATABASES = { ‘default’: { # mas cosas 'OPTIONS' : { 'init_command': 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED' } }
  25. “Conclusiones” • Conocer nuestras queries. • 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.
  26. Muchas Gracias! ¿Preguntas? Martin Alderete @alderetemartin martin.a@devsar.com