41º PUG-PE

Utilizando o ORM do Django de Maneira Elegante



Bernardo Fontes


Recife/PE

28 de Novembro de 2015

Olar!

twitter.com/bbfontes

github.com/berinhard

garimpo.fm

pessoas.cc

bernardoxhc@gmail.com

berinhard.github.io/talks/


slideshare.net/bernardofontes


I <3

#pyselfie

## Todos nós **amamos** o ORM do Django. - ### Certo?
## Super-poderes do ORM - ### Agnóstico à **tecnologia infra** - ### Fornece uma **API simples** - ### Conexões e semânticas do **banco invisíveis**
## Responsabilidades básicas ORM - ### Fornecer **entidades únicas** representando entradas no banco - ### Viabilizar operações em **coleções de objetos** (select, update, delete)
## API de Objetos ```python >>> from django.contrib.auth.models import User >>> user = User.objects.get(pk=42) >>> user.set_password('morena_tropicana') >>> user.save() ```
## API de Objetos ```python from django.contrib.auth.hashers import make_password class User(models.Model): # campos do model def set_password(self, raw_password): self.password = make_password(raw_password) self._password = raw_password ```
## Método `set_password` - ### Semântica de **domínio** (atualizar senha) - ### **Encapsula** implementação do objeto user - ### APIs clientes **menos verbosas** - ### Código mais limpo, **testável** e **reusável**
## Por que não fazemos o mesmo com **coleções**?
## Nossa TODO List ```python from django.db import models PRIORITY_CHOICES = [(1, 'High'), (2, 'Low')] class Todo(models.Model): content = models.CharField(max_length=100) is_done = models.BooleanField(default=False) owner = models.ForeignKey('auth.User') priority = models.IntegerField(choices=PRIORITY_CHOICES, default=1) ```
## Um exemplo de uso ```python def dashboard(request): todos = Todo.objects.filter( owner=request.user is_done=False priority=1 ) ctx = {'todos': todos} return render(request, 'todos/list.html', ctx) ``` - ### Problemas?
## Problemas - ### Implementação de `Todo` **vazando** - ### Consultas mais **verbosas** - ### **Repetição de queries** espalhadas pelo sistema - ### Manutenação/alteração são mais **custosas** - ### Sem **semântica** (o que é priority=1?) - ### Necessidade de testar com o banco configurado corretamente
## Solução: Queries com **semântica de domínio**
## ***Send messages, not objects*** ## Alan Kay - ou algo bem parecido com isso
## Caminhos no ORM do Django - ## Custom **Model Managers** - ## Custom **QuerySets**
## Model Managers - ## **Interface** para realizar queries a partir de Models - ## `objects` é sempre o **manager padrão**
## QuerySets - ## É uma **coleção de objetos** recuperados do banco - ## Lazy-evaluation - ## **Fluent Interface**: Permite chamadas encadeadas. Ex: all().count()
### QuerySet ```python class QuerySet(object): def count(self): """ Performs a SELECT COUNT() and returns the number of records as an integer. If the QuerySet is already fully cached this simply returns the length of the cached results set to avoid multiple SELECT COUNT(*) calls. """ if self._result_cache is not None: return len(self._result_cache) return self.query.get_count(using=self.db) # e vai embora... ```
### Model Manager até 1.6 ```python class Manager(object): def get_query_set(self): return QuerySet(self.model, using=self._db) def all(self): return self.get_query_set() def count(self): return self.get_query_set().count() def filter(self, *args, **kwargs): return self.get_query_set().filter(*args, **kwargs) # e vai embora... ```
### 1ª Abordagem: Múltiplos Managers ```python class IncompleteTodoManager(models.Manager): def get_query_set(self): return super(TodoManager, self).get_query_set().filter(is_done=False) class HighPriorityTodoManager(models.Manager): def get_query_set(self): return super(TodoManager, self).get_query_set().filter(priority=1) class Todo(models.Model): objects = models.Manager() # manager padrão para manter API incomplete = models.IncompleteTodoManager() high_priority = models.HighPriorityTodoManager() # Campos na tabela ``` ```python >>> Todo.incomplete.all() >>> Todo.high_priority.all() ```
## Problemas da 1ª abordagem - ### **Verboso**: implementamos 2 classes e configuramos o model 3 vezes - ### **Sem Chain Calls**: não conseguiríamos filtrar por tarefas de alta prioridade **e** incompletas - ### **Related Manager**: se o model for utilizado em relacionamentos com outro models, os métodos não estarão visíveis
### 2ª Abordagem: Métodos no Manager ```python class TodoManager(models.Manager): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class Todo(models.Model): objects = TodoManager() # Campos na tabela ``` ```python >>> Todo.objects.incomplete() >>> Todo.objects.high_priority() ```
## Problemas da 2ª abordagem - ### **Sem Chain Calls**: não conseguiríamos filtrar por tarefas de alta prioridade **e** incompletas
### 3ª Abordagem: Custom QuerySet ```python class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class TodoManager(models.Manager): def get_query_set(self): return TodoQuerySet(self.model, using=self._db) class Todo(models.Model): objects = TodoManager() # Campos na tabela ``` ```python >>> Todo.objects.all().incomplete() >>> Todo.objects.all().high_priority() ```
## Problemas da 3ª abordagem - ### **Entry points**: não conseguiríamos filtrar inicialmente invocando os métodos do queryset - ### Maaaas, tem como resolver
## 3.1: Resolvendo na mão ```python class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class TodoManager(models.Manager): def get_query_set(self): return TodoQuerySet(self.model, using=self._db) def incomplete(self): return self.get_query_set().incomplete() def high_priority(self): return self.get_query_set().high_priority() ``` ```python >>> Todo.objects.incomplete().high_priority() >>> Todo.objects.high_priority().incomplete() ```
## 3.2: Django < 1.7 ``` $ pip install django-model-utils ``` ```python from model_utils.managers import PassThroughManager class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = PassThroughManager.for_queryset_class(TodoQuerySet)() ```
## 3.3: Django >= 1.7 ```python class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = TodoQuerySet.as_manager() ```
## Resumo - ### Utilizar o ORM puro do Django para consultas é difícil de gerenciar - ### É importante utilizarmos a semântica do domínio inclusive nas queries - ### Managers são utilizados para entry-points customizados - ### QuerySets geram APIs mais elegantes por serem mais fluentes

Obrigado!

Bernardo Fontes

twitter.com/bbfontes

github.com/berinhard

garimpo.fm

pessoas.cc

bernardoxhc@gmail.com