Django: API REST de filmes com Django Rest Framework e PostgreSQL
Desenvolva com Django e Django Rest Framework uma API REST de filmes persistida no PostgreSQL, com serializers, views genéricas, migrações, variáveis de ambiente com django-environ, relacionamento 1xN com gêneros e uma estrutura de pastas refatorada.
1. Visão geral
Neste material, vamos utilizar o arcabouço Django para desenvolver uma API REST para manipulação de filmes. Nossa API terá os seguintes endpoints.
| Endpoint | Finalidade |
|---|---|
GET /filmes |
obtém a coleção de filmes |
GET /filmes/id |
obtém os dados do filme cujo id foi especificado como parâmetro de path |
POST /filmes |
armazena um novo filme enviado como JSON |
DELETE /filmes/id |
apaga o filme cujo id foi especificado como parâmetro de path |
O que você vai aprender
- Como criar um projeto Django com o Django Rest Framework (DRF) e o driver psycopg2
- Como configurar o acesso ao PostgreSQL e criar um database com o pgAdmin
- Como definir classes de modelo, serializers e views genéricas do DRF
- Como mapear URLs com variáveis de path
- Como funcionam
makemigrationsemigrate, inclusive migrações de dados - Como testar a API com a Thunder Client
- Como tirar senhas e chaves do código com o django-environ e o
.gitignore - Como criar um relacionamento 1xN e serializar objetos aninhados
- Como organizar models, serializers e views em pastas
O que você vai precisar
- Python 3 instalado
- PostgreSQL e pgAdmin instalados
- VS Code com a extensão Thunder Client
2. Ambiente virtual e novo projeto Django
Crie uma pasta para abrigar seus projetos Django. No Windows, uma sugestão é
C:\Users\usuario\Documents\dev\django
Em sistemas Unix-like, use
/home/usuario/dev/django
Abra um terminal (CMD no Windows, Bash em sistemas Unix-like) e navegue até o diretório recém-criado.
Terminal (Windows)
cd C:\Users\usuario\Documents\dev\django
Terminal (Unix-like)
cd /home/usuario/dev/django
Use
Terminal
python -m venv venv
para criar um ambiente virtual Python chamado venv utilizando o módulo venv. Após a sua execução, uma pasta chamada venv deve ser criada na raiz de seu projeto.
Depois da criação do ambiente virtual, é preciso ativá-lo. Usuários Windows podem utilizar um terminal "cmd" ou um terminal "Powershell". A tabela a seguir resume a forma como o ambiente virtual Python pode ser ativado em qualquer caso.
| Terminal | Comando(s) |
|---|---|
| Windows cmd | venv\Scripts\activate.bat |
| Windows Powershell | Set-ExecutionPolicy -Scope CurrentUser unrestricted e, depois, venv\Scripts\Activate.ps1 |
| Unix-like terminals | . venv/bin/activate |
Em qualquer caso, você pode verificar se o ambiente foi ativado com sucesso com
Terminal
pip -V
A pasta de seu ambiente virtual deve ser exibida.

O pip em uso é o do ambiente virtual.
O nome do ambiente virtual que você criou também deve aparecer.

O nome do ambiente virtual ativo aparece no prompt.
A seguir, podemos instalar o pacote Django. Essa instalação será válida apenas para o ambiente virtual Python ativo no momento.
Terminal
pip install django
Assim, temos um ambiente virtual Python que já inclui o Django, que poderemos utilizar sempre que necessário.
O próximo passo é criar um projeto Django. Um projeto Django é uma coleção de aplicações Django e configurações. Em geral, ele possui
- um "CLI" (command line interface) que nos permite interagir com seu conteúdo
- arquivos de configurações
- arquivos de definição de URLs
Podemos criar o projeto com
Terminal
django-admin startproject filmes_project
Uma pasta deve ter sido criada, ao lado da pasta que representa o seu ambiente virtual.

A pasta do projeto ao lado da pasta venv.
Abra o VS Code e clique em File >> Open Folder. Navegue para encontrar a pasta de seu projeto e vincule o VS Code a ela. Veja o resultado esperado.

O projeto aberto no VS Code.

Ative o Auto Save.
No VS Code, clique em Terminal >> New terminal para abrir um terminal interno do VS Code. Lembre-se de ativar o ambiente virtual que criamos anteriormente, assim ele será válido para essa instância de terminal que acabamos de abrir.
Veja o resultado esperado.

O ambiente virtual ativado no terminal do VS Code.
3. Dependências, aplicação e PostgreSQL
Django Rest Framework (DRF) e psycopg2 (driver PostgreSQL)
Para criar a API Rest, vamos utilizar o conhecido DRF. Se desejar saber mais sobre ele, visite https://www.django-rest-framework.org/.
Além disso, nossa aplicação se conectará a uma instância do PostgreSQL e, para tal, ela precisa ser capaz de se comunicar utilizando o protocolo do PostgreSQL. Ele é implementado por diferentes pacotes, que geralmente levam o nome de "driver". Neste material, vamos utilizar o pacote psycopg2. No terminal do VS Code, use
Terminal
pip install django djangorestframework psycopg2
para instalar ambos.
Nova aplicação
Até então, temos apenas um projeto Django. Agora precisamos criar uma aplicação que fará parte dele. Para isso, use
Terminal
python manage.py startapp filmes_app
Adicionando aplicações ao projeto
No arquivo settings.py do projeto, precisamos adicionar a nossa aplicação como parte dele, na lista de INSTALLED_APPS. Como vamos usar o DRF, também precisamos adicionar uma aplicação chamada rest_framework. Veja.
filmes_project/settings.py
# ...
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"filmes_app"
]
# MIDDLEWARE = [
# ...
Configurando os dados de acesso ao PostgreSQL
No arquivo settings.py do projeto, temos uma variável chamada DATABASES. Ela referencia um dicionário Python que contém configurações de acesso a bases de dados. Observe que, por padrão, o Django usa o SQLite 3.
filmes_project/settings.py (padrão)
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
Vamos alterar para utilizar o PostgreSQL. Veja como fica.
filmes_project/settings.py
# ...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'pessoal_pdfs_rest_filmes',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
}
}
# ...
4. Criando o database com o pgAdmin
Observe que estamos utilizando um nome específico para o database. Embora possa lidar com a criação de tabelas, o Django não cria o database. Devemos fazê-lo manualmente. Para isso, abra o pgAdmin. Clique com o direito em Servers e escolha Register >> Server.

Registrando um servidor no pgAdmin.
O nome pode ser algo como localhost. Na aba Connection, precisamos especificar os detalhes para acesso ao servidor local. Clique em Save a seguir.

Os dados de conexão com o servidor local.
Clique sobre localhost à direita, clique com o direito sobre Databases e escolha Create >> Database.

Criando um database.
Coloque o mesmo nome que tenha especificado no arquivo settings.py do seu projeto Django. Clique em Save a seguir.

O nome do database igual ao do settings.py.
5. Modelo, serializer, views e URLs
Classe de modelo que descreve o que é um filme
No arquivo models.py da aplicação, faça a definição de uma classe. Ela herda de models.Model e descreve as propriedades de interesse de um filme, além de aplicar validações eventualmente desejadas. Podemos também definir o método __str__, responsável por produzir uma representação textual dos objetos da classe. Observe que, neste momento, estamos fazendo uso do mecanismo de mapeamento objeto relacional provido pelo Django.
filmes_app/models.py
from django.db import models
# Create your models here.
class Filme(models.Model):
titulo = models.CharField(max_length=100)
descricao = models.TextField()
diretor = models.CharField(max_length=100)
def __str__(self):
return self.titulo
Serializers: convertendo um objeto Python em JSON e vice-versa
O DRF nos permite especificar a forma como um objeto Python deve ser convertido para JSON e vice-versa. Em geral, escolhemos os campos desejados e o trabalho "duro" fica todo por conta de classes utilitárias. Uma classe que desempenha tal papel é chamada de Serializer. Crie um arquivo chamado serializers.py na pasta da aplicação. Veja a definição de um serializer que converte objetos do tipo filme incluindo todos os seus campos.
filmes_app/serializers.py
from rest_framework import serializers
from .models import Filme
class FilmeSerializer (serializers.ModelSerializer):
class Meta:
model = Filme
fields = '__all__'
Views para lidar com as requisições
Nossa API possui endpoints que são acessados utilizando métodos comuns do protocolo HTTP, como GET, POST etc. O DRF oferece classes genéricas que implementam os detalhes de mais baixo nível do protocolo e simplificam bastante a nossa tarefa. Neste exemplo, vamos usar duas classes:
- ListCreateAPIView: para implementar os endpoints
GET /filmesePOST /filmes. Observe que o padrão de acesso é o mesmo, o que muda é o método do protocolo HTTP. Por isso faz sentido que uma única classe seja capaz de lidar com os dois endpoints. - RetrieveDestroyAPIView: para implementar os endpoints
GET /filmes/{id}eDELETE /filmes/{id}. Novamente, o padrão de acesso é o mesmo, o que muda é o método do protocolo HTTP. Essa classe é capaz de lidar com ambos, como o nome sugere.
Observe que, em nossa implementação, fazemos uso de dois atributos. Ambos são definidos pela classe da qual herdamos.
queryset: associamos a esse atributo a coleção sobre a qual desejamos operar. Tanto para indicar qual lista de objetos deve ser devolvida quanto para indicar a qual lista um novo objeto deve ser adicionado.serializer_class: associamos a esse atributo o nome de uma classe que é responsável por fazer eventuais validações e conversões de objeto Python para JSON e vice-versa. Ou seja, a classe Serializer que criamos há pouco.
Veja como fica. Use o arquivo views.py da aplicação.
filmes_app/views.py
from rest_framework import generics
from .models import Filme
from .serializers import FilmeSerializer
class FilmeListCreate(generics.ListCreateAPIView):
queryset = Filme.objects.all()
serializer_class = FilmeSerializer
class FilmeRetrieveDestroy(generics.RetrieveDestroyAPIView):
queryset = Filme.objects.all()
serializer_class = FilmeSerializer
Mapeamento de URLs
Precisamos fazer o mapeamento URL/View como a seguir.
| URL | View |
|---|---|
filmes/ |
FilmeListCreate |
filmes/<int:pk> |
FilmeRetrieveDestroy |
Crie um arquivo chamado urls.py na pasta da aplicação. Veja seu conteúdo.
filmes_app/urls.py
from django.urls import path
from . import views
urlpatterns = [
path ('filmes/', views.FilmeListCreate.as_view(), name="filme-list-create"),
path('filmes/<int:pk>', views.FilmeRetrieveDestroy.as_view(), name='filme-retrieve-destroy')
]
Lembre-se que é necessário incluir o módulo urls da aplicação que acabamos de criar no módulo principal do projeto. O arquivo também se chama urls.py e se encontra na pasta do projeto. Veja como fica.
filmes_project/urls.py
# ... (docstring do arquivo)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path('', include('filmes_app.urls')),
]
6. Migrações: criando a estrutura da base
O mecanismo de mapeamento objeto relacional do Django permite que criemos as tabelas do banco de dados em função de classes de modelo que tenhamos definido. Além disso, contamos com um sistema de migração que permite alternarmos entre diferentes versões da base conforme a necessidade. Todo o histórico de alterações é mantido em tabelas também criadas na nossa base pelo Django. Temos dois comandos
makemigrations: detecta eventuais diferenças entre suas classes de modelo e a estrutura da base de dados atual. Cria arquivos que, se executados, fazem as transformações necessárias. Ou seja, aqui a base não é alterada ainda, o que dá maior flexibilidade ao desenvolvedor. São gerados arquivos de migração que ficam armazenados na pastamigrationsda aplicação.migrate: executa os arquivos criados pelo comandomakemigrations, alterando a estrutura da base.
No terminal do VS Code, execute o primeiro com
Terminal
python manage.py makemigrations filmes_app
Veja o feedback textual dado no terminal.

O arquivo de migração criado.
Por curiosidade, expanda a pasta migrations da aplicação. Observe que um arquivo chamado 0001_initial.py foi criado. Abra-o e veja seu conteúdo. Ali temos código Python que, quando executado, altera a estrutura da base. Não precisamos mexer neste arquivo, embora possamos.
filmes_app/migrations/0001_initial.py
# Generated by Django 4.2.5 on 2023-09-11 03:23
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Filme",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("titulo", models.CharField(max_length=100)),
("descricao", models.TextField()),
("diretor", models.CharField(max_length=100)),
],
),
]
Se você abrir o pgAdmin, expandir até encontrar a seção de tabelas e clicar em Refresh, como na figura a seguir, perceberá que nenhuma tabela foi criada ainda.

Ainda não há tabelas no database.
Execute o próximo comando no terminal do VS Code
Terminal
python manage.py migrate
Veja o feedback textual no terminal.

As migrações aplicadas.
Volte ao pgAdmin e clique Refresh sobre Tables novamente. Veja que foram criadas diversas tabelas. Neste momento, a maioria é referente ao próprio controle realizado pelo Django. Observe que uma delas serve para abrigar dados de filmes.

As tabelas criadas, entre elas filmes_app_filme.
7. Testando com a Thunder Client
Agora podemos verificar se a aplicação está funcionando corretamente. Comece colocando o servidor em execução com
Terminal
python manage.py runserver
Veja o feedback textual esperado.

O servidor em execução.
Há uma extensão para o VS Code chamada Thunder Client. Trata-se de um cliente HTTP que nos permite construir requisições mais detalhadamente do que faríamos com um navegador comum. É uma ferramenta essencial para o desenvolvedor. Se você ainda não tiver, faça a sua instalação.

A extensão Thunder Client.
Uma vez que ela tenha sido instalada, você deve ser capaz de ver seu ícone na barra lateral esquerda do VS Code.

O ícone da Thunder Client.
Comece criando uma nova coleção. Vamos utilizá-la para agrupar as requisições referentes a este projeto.

Criando uma coleção.
Depois de escolher um nome para a coleção, clique com o direito sobre seu nome e escolha New Request.

Criando uma requisição na coleção.
Dê um nome para a requisição e aperte Enter. Esta primeira requisição servirá para salvarmos um filme na base.

A requisição Salvar Filme.
Faça os ajustes destacados a seguir e clique em Send para fazer uma requisição.
Corpo da requisição (JSON) · POST localhost:8000/filmes/
{
"titulo": "Titanic",
"descricao": "Um filme sobre o Titanic",
"diretor": "{{#name}}"
}

O filme salvo, com um nome de diretor gerado aleatoriamente.
No pgAdmin, mantenha seu database selecionado (basta clicar no seu nome) e clique em Tools >> Query Tool.

Abrindo a Query Tool.
Execute um SELECT e verifique que os dados foram armazenados na tabela.
Query Tool
SELECT * FROM filmes_app_filme;

O filme armazenado na tabela.
Como exercício, crie as demais requisições na Thunder Client e faça novos testes.
8. Variáveis de ambiente com django-environ
Considere os seguintes valores de que projetos geralmente dependem para funcionar
- senhas
- chaves de API
- URL de acesso à base de dados
entre muitos outros. Estes valores não devem ser armazenados diretamente ("chumbados") no código Python por, pelo menos, duas razões:
- são valores que variam em função do ambiente: se estamos em tempo de desenvolvimento, provavelmente estamos acessando uma base própria para isso, apenas para os testes do desenvolvedor, isolada de outros ambientes, especialmente de produção
- são valores que não devem fazer parte do controle de versão, especialmente se estivermos utilizando um repositório público.
As diferentes linguagens de programação possuem diferentes mecanismos para lidar com isso. Em Python, algumas opções são:
- python-decouple: https://pypi.org/project/python-decouple/
- python-dotenv: https://pypi.org/project/python-dotenv/
- django-environ: https://pypi.org/project/django-environ/
Entre outros. Neste material, utilizaremos o pacote django-environ. O primeiro passo é fazer a sua instalação.
Terminal
pip install django-environ
A seguir, criamos um arquivo chamado .env na raiz do projeto, lado a lado com o arquivo manage.py.

O arquivo .env na raiz do projeto.
Quando um projeto Django é colocado em execução, o arquivo settings.py é executado automaticamente. Por isso, vamos fazer a leitura do arquivo .env nele. Para tal
- importamos o módulo
environ - construímos um objeto do tipo
environ.Env - usamos o método
read_envpara ler o conteúdo
Observe que vamos utilizar o "diretório base" da aplicação, que é a pasta que contém o arquivo .env que criamos. Ele já vem definido por padrão e se encontra na variável BASE_DIR.
Veja como ficou até agora.
filmes_project/settings.py
# ...
from pathlib import Path
import environ
env = environ.Env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
environ.Env.read_env(BASE_DIR / Path(".env"))
# ...
Ainda no arquivo settings.py, observe que temos dois valores.
SECRET_KEY: utilizada pelo Django em funcionalidades como
- assinatura criptográfica de dados mantidos em sessão, para que seja possível verificar que os dados enviados pelo cliente não foram adulterados
- geração de tokens para reconfiguração de senha perdida
DEBUG: valor booleano que indica que a aplicação está em modo de depuração. Devemos configurá-la como True apenas em tempo de desenvolvimento. Veja algumas de suas características.
- Quando vale
True, o Django exibe a pilha de chamada de métodos quando um erro acontece. - Mantém as queries de consulta ao banco em memória, o que é bom para depuração mas causa impacto no desempenho da aplicação, algo desnecessário em tempo de produção.
- Por questões de segurança, ela nunca deve valer
Trueem tempo de produção, já que uma pessoa mal-intencionada poderia utilizar técnicas para tentar atacar a aplicação e encontrar as informações mantidas em memória sobre seu funcionamento.
Assim, vamos definir ambas no arquivo .env.
.env
SECRET_KEY=django-insecure-9^t&7y^1hb*j8d=15+_^l0tnb7jqy)!-oyu8o$o5u)5-@1c*-4
DEBUG=True
A seguir, passamos a utilizá-las no arquivo settings.py.
filmes_project/settings.py
# ...
env = environ.Env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
environ.Env.read_env(BASE_DIR / Path(".env"))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG')
# ...
Se desejar, você pode testar a configuração exibindo os valores das variáveis.
filmes_project/settings.py
# ...
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG')
print(SECRET_KEY, DEBUG)
# ...
Para executar e ver o resultado, use
Terminal
python filmes_project/settings.py
Observe.

Os valores lidos do arquivo .env.
Depois do teste, você pode remover a instrução print.
Também podemos fazer a construção do objeto Env mantendo algumas variáveis com valor padrão. Elas terão aquele valor a menos que o arquivo .env indique outro. Como é recomendável usar DEBUG=True apenas em ambiente de desenvolvimento, podemos adotar a seguinte estratégia:
- ao construir o objeto
Env, indicamos que o valor deDEBUGé igual aFalse - redefinimos o valor de
DEBUG(como já está feito neste exemplo) no arquivo.env.
Fica assim. Observe que atribuímos à variável DEBUG uma tupla: o primeiro valor indica o tipo ao qual o segundo será convertido.
filmes_project/settings.py
from pathlib import Path
import environ
env = environ.Env(
DEBUG = (bool, False)
)
Neste exemplo, o valor que prevalece é aquele definido no arquivo .env. Se não houver definição no arquivo .env, fica valendo o valor definido na construção do objeto Env.
Dados de acesso à base
Nossa aplicação possui dados de acesso a bases de dados. Estes também são variáveis de ambiente, já que podem variar em função do ambiente (desenvolvimento, produção etc.). Assim, é interessante fazer a sua definição num arquivo isolado, como o arquivo .env. Veja como fica o arquivo .env.
.env
SECRET_KEY=django-insecure-9^t&7y^1hb*j8d=15+_^l0tnb7jqy)!-oyu8o$o5u)5-@1c*-4
DEBUG=TRUE
DATABASE_DEFAULT_NAME=pessoal_pdfs_rest_filmes
DATABASE_DEFAULT_USER=postgres
DATABASE_DEFAULT_PASSWORD=postgres
DATABASE_DEFAULT_HOST=localhost
DATABASE_DEFAULT_PORT=5432
A seguir, no arquivo settings.py, passamos a utilizá-las.
filmes_project/settings.py
# ...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DATABASE_DEFAULT_NAME'),
'USER': env('DATABASE_DEFAULT_USER'),
'PASSWORD': env('DATABASE_DEFAULT_PASSWORD'),
'HOST': env('DATABASE_DEFAULT_HOST'),
'PORT': env('DATABASE_DEFAULT_PORT'),
}
}
# ...
Execute a aplicação com
Terminal
python manage.py runserver
e faça nova requisição com a Thunder Client, certificando-se de que os novos ajustes estão corretos.

A API funcionando com as configurações lidas do .env.
9. .gitignore
É fundamental utilizarmos um arquivo chamado .gitignore na raiz da aplicação e, entre outras coisas, especificar que o arquivo .env não deve entrar no controle de versão. Caso ainda não possua um arquivo .gitignore, crie um na raiz.

O arquivo .gitignore na raiz.
Além do arquivo .env, há diversos outros arquivos que podemos estar interessados em especificar num arquivo .gitignore de um projeto Django. Veja um possível conteúdo para ele.
.gitignore
#pasta com código compilado (bytecode) em cache
__pycache__/
#código compilado ou arquivos para otimização
*.py[cod]
#código compilado para Jython
*$py.class
#a base de dados SQLite
db.sqlite3
#o arquivo .env
.env
#do MacOS
.DS_Store
#arquivos de log
*.log
Há diversos outros arquivos que, ao longo do tempo, podem ser adicionados ao .gitignore, conforme o projeto passa a utilizar mais recursos. O site https://www.toptal.com/developers/gitignore oferece uma boa ferramenta geradora de arquivos .gitignore. Basta buscar pelo tipo desejado. Caso deseje testar, basta buscar por Django. Há também uma extensão para o VS Code, caso deseje verificar. Seu nome é "gi".

A extensão gi.
Para utilizá-la, basta apertar CTRL+SHIFT+P e buscar por gi. Talvez você precise "rolar" a lista até encontrar a extensão de nome gi.

O comando gi na paleta de comandos.
Depois de selecioná-la, busque pelo tipo desejado. Neste caso, Django. Você pode buscar por nome de IDE (VS Code, Eclipse etc.), nome de linguagem de programação (Java, C++ etc.) e mais.

Gerando um .gitignore para Django.
10. Relacionamento 1xN: gêneros e migração de dados
Neste exemplo, vamos adicionar um novo modelo ao projeto Django. Passaremos a lidar com objetos do tipo Gênero, os quais terão id e descrição. Além disso, há um relacionamento 1xN entre gênero e filme: um filme tem um gênero; um gênero pode estar associado a N filmes. Abra o arquivo models.py da aplicação e defina a nova classe de modelo, responsável por dizer o que é um Gênero. Observe que, para testes futuros, já sobrescrevemos o método __str__.
filmes_app/models.py
from django.db import models
# Create your models here.
class Genero(models.Model):
descricao = models.CharField(max_length=100)
def __str__(self):
return self.descricao
class Filme(models.Model):
titulo = models.CharField(max_length=100)
descricao = models.TextField()
diretor = models.CharField(max_length=100)
def __str__(self):
return self.titulo
A seguir, vamos criar um arquivo de migração para que a tabela correspondente à classe Genero seja criada.
Terminal
python manage.py makemigrations
Observe que um novo arquivo de migração foi criado.

O arquivo 0002_genero.py.
Por curiosidade, abra o arquivo e inspecione o seu conteúdo. Apenas observe, não há nada para alterar.
filmes_app/migrations/0002_genero.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("filmes_app", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Genero",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("descricao", models.CharField(max_length=100)),
],
),
]
Uma migração de dados
Antes de executar a migração, vamos criar uma nova, inicialmente vazia. Ela terá como finalidade fazer o cadastro de um novo gênero. Para isso, use
Terminal
python manage.py makemigrations filmes_app --empty --name inserir_genero_inicial
Veja o arquivo criado.

A migração vazia 0003_inserir_genero_inicial.py.
Como esta é uma migração vazia, vamos abrir o arquivo e escrever código explicando o que ela deve fazer, quando aplicada. Começamos definindo uma função que
- obtém uma referência ao modelo Gênero
- utiliza o método
createde sua propriedadeobjectsa fim de criar um novo gênero
filmes_app/migrations/0003_inserir_genero_inicial.py
from django.db import migrations
def inserir_genero_inicial(apps, schema_editor):
Genero = apps.get_model('filmes_app', 'Genero')
Genero.objects.create(descricao="Romance")
class Migration(migrations.Migration):
dependencies = [
("filmes_app", "0002_genero"),
]
operations = []
Observe que a migração possui um campo chamado dependencies. Ele indica que ela somente pode ser executada depois de a migração ali especificada ter sido executada. Isso faz sentido, afinal, um gênero somente pode ser criado caso a tabela capaz de armazená-lo exista.
A seguir, na lista operations, incluímos a função que criamos anteriormente, responsável pelo cadastro do novo gênero.
filmes_app/migrations/0003_inserir_genero_inicial.py
from django.db import migrations
def inserir_genero_inicial(apps, schema_editor):
Genero = apps.get_model('filmes_app', 'Genero')
Genero.objects.create(descricao="Romance")
class Migration(migrations.Migration):
dependencies = [
("filmes_app", "0002_genero"),
]
operations = [
migrations.RunPython(inserir_genero_inicial)
]
Depois disso, podemos executar as migrações.
Terminal
python manage.py migrate
Já no pgAdmin, encontre o seu database, clique sobre ele, expanda Schemas >> public e encontre Tables. Expanda Tables, encontre as tabelas referentes a filmes e a gêneros e veja as suas colunas.

As tabelas de filmes e de gêneros.

As colunas das tabelas.
11. A chave estrangeira entre filme e gênero
A seguir, precisamos estabelecer o relacionamento entre filme e gênero. Para isso, vamos usar uma chave estrangeira. Cada filme tem um novo campo. Ele referencia o id do gênero a que aquele filme está associado.
filmes_app/models.py
from django.db import models
# Create your models here.
class Genero(models.Model):
descricao = models.CharField(max_length=100)
def __str__(self):
return self.descricao
class Filme(models.Model):
titulo = models.CharField(max_length=100)
descricao = models.TextField()
diretor = models.CharField(max_length=100)
genero = models.ForeignKey(Genero, on_delete=models.CASCADE)
def __str__(self):
return self.titulo
Como fizemos alterações nas classes de modelo, precisamos realizar o processo de migração novamente. Primeiro, geramos o código Python que fará as alterações necessárias na base, sem ainda, entretanto, executá-lo.
Terminal
python manage.py makemigrations
Veja a mensagem obtida.

O Django pede um valor para os filmes já existentes.
A sua aparição é natural. Como o relacionamento entre filmes e gêneros é obrigatório (não pode existir filme sem gênero), não podemos adicionar essa nova coluna à tabela de filmes, já que, no momento, ela já possui algumas linhas. Se a coluna fosse adicionada, qual valor ela teria? Temos algumas opções:
- especificar agora, na linha de comando, um valor de id de gênero já existente, que será inserido para cada filme já existente.
- apertar CTRL+C no terminal agora e ajustar um valor padrão no arquivo
models.py.
Vamos optar pela primeira. Assim, digite 1.

O Django pede o valor padrão.
A seguir, você deverá digitar o valor desejado. Basta digitar 1 novamente, que é o id do único gênero existente.

A migração 0004_filme_genero.py criada.
Agora podemos executar a nova migração.
Terminal
python manage.py migrate
No pgAdmin, execute um SELECT na tabela de filmes e observe que a nova coluna existe. Cada filme existente tem o valor padrão associado a ele.

A nova coluna genero_id na tabela de filmes.
Na Thunder Client, faça uma requisição e veja o resultado.

Cada filme traz apenas o id do gênero.
Serializando o gênero completo
Observe que cada filme possui apenas o id do gênero a que está associado. Podemos personalizar isso, fazendo com que cada filme possua o objeto JSON que representa seu gênero por completo.
Para isso, no arquivo serializers.py, especifique uma classe serializadora para os gêneros, explicando que ela deve incluir todos os campos de gênero. Depois disso, na classe serializadora de filmes, explique que a forma a ser utilizada para serializar um gênero de um filme é aquela determinada pelo serializador de gênero, ou seja, incluindo todos os campos.
filmes_app/serializers.py
from rest_framework import serializers
from .models import Filme
from .models import Genero
class GeneroSerializer (serializers.ModelSerializer):
class Meta:
model = Genero
fields = '__all__'
class FilmeSerializer (serializers.ModelSerializer):
#o nome genero deve ser igual ao nome especificado no modelo de filme
genero = GeneroSerializer()
class Meta:
model = Filme
fields = '__all__'
12. Refatorando: models, serializers e views em pastas
À medida que a aplicação cresce, pode ser difícil mantê-la caso tenhamos
- um único arquivo para abrigar as classes de modelo
- um único arquivo para abrigar as views
- um único arquivo para abrigar as classes serializadoras
Vamos organizar a estrutura da aplicação criando uma pasta para cada item desses e, dentro dela, um arquivo separado para cada classe.
Models
Começamos pelas classes de modelo. Crie uma pasta chamada models na raiz da aplicação. Na pasta models, crie um arquivo chamado genero.py. Ele servirá para definir a classe de modelo que descreve o que é um gênero.

A pasta models.
O código do arquivo genero.py é o mesmo que tínhamos anteriormente no arquivo models.py.
filmes_app/models/genero.py
from django.db import models
class Genero(models.Model):
descricao = models.CharField(max_length=100)
def __str__(self):
return self.descricao
Repita os passos para a classe de modelo Filme, criando um arquivo filme.py para ela na pasta models. Observe que ela precisa importar o modelo de gênero também. Veja o código do arquivo filme.py. É o mesmo que tínhamos antes também.
filmes_app/models/filme.py
from django.db import models
from .genero import Genero
class Filme(models.Model):
titulo = models.CharField(max_length=100)
descricao = models.TextField()
diretor = models.CharField(max_length=100)
genero = models.ForeignKey(Genero, on_delete=models.CASCADE)
def __str__(self):
return self.titulo
Na pasta models, vamos criar um arquivo chamado __init__.py (dois underscores de cada lado). Ele serve para designar o diretório em que se encontra como um pacote Python, permitindo que módulos ali existentes sejam importados por outros. Ele também pode ter código de inicialização do pacote, entre outras coisas.

O arquivo init.py na pasta models.
Neste arquivo, vamos importar nossas classes de modelo.
filmes_app/models/__init__.py
from .filme import Filme
from .genero import Genero
Observe que a existência do arquivo __init__.py numa pasta chamada models nos permite continuar com os mesmos imports que antes faziam referência ao arquivo original models.py. Assim, você pode apagá-lo, ele não será mais necessário.
Serializers
Agora vamos repetir o processo para as classes serializadoras. Comece criando uma pasta chamada serializers na raiz da sua aplicação.

A pasta serializers.
Crie um arquivo chamado genero_serializer.py dentro da pasta recém-criada. Veja seu conteúdo. Observe que, agora, os imports de modelos precisam ser relativos à existência das duas pastas. Não se esqueça de ajustar, indicando que a pasta models é subpasta de filmes_app. A definição do serializer é exatamente a mesma que tínhamos antes. Só o import mudou.
filmes_app/serializers/genero_serializer.py
from rest_framework import serializers
#lembre-se de ajustar o import
from filmes_app.models import Genero
class GeneroSerializer (serializers.ModelSerializer):
class Meta:
model = Genero
fields = '__all__'
Crie um arquivo chamado filme_serializer.py dentro da pasta recém-criada. Veja seu conteúdo. Idêntico ao anterior também, a menos do import.
filmes_app/serializers/filme_serializer.py
from rest_framework import serializers
#lembre-se de ajustar os imports
from filmes_app.models import Filme
from .genero_serializer import GeneroSerializer
class FilmeSerializer (serializers.ModelSerializer):
#o nome genero deve ser igual ao nome especificado no modelo de filme
genero = GeneroSerializer()
class Meta:
model = Filme
fields = '__all__'
Crie também um arquivo chamado __init__.py na sua pasta serializers e importe as classes serializadoras.
filmes_app/serializers/__init__.py
from .filme_serializer import FilmeSerializer
from .genero_serializer import GeneroSerializer
Neste momento, já podemos apagar o arquivo serializers.py original.
Views
O processo para as views é análogo. Crie uma pasta chamada views na raiz da aplicação. Dentro dela, crie um arquivo chamado __init__.py.

A pasta views.
A seguir, crie um arquivo chamado filme_views.py. Seu conteúdo é o mesmo que tínhamos anteriormente, a menos dos imports. Veja.
filmes_app/views/filme_views.py
from rest_framework import generics
#ajuste os imports
from filmes_app.models import Filme
from filmes_app.serializers import FilmeSerializer
class FilmeListCreate(generics.ListCreateAPIView):
queryset = Filme.objects.all()
serializer_class = FilmeSerializer
class FilmeRetrieveDestroy(generics.RetrieveDestroyAPIView):
queryset = Filme.objects.all()
serializer_class = FilmeSerializer
No arquivo __init__.py da pasta recém-criada, faça o import das views a partir do novo arquivo.
filmes_app/views/__init__.py
from .filme_views import FilmeListCreate, FilmeRetrieveDestroy
Neste momento, também já é possível apagar o arquivo views.py original.
Pode ser uma boa ideia reiniciar o servidor e realizar novos testes.
13. Exercícios
Exercício 1: pessoas, diretores e atores
Comece clonando o repositório de seu grupo.
Terminal (G1)
git clone https://github.com/professorbossini/20232_maua_tti203_g1_rest_filmes.git .
Terminal (G2)
git clone https://github.com/professorbossini/20232_maua_tti203_g2_rest_filmes .
- Adicione um novo modelo para representar pessoas. Pessoas têm nome e idade. Implemente endpoints para CRUD básico de pessoas (POST, DELETE, GET e PUT).
- No momento, filmes têm diretor representado como String. Faça com que diretores sejam do tipo Pessoa. Estabeleça um relacionamento 1xN entre filmes e diretores: um filme tem um diretor; um diretor dirige muitos filmes.
- Adicione atores aos filmes, com um relacionamento do tipo NxN. Atores são pessoas. Um filme tem muitos atores; um ator atua em muitos filmes.
Estude sobre relacionamento NxN aqui: https://docs.djangoproject.com/en/4.2/topics/db/examples/many_to_many/.
Exercício 2: filtro por bilheteria
Este exercício foi proposto para quem é do G1 e já fez a aula de reposição do dia 11/09.
Se desejar, clone o projeto do professor. Para tal, crie uma pasta nova, vazia e use
Terminal
git clone https://github.com/professorbossini/pessoal_django_filmes.git .
Lembre-se de ativar o ambiente virtual Python (veja o passo "Ambiente virtual e novo projeto Django").
- Adicione um campo "bilheteria" (número real positivo) ao modelo que descreve o que é um filme.
- Execute os passos necessários (migrations) para alterar a estrutura da base de dados gerenciada pelo PostgreSQL.
- Defina um novo endpoint que recebe um valor de bilheteria como parâmetro de query. Ou seja, assim:
localhost:8000/filmes?bilheteria=1000(o trechobilheteria=1000é um parâmetro de query).
O endpoint deve devolver somente os filmes que tenham valor de bilheteria maior ou igual àquele especificado como parâmetro de query. Para fazer esse endpoint, você vai querer estudar o link a seguir: https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-query-parameters.
Ver resposta
Veja um exemplo de como poderia ficar. Tente fazer você mesmo antes, estudando a documentação.
filmes_app/views/filme_views.py
class FilmeListCreate(generics.ListCreateAPIView):
serializer_class = FilmeSerializer
def get_queryset(self):
queryset = Filme.objects.all()
min_bilheteria = self.request.query_params.get('bilheteria', None)
if min_bilheteria is not None:
queryset = queryset.filter(bilheteria__gte=min_bilheteria)
return queryset
Referências
- Django Rest Framework: https://www.django-rest-framework.org/
- Generic views do DRF: https://www.django-rest-framework.org/api-guide/generic-views/
- django-environ: https://pypi.org/project/django-environ/
- Gerador de .gitignore: https://www.toptal.com/developers/gitignore