opções no HTML, num enum e no banco
No encontro 12, o projeto series ficou com as quatro operações do CRUD feitas pelo navegador:
Antes de salvar, o @Valid confere as regras da entidade, e o th:errors mostra a mensagem ao lado do campo quando alguma regra falha.
Este encontro é a distância. Vamos alterar o mesmo projeto series, passo a passo, e cada passo tem um bloco Confere com o que deve aparecer no navegador ou no banco antes de seguir.
Até aqui, todo campo dos nossos formulários é digitado: nome, plataforma, ano e gênero.
Hoje vamos ver como oferecer uma lista de opções para a pessoa escolher, com o elemento select, e três formas de montar essa lista:
Hoje a plataforma é um campo de texto, e o @NotBlank só garante que alguma coisa foi digitada. Ele não confere se o que foi digitado é uma plataforma que existe, nem se foi escrito sempre do mesmo jeito.
Vamos ver o problema no banco.
Para o banco, são quatro plataformas diferentes. Ou seja, qualquer relatório que conte séries por plataforma vai sair errado, e o erro nasceu no formulário, que deixou cada pessoa escrever do seu jeito.
O select é o elemento do HTML que mostra uma lista de opções, e a pessoa escolhe uma delas em vez de digitar:
Como a pessoa só consegue escolher o que está na lista, a grafia passa a ser sempre a mesma.
Cada option tem duas partes, e elas não precisam ser iguais:
Ao enviar o formulário, o navegador monta o par nome=valor com o name do select e o value da opção escolhida, do mesmo jeito que faz com um input.
No formulário de séries, trocamos o input pelo select. O th:field funciona no select do mesmo jeito que funcionava no input: ele gera sozinho o id e o name a partir do atributo plataforma da Serie.
Nada muda no controller nem na entidade, porque o navegador continua enviando plataforma=algum texto, e a Serie continua guardando a plataforma como String.
Na página de edição, o select precisa abrir com a plataforma que a série já tem. O th:field faz isso sozinho: ele compara o value de cada option com a plataforma da Serie e marca a que for igual com selected="selected".
É o mesmo trabalho que ele fazia no input, quando preenchia o value com o dado já salvo.
O select limita o que a pessoa escolhe no navegador, mas não limita o que chega ao servidor. Qualquer cliente HTTP pode enviar plataforma=Qualquer Coisa, e a Serie aceita, porque para o Java a plataforma ainda é uma String qualquer.
Além disso, a lista de plataformas está escrita duas vezes, em formulario.html e em edicao.html. Para acrescentar uma plataforma, precisamos lembrar de mudar as duas páginas.
No encontro 1, usamos um enum para resolver exatamente este problema no catálogo de jogos: a plataforma era uma String, e cada um escrevia de um jeito.
O enum é um tipo com um conjunto fechado de valores. Uma variável do tipo Plataforma só pode guardar um dos valores declarados nele, e o compilador recusa qualquer outro.
Criamos o enum Plataforma no pacote dominio. Cada constante carrega também o nome que a pessoa vai ver, como no PlataformaComFabricante do encontro 1:
Precisamos dos dois porque o nome de uma constante não pode ter espaço nem sinal de mais, e a pessoa quer ler "Prime Video" e "Disney+".
Na Serie, a plataforma deixa de ser String e passa a ser do tipo Plataforma. O getter e o setter mudam junto.
A regra também muda: o @NotBlank só serve para texto, então usamos @NotNull, que confere se alguma plataforma foi escolhida.
O banco não tem o tipo Plataforma, então o JPA precisa decidir como gravar o enum na coluna. Existem duas formas:
Sem a anotação, o JPA usa ORDINAL. Usamos STRING porque o banco fica legível, e porque com ORDINAL basta mudar a ordem das constantes no enum para todas as séries trocarem de plataforma.
A tabela serie já tem plataformas gravadas como texto livre, como "Netflix" e "Netflx". Ao ler essas linhas, o JPA tenta transformar cada texto numa constante do enum, não encontra, e a lista de séries para com 500.
Como estas séries são de teste, apagamos a tabela. Ao subir de novo, a aplicação cria a tabela serie outra vez, já com a coluna plataforma no formato do enum.
Para não escrever as opções à mão no HTML, o controller entrega à página a lista de plataformas, do mesmo jeito que entrega a Serie:
O mesmo acontece no método edicao, que monta a página de edição.
Quando alguma regra falha, o cadastrar devolve a mesma página do formulário. Essa página também tem o select, e por isso também precisa da lista de plataformas.
Por isso o cadastrar passa a receber o Model e coloca a lista antes de devolver o formulário. O atualizar, que devolve a página de edição quando a validação falha, recebe a mesma alteração.
Em vez de cinco option escritas à mão, temos uma só, repetida pelo th:each para cada plataforma da lista, como fizemos com as linhas da tabela na página de listagem:
O "Netflix" escrito dentro da option só aparece se o arquivo for aberto direto, sem o Thymeleaf. Na página gerada, ele é substituído.
A pessoa continua vendo "Prime Video", mas o navegador envia plataforma=PRIME_VIDEO. Ao montar a Serie, o Spring percebe que o atributo plataforma é um enum e procura a constante com esse nome exato, sem precisarmos escrever nenhuma conversão.
Se chegar um texto que não é o nome de nenhuma constante, a conversão falha, e o formulário volta com erro em vez de salvar.
Na listagem, ${serie.plataforma} agora é uma constante do enum. Se deixarmos assim, a tabela mostra PRIME_VIDEO.
Com ${serie.plataforma.nome}, o Thymeleaf chama getPlataforma() e, em seguida, getNome() da constante, e a tabela mostra Prime Video.
O enum funciona bem quando as opções são poucas e só mudam quando o próprio sistema muda. Por exemplo:
Nesses casos, uma opção nova vem sempre junto com uma regra nova no código, e faz sentido que só quem programa possa criá-la.
Plataformas de streaming aparecem e mudam de nome com frequência. Para acrescentar a Globoplay, alguém precisa mudar o enum, compilar e publicar a aplicação de novo.
E ainda tem um detalhe no banco: a coluna plataforma foi criada com a lista das cinco constantes que existiam naquele dia, e o ddl-auto=update não altera essa lista. Ao salvar uma série com GLOBOPLAY, o banco recusa o valor, e a aplicação responde 500.
Ou seja, quando quem usa o sistema precisa cadastrar novas opções, a lista não deveria estar no código, e sim no banco.
A ideia agora é guardar as plataformas numa tabela própria. Cada série deixa de guardar o nome da plataforma e passa a guardar só o id da linha correspondente, numa coluna plataforma_id.
Ou seja, é a chave estrangeira do encontro 4: cada série pertence a uma plataforma, e uma plataforma tem várias séries. Para criar a Globoplay, basta inserir uma linha nova na tabela plataforma.
O enum Plataforma é apagado, e no lugar dele criamos uma classe Plataforma com @Entity, no mesmo pacote dominio. Ela tem só id e nome, e vira a tabela plataforma.
É a mesma forma da Serie no encontro 9, com menos atributos.
Na Serie, o tipo do atributo continua Plataforma, mas agora é a entidade, e não mais o enum. Trocamos @Enumerated por @ManyToOne: muitas séries → uma plataforma.
Como no encontro 4, o lado com @ManyToOne é o dono do relacionamento, e é nele que o JPA cria a coluna plataforma_id, com a chave estrangeira apontando para a tabela plataforma.
Para buscar as plataformas no banco, criamos um DAO para a nova entidade, no pacote dao, seguindo exatamente a forma do SerieDAO: uma interface com as operações e uma implementação com o EntityManager.
Neste encontro, precisamos só de listar todas, para montar o select, e de buscar uma pelo id. O ORDER BY p.nome deixa a lista em ordem alfabética.
A tabela serie tem agora uma coluna plataforma no formato do enum, que não serve mais. Apagamos a tabela antes de subir a aplicação, e ela é criada de novo, agora com a coluna plataforma_id.
Ao subir, a aplicação também cria a tabela plataforma, vazia. Como ainda não temos uma página para cadastrar plataformas, inserimos as cinco pelo cliente do banco, depois que a aplicação subir.
O controller passa a receber também o PlataformaDAO no construtor, do mesmo jeito que recebe o SerieDAO.
Nos quatro métodos que devolvem um formulário, trocamos Plataforma.values() por plataformaDAO.listarTodas(). O nome "plataformas" continua o mesmo, então a página ainda encontra a lista com ${plataformas}.
O select muda em três lugares em relação ao do enum:
O for do label acompanha o id gerado pelo th:field, que agora é plataforma.id.
O navegador envia plataforma.id=3. O ponto no nome quer dizer: o id da plataforma da série. Ao montar a Serie, o Spring encontra a plataforma ainda vazia, cria uma Plataforma nova, coloca o 3 no id dela e a guarda na série.
Ou seja, a Serie chega ao controller com uma Plataforma que só tem o id preenchido. O nome continua null, porque o formulário não enviou nome nenhum.
O cadastrar não muda em relação à etapa do enum, a não ser pela lista de plataformas. Mesmo com a plataforma pela metade, o salvar funciona, porque a tabela serie não guarda a plataforma inteira: ela guarda só a coluna plataforma_id.
Logo, para gravar a chave estrangeira, o JPA só precisa do id da plataforma, e é exatamente o que o formulário mandou. O mesmo vale para o atualizar, na edição.
Na edição, a série vem do banco com a plataforma completa. O th:field="*{plataforma.id}" lê o id dessa plataforma e compara com o value de cada option, que também é um id. A option igual recebe selected.
Quando a validação falha e o formulário volta, a plataforma tem só o id, mas é só isso que o select precisa para continuar marcando a opção escolhida.
O @NotNull na plataforma confere se a série chegou ao controller com alguma plataforma. Pode acontecer:
Por isso o nosso select não tem uma opção em branco do tipo "Escolha uma plataforma": sempre há uma opção marcada, e a série sempre chega com uma plataforma.
A página de listagem não muda. Quando o SerieDAO busca as séries, o JPA busca também a plataforma de cada uma, porque o @ManyToOne carrega o outro lado junto.
Então ${serie.plataforma.nome} continua funcionando, agora chamando getNome() da entidade Plataforma, e não mais da constante do enum.
As duas formas montam o mesmo select, e a escolha depende de quem cria as opções:
E quando cada opção também precisa guardar outras informações, como o telefone de um cliente, ela já é uma entidade, e a tabela é o único caminho.

Quer trocar mais uma ideia? Me chama!