Saiba como organizar os dados em uma hierarquia usando django-mptt em um ambiente que usa API 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. 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!