4.2 Banco de Dados no Django

Continuando nossa implementação, observamos que, para que os itens da lista fiquem disponível e possam ser consultados a posteriori faz-se necessário que os mesmos sejam armazenados em um meio de armazenamento persistente.

No caso do Django podemos contar com a disponibilidade de um mecanismo denominado Object-Relational Mapper (ORM) ou Mapeador Objeto-Relacional, que transforma objetos de nossa aplicação em tabelas de um banco de dados relacional.

Como destaca Percival (2017), nada melhor do que escrever um teste de unidade para conhecer melhor o mecanismo de ORM e como ele pode ser utilizado na melhoria de nossa aplicação. Desse modo, podemos criar uma nova classe de teste dentro de nosso arquivo lists/tests.py. O código da mesma segue abaixo. Observe que omitimos o restante do código que estava no arquivo tests.py para melhor visualização.

# Inserir abaixo do código já exitente

from lists.models import Item

class ItemModelTest(TestCase):

	def test_saving_and_retriving_items(self):
		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.save()

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(second_saved_item.text, 'Item the second')

Aparentemente, o armazenamento de itens utilizando o ORM não é algo complicado. Os testes indicam que devemos ter um objeto do tipo Item, definirmos os valores de seus atributos (no caso esperamos ter um atributo text) e, em seguida, invocar o método save() nesse objeto. A mágica ocorre nesse momento em que o método save() é invocado e o objeto é então armazenado no banco de dados interno do Django.

Posteriormente, optamos pelo uso do método Item.objects.all() que recupera a lista de objetos já armazenada e fazemos os testes para termos certeza de que o que foi recuperado é o que havia sido salvo anteriormente.

Ao executar os testes unitários da forma como estão obtemos a saída abaixo como era de se esperar, ou seja, o teste está acusando que não foi capaz de encontrar a classe Item (linha 16).

Para iniciarmos as pequenas alterações com a intenção de fazer o teste acima avançar, precisamos editar o arquivo lists/models.py e criar dentro dele a classe Item conforme abaixo:

Com essa alteração o resultado da execução dos testes acusa o próximo erro, indicando que nosso objeto não contém um método save() (linha 12).

A solução para isso é transformar nosso objeto Item em um verdadeiro Model do Django, herdando da classe models.Model.

Com isso, o resultado dos nossos testes avançam e passam a emitir a próxima mensagem de erro dada abaixo (linha 14):

A mensagem de erro é bem longa mas o mais importante é a informação apresentada nas linhas 14 e 47, ou seja, apesar de termos utilizado o ORM para modelar como um objeto será armazenado no banco de dados relacional, para que tudo funcione é necessário utilizar um segundo sistema que irá criar esse banco de dados. Na terminologia do Django, o sistema responsável por isso se chama de migrações ou migrations.

Primeira Migração de Banco de Dados

O sistema de migrações pode se pensado como um sistema de controle de versões para banco de dados. Ocorrendo qualquer alteração nas classes que devem ter seus objetos armazenados, é necessário realizar uma nova migração.

Por hora, basta sabermos como realizar a operação de migração, e ela é feita com o comando abaixo:

Feito isso, ao tentar reexecutar os testes observamos que a execução dos mesmos vai até encontrar o atributo .text (linha 12) que ainda não está declarado em nossa classe e, portanto, não está presente nos objetos Item criados.

Para incluir esse atributo, vamos editar o arquivo lists/models.py conforme abaixo. Como pode ser observado, estamos utilizando o tipo TextField() para o atributo text. O Django oferece muitos outros tipos que podem ser encontrados na documentação oficial em https://docs.djangoproject.com/en/3.2/ref/models/fields/.

Entretanto, antes de reexecutarmos os testes, após a alteração, precisamos realizar uma nova migração do banco de dados. Alterou a classe Item é necessária nova migração.

Ao tentar realizar a migração, o Django nos alerta que não podemos deixar nenhum campo com valor null. Como deixamos, ele sugere duas possíveis alternativas. Escolhemos a segunda mas, caso queiramos eliminar esse problema, basta incluirmos o valor padrão na nossa classe conforme abaixo (linha 5) e realizar uma nova migração.

Com isso, ao reexecutar os testes de unidade não temos mais falhas.

Antes de prosseguirmos podemos colocar as alterações sob controle de versão com os comandos abaixo:

Salvando o POST no Banco de Dados

Com todos os testes unitários passando, está na hora de melhorarmos os testes unitários para garantir que um item submetido via POST seja salvo no banco de dados e, posteriormente, recuperado.

Para fazer isso, podemos modificar ligeiramente o teste test_can_save_a_POST_request, já presente no nosso conjunto de teste em lists/tests.py, conforme abaixo. Inserimos nele as linhas de 18 a 21. O que essas linhas fazem é 1) verificar se um novo item foi salvo no banco de dados; 2) recuperar o primeiro elemento salvo; e 3) verificar se o texto nesse elemento corresponde ao item submetido via POST.

Ao executar os testes unitários, o resultado agora é o exibido abaixo:

Uma possível correção em nossa view (lists/views.py) para resolver esse problema poderia ser a apresentada abaixo (linhas 6 a 8):

A solução acima faz os testes unitários passarem mas está longe de ser a solução ideal.

Por hora. podemos ainda fazer uma refatoração para eliminar uma redundância em nossa view da linha 11 acima.

Considerando a solução que chegamos até o momento, ela apresenta ainda ao menos dois problemas: 1) estamos salvando um item vazio a cada requisição para página inicial que não seja via POST; e 2) ainda não estamos armazenando no banco qualquer informação de quem a solicitou e, portanto, não conseguimos listar itens para pessoas distintas. Vamos anotar essas pendência para resolvê-las mais adiante.

Na verdade, podemos anotar em um rascunho as pendências que detectamos, tais como:

  • Não salvar itens em branco a cada requisição

  • Code smell: teste de POST é longo demais

  • Exibir vários itens da tabela

  • Aceitar mais de uma lista

" Sempre que identificamos problemas com antecedência, há a necessidade de fazer uma avaliação para saber se devemos parar o que estamos fazendo e recomeçar ou se devemos deixá-los de lado até mais tarde. Às vezes, terminar o que estamos fazendo ainda valerá mais a pena, enquanto em outras ocasiões o problema poderá ser tão significativo que justificaria parar e repensar." (Percival, 2017)

Podemos iniciar atacando o primeiro item da lista acima. Inicialmente, para garantir que nossa solução irá funcionar, vamos escrever um novo teste unitário para assegurar que uma chamada normal a nossa página não deveria gerar um Item salvo em banco de dados. Criamos então o teste test_only_saves_items_when_necessary conforme abaixo (linhas 15 a 17).

Ao executar esse teste teremos uma falha esperada pois Item.objects.count() está retornando 1 e deveria ser 0.

Vamos então corrigir nossa view para lidar com essa situação.

A solução acima trata com sucesso o nosso problema de salvar itens apenas quando a página aceitar requisições via POST. A variável new_item_text guarda o texto submetido via POST ou um string vazio em outras ocasiões. Além disso, observa-se que quando for uma requisição POST utilizamos em seguida o método Item.objects.create() que também cria um item e o armazena no banco sem a necessidade do save(). Com isso, conseguimos executar os testes com sucesso.

Vamos aproveitar o momento e colocar nosso código sob controle de versão antes de continuarmos.

Redirecionando Após o POST

Um dos princípios de programação Web é a recomendação de que sempre seja feito um redirecionamento após um POST. A alteração do código de nossa aplicação para atender essa demanda trará outros benefícios, tais como a eliminação da necessidade de atribuir um string vazio para o new_item_text, melhorando o código nesse aspecto também.

Como sempre, antes de iniciarmos as alterações no código vamos redigir um novo teste unitário, ou aprimorar um existente, que verifique se estamos redirecionando após um POST e retornando à página principal. Segue o código completo do arquivo lists/tests.py já com a alteração nas linhas 26 e 27.

Nessas linhas estavam os asserts abaixo:

Fizemos essa substituição pois não mais esperamos mais de resposta um .content renderizado contra um template, mas sim que tenha ocorrido um redirecionamento. Ao executar os testes unitários modificados temos o seguinte resultado:

Em função do erro, podemos agora alterar nossa aplicação para fazer esse teste passar. O código alterado do lists/views.py é mostrado abaixo:

E após essa alteração os testes unitários passam, conforme mostrado abaixo:

Melhoria nos Testes

Uma boa prática em relação a casos de teste é que deveríamos tentar fazer com que cada teste se concentre em apenas um aspecto do sistema. Infelizmente, da forma como está, um único teste está com mais responsabilidades do que deveria e merece ser refatorado. O teste problemático é o test_can_save_a_POST_request que não apenas verifica se um POST está permitindo salvar o dado submetido, mas também está verificando se o redirecionamento esta ocorrendo adequadamente. O teste alterado abaixo separa essas duas atribuições em testes distintos, teste test_can_save_a_POST_request, implementado nas linhas de 19 a 24, e test_redirects_after_POST, implementado nas linhas de 26 a 30.

Para ter certeza que a refatoração do teste unitário foi bem sucedida, segue o resultado da execução dos testes:

Com essa alteração, já cumprimos os dois primeiros itens da nossa lista de tarefas:

  • Não salvar itens em branco a cada requisição

  • Code smell: teste de POST é longo demais

  • Exibir vários itens da tabela

  • Aceitar mais de uma lista

Renderizando Itens no Template

O terceiro item da lista iremos cumprir nesta seção. Para isso, inicialmente vamos escrever um novo teste unitário para verificar se nosso template permite a exibição de mais de um item na lista. O novo teste (test_displays_all_list_itens) está entre as linhas 32 e 39 baixo.

Ao executar os testes unitários temos o erro AssertionError: 'itemey 1' not found in..., como exibido abaixo (linha 12).

Para oferecer uma solução para o template permitir tal exibição faremos uso da tag {% for .. in .. %} (linha 12) que permite iterar sobre listas dentro do template home.html e o código do mesmo fica como abaixo:

Agora resta apenas corrigir as variáveis utilizadas na view para que as informações corretas sejam passadas para o template.

Com isso os testes de unidade passam com sucesso:

Entretanto, ao executar os testes funcionais, esses ainda apresentam falha, conforme abaixo:

Como podemos observar, pela descrição apresentado acima, está bem complicado entender o que deve ser feito para resolver o problema. Algumas vezes procurar a raiz de uma falha não é uma atividade trivial. No caso acima, a solução é tentar carregar nosso site diretamente no navagador para tentar entender o que está ocorrendo.

Como podemos observar na imagem abaixo, ao carregar o site http://localhost:8000, aparece a tela com uma mensagem semelhante a essa:

Mensagem de depuração do Django (adaptado de Percival (2017))

Observa-se que a exceção ocorreu pois o Django não é capaz de encontrar uma tabela, denominada lists_items no banco de dados de produção. Com isso podemos concluir que nos testes unitários, a classe django.test.TestCase do Django oferece um mecanismo para o gerenciamento de um banco de dados interno facilitando nosso vida. Entretanto, os testes funcionais, estendem a classe unittest.TestCase e, desse modo, não possuem essa funcionalidade embutida neles.

A solução nesse caso é criar essa tabela em um banco de dados que possa ser utilizada em produção. O Django oferece um banco de dados em memória (SQLite) que pode ser utilizado. A configuração do mesmo está disponível no arquivo superlists/settings.py. Os dados armazenados são salvos em um arquivo chamado de db.sqlite3, localizado no diretório base do projeto, ou seja, no mesmo diretório do arquivo manage.py.

Configurado o banco de dados que será utilizado, basta usarmos um outro aplicativo do Django, denominado migrate para gerar as tabelas exigidas por nossa aplicação.

Executado o migrate, a página web acessada diretamente não exibe mais as mensagens de depuração e a execução dos testes funcionais avançam.

Aparentemente, pela mensagem de erro, o que está faltando é apenas corrigirmos a numeração dos itens de nossa lista. Podemos fazer isso alterando o nosso template conforme abaixo, utilizando a tag {{ forloop.counter }}:

Finalmente, ao executar os testes funcionais novamente paramos na instrução self.fail('Finish the test!'), encerrando assim mais uma etapa da implementação da história de usuário até esse ponto.

Entretanto, ao observar a página de nossa lista de itens, há um único inconveniente: a cada execução novos itens são duplicados e aparecem na tela da nossa aplicação.

Não é a solução ideal mas, no momento, podemos apenas remover o arquivo dq.sqlite3 e recriá-lo com os comandos abaixo:

Para encerrar essa parte, vamos colocar todo o código sob controle de versão:

Last updated

Was this helpful?