Usando Django MPTT em APIs REST
in Django on Django, Rest
Essa semana precisei fazer com que criasse uma simples feature para categorias e sub-categorias, mas para uma API REST. De forma convencional com Django usei o Django MPTT, e ele trás algumas boas features para serem persistidas e renderizadas com os templates do framework, mas aproveitei para fazer uns testes com o Django REST Framework. Assim, vamos aprender a preparar a serialização desse tipo de estrutura de dados, tanto para consulta como para persistência.
Criando o projeto
Vamos criar uma simples aplicação de cadastro de categorias e subcategorias, nada tão complexo. Assim vamos criar o projeto Django, e instalar as dependências necessárias:
(categories)$ pip install djangorestframework django-mptt
Configure para que sua aplicação use as bibliotecas:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mptt',
'rest_framework',
]
Feito isso, vamos criar a nossa aplicação.
(categories)$ python manage.py startapp core
(categories)$ mv core categories/
Registre essa nova aplicação, para iniciarmos o nosso desenvolvimento:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mptt',
'rest_framework',
'categories.core',
]
Preparando o modelo
Vamos criar um modelo chamado Category que irá representar o que precisamos:
Preparando o modelo
Vamos criar um modelo chamado Category que irá representar o que precisamos. Ele precisa herdar da classe MPTTModel. Veja a documentação para mais informações:
# -*- coding: utf-8 -*-
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
"""
Representa as categorias e
subcategorias do sistema
"""
name = models.CharField(max_length=80, unique=True)
parent = TreeForeignKey('self', null=True, blank=True,
related_name='children', db_index=True)
class MPTTMeta:
db_table = "categories"
order_insertion_by = ['name']
def __str__(self):
return self.name
O que fizemos é nada mais que criar um novo modelo. Ele possui o atributo name que representa o nome da categoria e o atributo parent que vai ser uma chave estrangeira do mesmo tipo de modelo, para associar as sub-categorias que são do mesmo tipo.
Para aplicar ao nosso banco, gere a migração e a execute:
(categories)$ python manage.py makemigrations
Migrations for 'core':
0001_initial.py:
- Create model Category
(categories)$ python manage.py migrate
Operations to perform:
Apply all migrations: contenttypes, auth, sessions, core, admin
Running migrations:
Rendering model states... DONE
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length...p OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null...y OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying core.0001_initial... OK
Applying sessions.0001_initial... OK
Criando serializer e views
Feito a nossa classe, vamos criar a classe serializer que vai tratar os dados vindos do usuário como provindas do servidor.
# -*- coding: utf-8 -*-
from rest_framework import serializers
from categories.core.models import Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ("id", "name",)
Vamos criar a view que vai listar e persistir as categorias e subcategorias da nossa API. Vamos usar os generics do Django REST.
# -*- coding: utf-8 -*-
from rest_framework import generics
from categories.core.serializers import CategorySerializer
from categories.core.models import Category
class CategoryView(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class CategoryDetailView(generics.RetrieveUpdateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
E depois crie uma endpoint para a nossa view em categories/urls.py:
# -*- coding: utf-8 -*-
from django.conf.urls import url
from django.contrib import admin
from categories.core.views import CategoryView, CategoryDetailView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^categories/(?P<pk>[0-9]+)$', CategoryDetailView.as_view()),
url(r'^categories/', CategoryView.as_view()),
]
Agora com tudo implementado e configurado, vamos rodar o servidor.
(categories)$ python manage.py runserver
Criando categoria e subcategoria
Vamos criar uma categoria. Vamos usar o httpie que é um belo CLI HTTP para fazer esses testes.
http POST http://localhost:8000/categories/ name="Category Bar"
HTTP/1.0 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 09 Jun 2016 15:29:38 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"id": 1,
"name": "Category Bar"
}
Agora vamos atualizar o nosso serializer para que ele possa aceitar subcategorias, mas de forma opcional, porque se criarmos outra categorias e tornar o children por padrão obrigatório conforme o Django REST Framework faz, vai gerar erro de validação.
# -*- coding: utf-8 -*-
from rest_framework import serializers
from categories.core.models import Category
class CategorySerializer(serializers.ModelSerializer):
children = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
many=True,
required=False
)
class Meta:
model = Category
fields = ("id", "name", "children",)
Vamos criar outra categoria que vai definir nosso registro anterior como subcategoria:
(categories) $ http POST http://localhost:8000/categories/ name="Category Foo" children:=[1]
HTTP/1.0 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 09 Jun 2016 15:39:09 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"children": [
1
],
"id": 2,
"name": "Category Foo"
}
Vendo na interface do Django REST temos isso:

Detalhando subcategorias
Vemos os filhos das categorias retornam somente a identificação do registro, e não é isso que queremos. Vamos fazer com que retorna além do id retorne também o seu nome.
# -*- coding: utf-8 -*-
from rest_framework import serializers
from categories.core.models import Category
class SubCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ("id", "name", "children",)
class CategorySerializer(serializers.ModelSerializer):
children = SubCategorySerializer(many=True, required=False)
class Meta:
model = Category
fields = ("id", "name", "children",)
Vendo na interface do Django REST temos isso:

Customizando serialização
Vamos supor que queremos mudar o nome children para subcategories. Podemos fazer dessa forma:
# -*- coding: utf-8 -*-
from rest_framework import serializers
from categories.core.models import Category
class SubCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ("id", "name", "children",)
class CategorySerializer(serializers.ModelSerializer):
subcategories = SubCategorySerializer(source="children", many=True, required=False)
class Meta:
model = Category
fields = ("id", "name", "subcategories",)
Vendo na interface do Django REST temos isso:

O problema que na nossa subcategoria, vemos que ele continua children já que não alteramos no serializer SubCategorySerializer. Você até poderia mudar, mas fica limitado a profundidade da estrutura, já que tiver mais de 20 níveis, teria que ficar mudando toda hora, e isso não é bom.
Criando um novo field
O que podemos fazer é criar um field chamado de RecursiveField em que ele vai serializar a partir do serializer do parente. Assim pode ter quandos níveis quiser, que ele vai fazer isso e fora que podemos eliminar o SubCategorySerializer, e mudar o nome do campo sem repetições.
# -*- coding: utf-8 -*-
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from categories.core.models import Category
class RecursiveField(serializers.BaseSerializer):
"""
Cria instancia do serializer parente e retorna os dados
serializados.
"""
def to_representation(self, value):
ParentSerializer = self.parent.parent.__class__
serializer = ParentSerializer(value, context=self.context)
return serializer.data
def to_internal_value(self, data):
ParentSerializer = self.parent.parent.__class__
Model = ParentSerializer.Meta.model
try:
instance = Model.objects.get(pk=data)
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Objeto {0} não encontrado".format(
Model().__class__.__name__
)
)
return instance
class CategorySerializer(serializers.ModelSerializer):
subcategories = RecursiveField(source="children",
many=True, required=False)
class Meta:
model = Category
fields = ("id", "name", "subcategories",)
Vendo na interface do Django REST temos isso:

Inserindo subcategorias
Vamos criar uma nova categoria, em que ela vai estar associada a nossa categoria Category Bar.
(categories)$ http POST http://localhost:8000/categories/ name="Category Foobar"
HTTP/1.0 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 09 Jun 2016 16:13:16 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"id": 3,
"name": "Category Foobar",
"subcategories": []
}
(categories)$ http PUT http://localhost:5000/categories/1 subcategories:=[5]
HTTP/1.0 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 09 Jun 2016 18:02:20 GMT
Server: WSGIServer/0.2 CPython/3.5.1
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"id": 1,
"name": "Category Bar",
"subcategories": [
{
"id": 5,
"name": "Category Foobar",
"subcategories": []
}
]
}
Vendo na interface do Django REST temos isso:

Vendo agora que temos três nívels na nossa hierarquia, o campo que criamos para o serializer evita repetição de código.
Refatorando nossa consulta
Agora que já implementamos nossos endpoints, vamos refatorar a consulta das categorias, já que ele deve retornar da forma correta, que é a partir das categorias-pai, e encadeado pelos filhos.
# -*- coding: utf-8 -*-
from rest_framework import generics
from categories.core.serializers import CategorySerializer
from categories.core.models import Category
class CategoryView(generics.ListCreateAPIView):
queryset = Category.objects.root_nodes()
serializer_class = CategorySerializer
class CategoryDetailView(generics.RetrieveUpdateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
O django-mptt oferece um manager próprio, que possui os tipos de consutas que precisamos. Aqui usamos o root_nodes() para listar as categorias retornando somente as categorias principais, que são base para as subcategorias.
Vendo na interface do Django REST temos isso:

Solução Alternativa
Acima é uma solução em que pode fazer as customização que necessitar, mas se não vai passar disso, recomendo que use o django-rest-framework-recursive que oferece uma serialização recursiva. Além da serialização no estilo de árvore, oferece alguns outros.
Assim vamos refatorar a solução que fizemos, deixando com menos código. Primeiro vamos instalar e configurar.
(categories)$ pip install djangorestframework-recursive
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mptt',
'rest_framework',
'rest_framework_recursive',
'categories.core',
]
Vamos refatorar o nosso serializers.py para usar o campo recursivo.
# -*- coding: utf-8 -*-
from rest_framework import serializers
from rest_framework_recursive.fields import RecursiveField
from categories.core.models import Category
class CategorySerializer(serializers.ModelSerializer):
subcategories = serializers.ListSerializer(source="children",
child=RecursiveField())
class Meta:
model = Category
fields = ("id", "name", "subcategories",)
Vendo na interface do Django REST temos isso:

Conclusão
Assim, vemos que não é tão complicado tratar esse tipo de estrutura de dados com o Django REST e o Django MPTT. Com o app que citei anteriormente, ele lista perfeitamente, mas no caso de persistência encontrei alguns problemas na validação, assim quando encontrar a solução irei atualizar esse post.
Repositório do projeto do post: https://github.com/gilsondevlabs/categories-api-rest
Espero que tenha gostado. Até mais!