Implementando uma CRUD API no Spring Boot com Kotlin — parte 2
No artigo anterior, criamos uma API REST capaz de criar e listar notas. Entretanto, a nossa proposta é criar uma CRUD API, ou seja, precisamos também adicionar as funcionalidades de alterar e remover as notas.
Sendo assim, neste artigo veremos como podemos tanto alterar como remover um recurso!
Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉
Para começar a brincadeira, vamos criar a função alter()
dentro do nosso NoteController
que vai ficar responsável em receber requisições capazes de alterar uma nota:
Tanto na função list()
como também na função add()
adicionamos mapeamentos do protocolo HTTP que indicam uma ação, por exemplo, para requisições do tipo GET pegamos nossos recursos e no tipo POST adicionamos um recurso…
Em outras palavras, como podemos indicar que queremos alterar um recurso considerando a arquitetura REST?
Mapeando a função para alterar um recurso
No HTTP, indicamos que queremos alterar um recurso por meio de requisições do tipo PUT. Sendo assim, vamos adicionar a annotation @PutMapping
na função alter()
:
Da mesma forma como fizemos na função add()
, precisamos também de um objeto do tipo Note
que vai ser recebido no corpo da requisição:
Até o momento não temos muita novidade ao que vimos anteriormente, porém, já que pretendemos alterar algo que exista na API, precisamos de alguns pontos de atenção, sendo assim, vamos entendê-los.
Entendendo a técnica para alterar recursos do REST
Em uma API REST, quando pretendemos operar com um recurso que exista, precisamos, primeiramente de uma informação que identifique o recurso.
Isso significa que além de enviar o objeto JSON com as informações que queremos alterar, precisamos também indicar, de forma explícita, um valor que identifique o recurso que queremos alterar.
No nosso caso, a maneira pela qual podemos identificar o nosso recurso Note
é por meio do seu id
, concorda?
“Legal, mas como podemos indicar para a requisição que queremos alterar um recurso de id específico?”
Uma das maneiras comuns para enviarmos tal informação é por meio do caminho da URL, por exemplo, já que acessamos nossos recursos com o caminho /notes, poderíamos enviar ids da seguinte maneira:
- /notes/1
- /notes/2
- /notes/3
Dessa forma, temos a capacidade de realizar requisições com a intenção de alterar um recurso especificando o seu id.
Entretanto, como podemos mapear esse tipo de caminho no nosso Controller?
Adicionando parâmetros no mapeamento
Todas as annotations de mapeamento do Spring recebem uma String
via parâmetro para indicar sob qual URL elas irão atender, por exemplo, se chegarmos na @PutMapping
e adicionarmos o valor "id"
:
Significa que a nossa função alter()
será chamada em uma requisição HTTP do tipo PUT com o seguinte valor http://localhost:8080/notes/id, ou seja, temos a capacidade de ajustar a forma pela qual uma função de um Controller é chamada.
Entretanto, perceba que dessa forma, fazemos com que o valor id seja considerado valor fixo que não muda! Sendo que a nossa ideia é fazer com que, nessa parte da URL, consigamos enviar valores variáveis… E agora?
Mapeando caminhos variáveis
Além de apenas declarar caminhos fixos, os mapeamentos do Spring também nos permite adicionar variáveis!
Isso significa que podemos indicar que a String "id"
é uma variável adicionando chaves:
Com essa configuração, a requisição será do tipo PUT com o caminho /notes, porém, temos a capacidade de mandar um valor qualquer logo em seguida!
Mas ainda tem um detalhe importante, pois da maneira como está configurado agora, em nenhum momento estamos pegando o valor variável do caminho…
Recebendo variáveis a partir da URL
Da mesma maneira como fizemos para receber a nossa nota via corpo da requisição, podemos também receber as variáveis da URL pedindo por um parâmetro que a representa, como por exemplo, um id
do mesmo tipo do id
da classe Note
, nesse caso, o tipo Long
:
Entretanto, repara que da mesma maneira como identificamos o objeto do tipo Note
como corpo da requisição, precisamos indicar que o parâmetro id
faz parte de uma variável da URL. Para isso utilizamos a annotation @PathVariable
:
Pronto, configuramos o nosso mapeamento, mas repara que ainda não estamos fazendo nada com os parâmetros que estamos recebendo…
Em outras palavras, temos que alterar o objeto que tenha o mesmo id
que enviamos.
Buscando uma nota pelo seu id
Considerando que iremos alterar um recurso que exista no nosso banco de dados, a princípio precisamos garantir se o mesmo existe, ou seja, temos que pedir para o Spring Data nos indicar se, a partir do id
recebido, temos uma nota salva.
Para isso, podemos utilizar a função exists()
do nosso noteResository
enviando o id
:
A partir da função exists()
o Spring Data retorna true
caso exista e false
caso contrário. Portanto, podemos adicionar um if
para tomarmos uma ação caso existir uma nota com o id
enviado:
Com a garantia da existência da nota, precisamos apenas salvar a nota que recebemos com a função save()
da mesma forma como fizemos na função add()
:
Nesse momento você deve estar pensando:
“Mas a função
save()
não é uma função para salvar objetos novos?"
Além de apenas salvar objetos novos, a função save()
também altera um objeto que já exista! Ou seja, se tiver um objeto com o mesmo id
no banco ele já faz esse serviço pra gente.
Conseguimos alterar o objeto! Porém, para verificar se realmente a nota será alterada, vamos retornar a função save()
que devolve o objeto que foi alterado:
Repara que além de retornar o objeto que foi salvo, estamos retornando uma instância vazia, pois dessa forma, quando não alterarmos uma nota, indicaremos que nada foi alterado com o objeto vazio, ou seja, com os valores padrões.
Pronto, implementamos! Agora vamos testar \o/
Testando a requisição PUT
Para realizar o teste, primeiro precisamos verificar quais notas temos. Atualmente tenho as seguintes notas quando faço uma requisição do tipo GET:
Repara que tenho tanto a nota 1 como também a 2, sendo assim, vou alterar a nota 1:
Veja que agora conseguimos alterar a nossa nota, mas surgiu um detalhe engraçado…
Faz sentido enviarmos o id
dentro do objeto da nota?
Se já enviamos o id
via variável na URL, significa que iremos alterar a nota que contenha esse id
, concorda? Inclusive, esse tipo de comportamento abre possibilidades para falhas comuns, como por exemplo, esse tipo de requisição:
Veja que a nossa aplicação apresentou um comportamento um tanto quanto estranho, isto é, pedimos para alterar uma nota de id
1 e foi salva uma nota nova de id
3…
“Por que isso aconteceu?”
Cuidados ao implementar API REST
Se observamos novamente o nosso código:
Atualmente estamos evitando a situação na qual tentamos alterar uma nota de id
que não exista e podemos até ver isso no seguinte teste:
Repara que tentamos alterar uma nota de id
4 e retorna uma resposta 200, mas, devolve um objeto com os valores padrões, inclusive, se buscarmos todas as notas:
Veja que só aparece as 3 notas que foram salvas.
“Então por que naquela situação deu problema?“
O detalhe é que quando enviamos um objeto sem id
, o Hibernate interpreta como um objeto novo, ou seja, ele vai salvar da mesma maneira como fazemos na função add()
. Nesse momento você deve estar pensando,
“Então é só tomarmos cuidado quando a nota não tiver um
id
?”
Não somente isso, pois também pode existir esse outro caso:
Veja que dessa vez tentamos alterar uma nota de id
3 via variável na URL, mas enviamos um id
que ainda não foi persistido no objeto JSON, como é o caso do 10, mas foi salvo uma nota de id
4.
Ações para defender a API REST
Se analisarmos num contexto geral, não faz sentido o cliente ter a possibilidade de manipular a informação de id
dos recursos, pois quando queremos alterar um objeto, o id
do mesmo, será recebido via variável da URL e, nos casos em que salvarmos uma nota nova, o servidor vai ficar responsável em gerar o id
.
Impedindo a deserialização do id
Já que estamos utilizando o Jackson, podemos bloquear a deserialização do id
com a annotation @JsonProperty
enviando os parâmetros value = “id"
e access = JsonProperty.Access.READ_ONLY
:
Desta maneira, evitamos as seguintes situações não desejadas:
- Quando alteramos uma nota com um
id
que existe, mas o objeto enviado via JSON possui umid
diferente que compromete um recurso existente ou cria um novo - Quando criamos um objeto novo e o objeto JSON está com um
id
que exista, então ele altera um recurso existente
Entretanto, ainda estamos tentando alterar uma nota de um objeto que não tem id
, ou seja, quando entramos dentro do if
precisamos fazer com que a nota que vai ser salva, contenha o mesmo id
recebido via variável da URL.
Criando um objeto com as informações desejadas
Uma das técnicas que podemos fazer é criar uma nova instância adicionando as informações do id
e do objeto note
recebido via parâmetro da função alter()
:
Veja que criamos o objeto safeNote
que contém as informação que esperamos, ou seja, estamos protegendo a nossa API com uma nota segura.
Mas já que estamos no Kotlin, será que existe uma possibilidade mais objetiva?
Ao invés de ficar criando uma nova instância manualmente cada vez que vier um objeto, temos a capacidade de utilizar uma feature do Kotlin capaz de nos ajudar nessa tarefa.
Conhecendo a feature Data Classes
Além de declararmos classes como vimos até agora, no Kotlin temos a capacidade de declarar Data Classes. Para declarar uma Data Class, basta apenas colocar o prefixo a keyword data
:
De modo geral, o objetivo deste tipo de classe (existem outros tipos hehe) é armazenar dados, porém, além disso, ela também já implementa algumas funções padrões baseando-se nas properties que foram declaradas via construtor primário:
equals()
/hashCode()
: para comparar objetos;toString()
: com o seguinte padrão"Note(id=valorDoId, title=valorDoTítulo, description=descriçãoDaNota)"
;componentN()
: indicar a ordem das properties quando utilizamos o recurso de Destructuring Declaration;copy()
: realiza a cópia do objeto.
Existem regras e outros detalhes importantes sobre Data Classes, mas, é um assunto para outro momento. Entretanto, fique à vontade em perguntar sobre o assunto ou consultar a documentação.
Dentre as funções disponíveis, a que pode nos ajudar é justamente a copy()
, considerando o nome da função, já sabemos que ela nos permite copiar um objeto.
Entretanto, ela também permite modificar o valor de uma property durante a cópia, como por exemplo, no momento que criamos a nossa safeNote
:
Estamos fazendo novamente uma cópia do objeto recebido com o id
desejado, porém, de uma maneira muito mais objetiva! Vamos testar?
Testando novamente a API
Primeiro começaremos com o fluxo normal, enviando o id
apenas via variável da URL:
Até o momento nenhuma novidade. Nosso segundo teste é verificar o que acontece quando enviamos um id
diferente quando tentamos alterar um recurso:
Uhul! O primeiro caso problemático já era 😄
Mas, e quando tentamos enviar um id
que já exista?
Veja que pedimos para alterar uma nota de id
1, mas enviamos uma nota de id
3 no corpo do objeto JSON, que por sinal existe no banco de dados, porém, a nossa API foi capaz de evitar esse tipo de problema!
Agora o último caso problemático que só comentamos mas não testamos, é quando tentamos criar uma nova nota enviando um id
no objeto JSON de um recurso que já existe.
Para testar esse comportamento basta apenas realizar uma requisição POST:
Veja que agora deixamos a nossa API segura! Mas ainda precisamos finalizar a nossa proposta inicial que é criar uma CRUD API, portanto, vamos começar a com a implementação da removação de notas.
Implementando a função para remover recursos
Para essa feature precisamos fazer algo similar ao que fizemos na função alter()
, ou seja, primeiro vamos declarar a função delete()
:
Mapeando função para remover um recurso
Em seguida, vamos mapear essa função para que indique ao HTTP que desejamos realizar uma remoção no recurso. Para isso utilizamos o verbo DELETE, nem preciso falar que vamos utilizar a annotation @DeleteMapping
, né?
Estamos com a função declarada, porém, como implementamos uma função que remove um recurso?
Basicamente precisamos apenas do id
do recurso, portanto, vamos receber esse id
da mesma maneira como fizemos na função alter()
:
Então, da mesma maneira, verificamos se a nota existe:
E então, basta apenas pedir para o noteRepository
remover com a função delete()
enviando o id
via parâmetro:
Para testar, basta apenas realizar uma requisição do tipo DELETE, no meu caso tenho notas com id do 1 a 5, vou remover a nota de id 2:
Repara que dessa vez não precisamos enviar nada via corpo da requisição, como também, recebemos como resposta 200 sendo que não retornamos nada na função delete()
… Vamos pegar todas as notas e ver se a de id
2 foi removida:
Veja que funcionou! Legal, mas agora, pra finalizar, vamos tentar remover uma nota de id
que não existe:
Veja que temos a mesma resposta 200, agora vamos pegar todas as notas novamente pra garantir se está tudo certo:
Nada aconteceu, ou melhor, finalizamos a nossa CRUD API REST! \o/
Código fonte
Caso surgir alguma dúvida, fique à vontade em consultar o código fonte que deixei no repositório no GitHub.
Para saber mais
Por mais que tenhamos finalizados a implementação da CRUD API na arquitetura REST, ainda existem pontos que vale considerar durante a implementação, como por exemplo:
- Utilização da camada Service para ficar entre o Controller e o Repository, pois, dessa forma, evitamos que o Controller lide com responsabilidade que não é dele, como é o caso de realizar comunicação direta com o Repository ou qualquer outra entidade.
- Retornar a classe
HttpEntity
ao invés do objeto direto para que seja possível se comunicar de maneira adequada com os clientes que consumirem a API.
Claro, ainda existem muitos outros detalhes que valem a pena aprender dentro do ecossistema do Spring Framework, como por exemplo, o projeto Spring Data REST ou o Spring Security.
Conclusão
Neste artigo aprendemos a implementar as funcionalidades tanto para alterar como para remover um recurso dentro de uma API REST.
Vimos que nesse tipo de implementação é muito comum realizarmos operações em recursos que existam, ou seja, quando alteramos ou removemos um recurso, primeiro precisamos garantir se o mesmo existe para depois tomarmos uma ação.
Além disso, aprendemos uma feature nova do Kotlin, que é justamente o Data Class que além de armazenar as informações da classe, implementa algumas funções que são úteis no nosso dia a dia, como por exemplo o copy()
que nos ajudou no processo manipular um objeto mantendo as informações desejadas.
Antes que eu esqueça, me conta sobre o que achou da implementação da CRUD API REST. Caso tiver dúvida fique à vontade em perguntar também 😄