41º PUG-PE
Utilizando o ORM do Django de Maneira Elegante
Bernardo Fontes
Recife/PE
28 de Novembro de 2015
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