Dica 3 - Django bulk_create e django-autoslug
Versões usadas no vídeo: Django 2.2.13, django-autoslug 1.9.7, python-slugify 4.0.0 e Python 3.8.2.
Nesta dica vamos ver dois assuntos juntos:
- slug e o django-autoslug: um campo que gera o slug automaticamente a partir de outro campo do model.
- bulk_create: inserir vários registros no banco de uma vez só, com uma única consulta, em vez de chamar
save()(oucreate()) um por um.
Pré-requisitos
Um projeto Django com um app core. No vídeo foi usado o projeto criado com o boilerplatesimple.sh da Dica 1 (projeto myproject, app em myproject/core), que já traz o django-extensions e o shell_plus da Dica 2.
O que é um slug: python-slugify
Um slug é um texto transformado para poder ser usado numa URL: tudo em minúsculas, sem acentos, com os espaços trocados por hífen. Para ver isso na prática, use a biblioteca python-slugify:
pip install python-slugify
E no python:
>>> from slugify import slugify
>>> text = 'Dicas de Django'
>>> slugify(text)
'dicas-de-django'
>>> url = f'example.com/{slugify(text)}'
>>> url
'example.com/dicas-de-django'
O mesmo código, como script:
from slugify import slugify
text = 'Dicas de Django'
print(slugify(text))
url = f'example.com/{slugify(text)}'
É assim que blogs e lojas montam URLs legíveis, como example.com/dicas-de-django, em vez de example.com/artigo/42.
django-autoslug
No Django, em vez de gerar o slug na mão, usamos o django-autoslug. Ele fornece o AutoSlugField, que preenche o slug sozinho, a partir de outro campo, na hora de salvar.
pip install django-autoslug
Se você escrever o model antes de instalar, o manage.py reclama assim que é executado:
ModuleNotFoundError: No module named 'autoslug'
O pacote se chama django-autoslug, mas o módulo que importamos é autoslug. Ele não precisa entrar no INSTALLED_APPS.
Os models
Vamos criar dois models no app core: Article (artigo) e Category (categoria).
# myproject/core/models.py
from django.db import models
from autoslug import AutoSlugField
class Article(models.Model):
title = models.CharField('título', max_length=200)
subtitle = models.CharField('sub-título', max_length=200)
slug = AutoSlugField(populate_from='title')
category = models.ForeignKey(
'Category',
related_name='categories',
verbose_name='categoria',
on_delete=models.SET_NULL,
null=True,
blank=True
)
published_date = models.DateTimeField(
'criado em',
auto_now_add=True,
auto_now=False
)
class Meta:
ordering = ('title',)
verbose_name = 'artigo'
verbose_name_plural = 'artigos'
def __str__(self):
return self.title
class Category(models.Model):
title = models.CharField('título', max_length=50, unique=True)
class Meta:
ordering = ('title',)
verbose_name = 'categoria'
verbose_name_plural = 'categorias'
def __str__(self):
return self.title
O que cada parte faz:
slug = AutoSlugField(populate_from='title'): o slug é gerado a partir dotitle. "Django Boilerplate" viradjango-boilerplate.category: umaForeignKeyparaCategory. ComoCategoryestá definida depois deArticleno arquivo, passamos o nome do model como texto ('Category'). Comon_delete=models.SET_NULL, se a categoria for apagada, o artigo fica sem categoria em vez de ser apagado junto; por isso o campo precisa denull=True(eblank=Truepara ser opcional nos formulários).published_date: comauto_now_add=True, a data é gravada só na criação do registro; comauto_now=False, ela não muda quando o registro é editado.ordering = ('title',): ordena por título. Repare na vírgula:('title',)é uma tupla; sem a vírgula,('title')seria só uma string.Category.titletemunique=True: não pode haver duas categorias com o mesmo nome.
Crie as tabelas:
python manage.py makemigrations
python manage.py migrate
Migrations for 'core':
myproject/core/migrations/0001_initial.py
- Create model Category
- Create model Article
Running migrations:
Applying core.0001_initial... OK
No vídeo, o migrate foi esquecido no começo, e o primeiro bulk_create deu erro porque a tabela ainda não existia. Se acontecer com você, saia do shell, rode os dois comandos acima e volte.
bulk_create
O bulk_create recebe uma lista de objetos (ainda não salvos) e insere todos no banco de uma vez, com um único INSERT. Para muitos registros, é muito mais rápido do que um save() por objeto.
Abra o shell_plus (ele já importa os models Article e Category):
python manage.py shell_plus
Inserindo as categorias
A ideia é sempre a mesma: montar uma lista auxiliar com os objetos e passar a lista para o bulk_create:
categories = [
'dicas',
'django',
'python',
]
aux = []
for category in categories:
obj = Category(title=category)
aux.append(obj)
Category.objects.bulk_create(aux)
[<Category: dicas>, <Category: django>, <Category: python>]
Repare que Category(title=category) só cria o objeto na memória; nada vai para o banco até o bulk_create.
Inserindo os artigos com a categoria
Agora considere uma lista de dicionários, cada um com título, subtítulo e o nome da categoria. Para gravar o artigo, precisamos do objeto Category, então buscamos a categoria pelo título. A categoria 'admin' não existe, de propósito, para tratarmos esse caso:
titles = [
{
'title': 'Django Boilerplate',
'subtitle': 'Django Boilerplate',
'category': 'dicas'
},
{
'title': 'Django extensions',
'subtitle': 'Django extensions',
'category': 'dicas'
},
{
'title': 'Django Admin',
'subtitle': 'Django Admin',
'category': 'admin'
},
{
'title': 'Django Autoslug',
'subtitle': 'Django Autoslug',
'category': 'dicas'
},
]
aux = []
for title in titles:
category = Category.objects.filter(title=title['category']).first()
article = dict(
title=title['title'],
subtitle=title['subtitle']
)
if category:
obj = Article(category=category, **article)
else:
obj = Article(**article)
aux.append(obj)
Article.objects.bulk_create(aux)
[<Article: Django Boilerplate>, <Article: Django extensions>, <Article: Django Admin>, <Article: Django Autoslug>]
Passo a passo:
Category.objects.filter(title=...).first()devolve a categoria ouNonese ela não existir. Com.get()daria uma exceçãoDoesNotExistpara a categoria'admin'; o.first()evita isso.articleé um dicionário com os campos do artigo, e**articledesempacota esse dicionário como argumentos nomeados:Article(**article)é o mesmo queArticle(title='...', subtitle='...').- Se a categoria existe, criamos o artigo com ela; senão, sem categoria (o campo aceita
null).
E o slug? Não passamos nenhum, mas o AutoSlugField preencheu cada um a partir do título, mesmo usando bulk_create:
>>> Article.objects.values_list('title', 'slug', 'category__title')
<QuerySet [('Django Admin', 'django-admin', None), ('Django Autoslug', 'django-autoslug', 'dicas'), ('Django Boilerplate', 'django-boilerplate', 'dicas'), ('Django extensions', 'django-extensions', 'dicas')]>
(A lista vem ordenada por título, por causa do ordering do model.)
Cuidados com o bulk_create
- O
bulk_createnão chama o métodosave()do model nem dispara os sinaispre_saveepost_save. Se o seu model faz algo importante nosave(), isso não vai acontecer. - O
AutoSlugFieldfunciona porque o slug é gerado nopre_savedo próprio campo, que o Django chama também nobulk_create. Mas, como os objetos são inseridos juntos, oAutoSlugField(unique=True)não consegue garantir slugs únicos entre os objetos do mesmo lote. - No SQLite (e em outros bancos, exceto PostgreSQL), os objetos devolvidos pelo
bulk_createficam sem opkpreenchido.
Conclusão
Com o AutoSlugField o slug se resolve sozinho, e com o bulk_create você insere dezenas ou milhares de registros com uma consulta só, montando primeiro a lista de objetos e depois mandando tudo de uma vez.
Links: