O roteamento em Django da segunda versão do framework recebeu uma ferramenta maravilhosa - conversores. Com a adição desta ferramenta, tornou-se possível não só configurar com flexibilidade os parâmetros nas rotas, mas também separar as áreas de responsabilidade dos componentes.
Meu nome é Alexander Ivanov, sou um mentor no Yandex.Practicum na faculdade de desenvolvimento de back- end e um desenvolvedor líder no Laboratório de Modelagem de Computadores. Neste artigo, vou guiá-lo pelos conversores de rota do Django e mostrar os benefícios de usá-los. A primeira coisa para começar são os limites de aplicabilidade:
- Django versão 2.0+;
- o registro das rotas deve ser feito com
django.urls.path
.
Portanto, quando uma solicitação chega ao servidor Django, ela primeiro passa pela cadeia de middleware e, em seguida, o URLResolver ( algoritmo ) é ativado . A tarefa deste último é encontrar um adequado na lista de rotas cadastradas.
Para uma análise substantiva, proponho considerar a seguinte situação: existem vários endpoints que devem gerar relatórios diferentes para uma determinada data. Vamos supor que os endpoints sejam assim:
users/21/reports/2021-01-31/
teams/4/reports/2021-01-31/
Quais seriam as rotas
urls.py
? Por exemplo, assim:
path('users/<id>/reports/<date>/', user_report, name='user_report'),
path('teams/<id>/reports/<date>/', team_report, name='team_report'),
Cada item em
< >
é um parâmetro de solicitação e será passado para o manipulador.
Importante: o nome do parâmetro ao registrar a rota e o nome do parâmetro no manipulador devem corresponder.
Então, cada manipulador teria algo assim (preste atenção às anotações de tipo):
def user_report(request, id: str, date: str):
try:
id = int(id)
date = datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise Http404()
# ...
Mas este não é um negócio real - copiar e colar tal bloco de código para cada manipulador. É razoável mover este código para uma função auxiliar:
def validate_params(id: str, date: str) -> (int, datetime):
try:
id = int(id)
date = datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise Http404('Not found')
return id, date
E em cada manipulador, haverá uma chamada simples para esta função auxiliar:
def user_report(request, id: str, date: str):
id, date = validate_params(id, date)
# ...
Em geral, isso já é digerível. A função auxiliar irá retornar os parâmetros corretos dos tipos necessários ou abortar o manipulador. Tudo parece estar bem.
Mas, na verdade, aqui está o que eu fiz: mudei parte da responsabilidade de decidir se esse manipulador deve ser executado para esta rota ou não, do URLResolver para o próprio manipulador. Acontece que o URLResolver fez seu trabalho mal, e meus gerenciadores não só precisam fazer um trabalho útil, mas também decidir se devem fazê-lo. Esta é uma violação clara do princípio SOLID de responsabilidade exclusiva . Isso não vai funcionar. Precisamos melhorar.
Conversores padrão
Django fornece conversores de rota padrão . É um mecanismo para determinar se uma parte da rota é apropriada ou não pelo próprio URLResolver. Um bom bônus: o conversor pode alterar o tipo do parâmetro, o que significa que o tipo de que precisamos pode vir imediatamente para o manipulador, e não para a string.
Os conversores são especificados antes do nome do parâmetro na rota, separados por dois pontos. Na verdade, todos os parâmetros têm um conversor; se não for especificado explicitamente, o conversor será usado por padrão
str
.
Cuidado: alguns conversores parecem tipos em Python, então pode parecer que são casts normais, mas não são - por exemplo, não há conversores padrãofloat
oubool
. Mais tarde, vou mostrar o que é um conversor.
Depois de observar os conversores padrão, fica óbvio para que
id
usar o conversor
int
:
path('users/<int:id>/reports/<date>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date>/', team_report, name='team_report'),
Mas e a data? Não existe um conversor padrão para ele.
Você pode, é claro, se esquivar e fazer isso:
'users/<int:id>/reports/<int:year>-<int:month>-<int:day>/'
Na verdade, alguns dos problemas foram eliminados, pois agora é garantido que a data será exibida em três números separados por hífens. No entanto, você ainda terá que lidar com as pastas de problemas no manipulador se o cliente enviar uma data incorreta, por exemplo, 2021-02-29 ou 100-100-100 em geral. Isso significa que essa opção não é adequada.
Nós criamos nosso próprio conversor
O Django, além dos conversores padrão, oferece a capacidade de criar seu próprio conversor e descrever as regras de conversão como você quiser.
Para fazer isso, você precisa seguir duas etapas:
- Descreva a classe do conversor.
- Registre o conversor.
Uma classe conversora é uma classe com um certo conjunto de atributos e métodos descritos na documentação (em minha opinião, é um pouco estranho que os desenvolvedores não tenham feito uma classe abstrata base). Os próprios requisitos:
- Deve haver um atributo que
regex
descreve a expressão regular para encontrar rapidamente a subsequência necessária. Vou mostrar como ele é usado mais tarde. - Implemente um método
def to_python(self, value: str)
para converter de uma string (afinal, a rota transmitida é sempre uma string) em um objeto Python, que será passado ao manipulador. - Implemente um método
def to_url(self, value) -> str
para converter de volta de um objeto Python em uma string (usado ao invocardjango.urls.reverse
ou marcarurl
).
A classe para converter a data ficará assim:
class DateConverter:
regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
def to_python(self, value: str) -> datetime:
return datetime.strptime(value, '%Y-%m-%d')
def to_url(self, value: datetime) -> str:
return value.strftime('%Y-%m-%d')
Sou contra a duplicação, por isso colocarei o formato da data em um atributo - é mais fácil manter o conversor se de repente eu quiser (ou precisar) alterar o formato da data:
class DateConverter:
regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
format = '%Y-%m-%d'
def to_python(self, value: str) -> datetime:
return datetime.strptime(value, self.format)
def to_url(self, value: datetime) -> str:
return value.strftime(self.format)
A classe está descrita, então é hora de registrá-la como um conversor. Isso é feito de forma muito simples: na função
register_converter
você precisa especificar a classe descrita e o nome do conversor para usá-lo nas rotas:
from django.urls import register_converter
register_converter(DateConverter, 'date')
Agora você pode descrever as rotas em
urls.py
(alterei deliberadamente o nome do parâmetro para
dt
a fim de não confundir a entrada
date:date
):
path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date:dt>/', team_report, name='team_report'),
Agora é garantido que os handlers serão chamados apenas se o conversor funcionar corretamente, o que significa que os parâmetros do tipo requerido chegarão ao handler:
def user_report(request, id: int, dt: datetime):
#
#
Parece incrível! E é assim, você pode verificar.
Sob o capô
Se você olhar de perto, surge uma questão interessante: em nenhum lugar há uma verificação de que a data está correta. Sim, existe uma temporada regular, mas uma data incorreta também é adequada para ela, por exemplo 2021-01-77, o que significa que
to_python
deve haver um erro nela. Por que isso funciona?
Sobre isso eu digo: "Jogue de acordo com as regras do framework, e ele vai jogar para você." Frameworks assumem uma série de tarefas comuns. Se a estrutura não pode fazer algo, uma boa estrutura oferece uma oportunidade de expandir sua funcionalidade. Portanto, você não deve se envolver na construção de bicicletas, é melhor ver como a estrutura oferece para melhorar suas próprias capacidades.
Django tem um subsistema de roteamento com a capacidade de adicionar conversores que cuidam da chamada do método
to_python
e captura de erros
ValueError
.
Aqui está o código do subsistema de roteamento Django sem mudanças (versão 3.1, arquivo
django/urls/resolvers.py
, classe
RoutePattern
, método
match
):
match = self.regex.search(path)
if match:
# RoutePattern doesn't allow non-named groups so args are ignored.
kwargs = match.groupdict()
for key, value in kwargs.items():
converter = self.converters[key]
try:
kwargs[key] = converter.to_python(value)
except ValueError:
return None
return path[match.end():], (), kwargs
return None
O primeiro passo é procurar correspondências na rota transmitida do cliente usando uma expressão regular. Aquele
regex
que é definido na classe conversora participa da formação
self.regex
, ou seja, é substituído em vez da expressão entre colchetes
<>
na rota.
Por exemplo,
transformar-se emusers/<int:id>/reports/<date:dt>/
^users/(?P<id>[0-9]+)/reports/(?P<dt>[0-9]{4}-[0-9]{2}-[0-9]{2})/$
No final, apenas o mesmo de regular
DateConverter
.
Esta é uma pesquisa rápida, superficial. Se nenhuma correspondência for encontrada, a rota definitivamente não é adequada, mas, se encontrada, é uma rota potencialmente adequada. Isso significa que você precisa iniciar o próximo estágio de verificação.
Cada parâmetro possui seu próprio conversor, que é usado para chamar o método
to_python
. E aqui está o mais interessante: a chamada é
to_python
encerrada
try/except
e os erros de tipo são detectados
ValueError
. É por isso que o conversor funciona mesmo no caso de uma data incorreta: cai um erro
ValueError
, e isso é considerado para que a rota não cabe.
Então, no caso de
DateConverter
, podemos dizer, sorte: no caso de uma data incorreta, cai um erro do tipo exigido. Se houver um erro de outro tipo, o Django retornará uma resposta 500.
Não pare
Parece que está tudo bem, os conversores estão funcionando, os tipos necessários vêm imediatamente para os manipuladores ... Ou não na hora?
path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
No manipulador para gerar um relatório, você provavelmente precisa dele
User
, e não dele
id
(embora possa ser o caso). Na minha situação hipotética, apenas um objeto é necessário para criar um relatório
User
. O que acontece então, novamente vinte e cinco?
def user_report(request, id: int, dt: datetime):
user = get_object_or_404(User, id=id)
# ...
Transferindo responsabilidades para o manipulador novamente.
Mas agora está claro o que fazer com ele: escreva seu próprio conversor! Ele irá certificar-se de que o objeto existe
User
e irá passá-lo ao manipulador.
class UserConverter:
regex = r'[0-9]+'
def to_python(self, value: str) -> User:
try:
return User.objects.get(id=value)
except User.DoesNotExist:
raise ValueError('not exists') # ValueError
def to_url(self, value: User) -> str:
return str(value.id)
Depois de descrever a aula, eu a registro:
register_converter(UserConverter, 'user')
Finalmente, descrevo a rota:
path('users/<user:u>/reports/<date:dt>/', user_report, name='user_report'),
Isso é melhor:
def user_report(request, u: User, dt: datetime):
# ...
Conversores para modelos podem ser usados com frequência, por isso é conveniente fazer a classe base de tal conversor (ao mesmo tempo, adicionei uma verificação para a existência de todos os atributos):
class ModelConverter:
regex: str = None
queryset: QuerySet = None
model_field: str = None
def __init__(self):
if None in (self.regex, self.queryset, self.model_field):
raise AttributeError('ModelConverter attributes are not set')
def to_python(self, value: str) -> models.Model:
try:
return self.queryset.get(**{self.model_field: value})
except ObjectDoesNotExist:
raise ValueError('not exists')
def to_url(self, value) -> str:
return str(getattr(value, self.model_field))
Em seguida, a descrição do novo conversor para o modelo será reduzida a uma descrição declarativa:
class UserConverter(ModelConverter):
regex = r'[0-9]+'
queryset = User.objects.all()
model_field = 'id'
Resultado
Os conversores de rota são um mecanismo poderoso que ajuda a tornar seu código mais limpo. Mas esse mecanismo apareceu apenas na segunda versão do Django - antes disso, tínhamos que fazer sem ele. É
get_object_or_404
daí que vêm as funções auxiliares do tipo ; sem esse mecanismo, bibliotecas legais como o DRF são feitas.
Mas isso não significa que os conversores não devam ser usados. Isso significa que (ainda) não será possível usá-los em todos os lugares. Mas, sempre que possível, recomendo que você não os negligencie.
Deixarei uma ressalva: aqui é importante não exagerar e não arrastar o cobertor na outra direção - você não precisa levar a lógica de negócios para o conversor. É necessário responder à pergunta: se tal rota é em princípio impossível, então esta é a área de responsabilidade do conversor; se tal rota for possível, mas sob certas circunstâncias ela não for processada, então isso já é responsabilidade do manipulador, serializador ou de outra pessoa, mas definitivamente não do conversor.
PS Na prática, fiz e usei apenas um conversor de datas, apenas o mostrado no artigo, já que quase sempre uso DRF ou GraphQL. Informe-nos se você usa conversores de rota e, em caso afirmativo, quais?