Kotlin - Rest Api completa com Spring Boot, Swagger, Mysql e docker #02

Kotlin - Rest Api completa com Spring Boot, Swagger, Mysql e docker #02

Parte 02 - Criando nossa classe de serviço e repositório

Agora que já temos nossa aplicação configurada, vamos falar um pouco de como iremos criar a estrutura de busca e manipulação dos dados. Em outro post vou abordar mais afundo sobre o tema, mas a ideia aqui é termos camadas separadas.

Antes de mais nada, precisamos criar o nosso modelo, a classe que vai conter as informações de um Produto. Criaremos o package model e dentro dele a classe em kotlin Product. Usaremos aqui o conceito de data class existente no Kotlin, economizando boas linhas que gastaríamos com Java por exemplo. Para nos auxiliar, vamos anotar essa classe como uma Entidade, com o @Entity e utilizar a anotação @Table para que quando iniciarmos a aplicação, ele identifique a tabela e crie se necessário.

package com.kotlin.crudexample.model

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.Table

@Entity
@Table(name = "Product")
data class Product(
    @Id @GeneratedValue val id: Long,
    val name: String,
)

Com nossa entidade criada, agora podemos trabalhar com o repositório. Criaremos um novo package chamado repository e vamos criar uma nova interface chamada ProductRepository:

package com.kotlin.crudexample.repository

import com.kotlin.crudexample.model.Product
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.stereotype.Repository

@Repository
@EnableJpaRepositories
interface ProductRepository : JpaRepository<Product, Long>

Veja que anotamos a interface com @Repository e @EnableJpaRepositories onde o spring nos auxilia com toda lógica por trás trabalhando como uma class de repositório JPA. Notamos também que expandimos a classe utilizando JpaRepository, passando um objeto e um id. No caso, iremos passar nossa entidade Product e dizer que o id é Long.

Agora, vamos tentar chamar esse Repositório e ver como o Spring se comporta. Iremos injetar ele em nosso Controller e ver quais métodos podemos acessar. Veja como o Kotlin deixa mais fácil injetarmos a interface de repositório. Basta passarmos como construtor e já podemos utilizar em nossas funcões:

package com.kotlin.crudexample.controller

import com.kotlin.crudexample.model.Product
import com.kotlin.crudexample.repository.ProductRepository
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class ProductController(val productRepository: ProductRepository) {

    @GetMapping("/products")
    fun getAll(): List<Product> = productRepository.findAll()
}

Em nosso método getAll(), alteramos o seu retorno chamando o nosso repositório de produto com o método de buscar todos itens da entidade findAll(). Se rodar a aplicação, veremos que agora teremos um retorno de uma lista vazia, já que não temos informações criadas em nossa base de dados. Rode a aplicação e veja nosso retorno vazio [] chamando http://localhost:8081/products.

Vamos inserir alguns dados? Então vamos criar nosso CRUD básico! Antes de prosseguir, aconselho ter instalado o Postman ou outro API Cliente de sua preferência para nos auxiliar nas chamadas das rotas (calma que logo mais chegamos no Swagger :D).

package com.kotlin.crudexample.controller

import com.kotlin.crudexample.model.Product
import com.kotlin.crudexample.repository.ProductRepository
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class ProductController(val productRepository: ProductRepository) {

    @GetMapping("/products")
    fun getAll(): List<Product> = productRepository.findAll()

    @GetMapping("/product/{id}")
    fun getById(@PathVariable(value = "id") id: Long): Product = productRepository.getById(id)

    @PutMapping("/product/{id}")
    fun update(@PathVariable(value = "id") id: Long, @RequestBody product: Product) {
        if (productRepository.existsById(id))
            productRepository.save(product)
    }

    @PostMapping("/product")
    fun create(@RequestBody product: Product) = productRepository.save(product)

    @DeleteMapping("/product/{id}")
    fun delete(@PathVariable(value = "id") id: Long) =
        takeIf { productRepository.existsById(id) }
            .let { productRepository.delete(productRepository.getById(id)) }
}

Agora nosso controller já consegue fazer algumas chamadas básicas. Veja que temos:

  • getAll() -> chamando nosso repositório e buscando todos itens da tabela Product;
  • getById(id) -> Passamos um id através da nossa rota (exemplo: /product/1) e então buscamos em nosso repositório o produto com o id que passamos;
  • update(id, body) -> Passamos um id junto com um corpo de mensagem. Esse corpo, a princípio vamos deixar como nossa entidade Product, então devemos passar o Id e Name no corpo (iremos refatorar mais a frente)
  • create(body) -> Chamamos o método save do repositório para criarmos o nosso novo produto;
  • delete(id) -> Iremos refatorar mais a frente, mas só para mostrar um pouco de como podemos trabalhar com Kotlin, podemos usar o takeIf para validarmos e termos um retorno boolean, se for true ele executa a próxima ação.

Em seu Postman, antes de realizar as chamadas, adicione aos seus Headers o Content-Type - application/json, conforme imagem

image.png

Então vamos inserir alguns dados, chamando a url http://localhost:8081/product como Post, passando em Body o novo produto Janela image.png Veja que o retorno foi o Status 200 e teremos a Janela criada com o Id 1. Adicione mais um produto chamado Porta, que irá retornar o Id 2. Perceba que com a anotação @GeneratedValue no Id de nossa Entidade, os Produtos são criados com o Id automaticamente, de forma crescente.

Ao chamarmos novamente nosso GET que lista os produtos http://localhost:8081/products teremos os nossos novos Produtos: Janela e Porta.

[
    {
        "id": 1,
        "name": "Janela"
    },
    {
        "id": 2,
        "name": "Porta"
    }
]

Vamos agora testar se o método de alterar está funcionando corretamente. Iremos fazer uma requisição PUT em http://localhost:8081/product/2 passando no corpo um novo nome, como por exemplo, Porta de Alumínio e o Id 2. (Obs: Você deve ter reparado que não ficou muito bacana essa função mas fique tranquilo pois Iremos refatorar mais pra frente)

Se fizemos uma requisição GET para listar a Porta, chamando http://localhost:8081/product/2 a aplicação deverá retornar exatamente o que solicitamos, Porta de Alumínio.

Para finalizarmos os testes iniciais, vamos testar se a aplicação está deletando corretamente um Produto. Vamos fazer a requisição DELETE no Postman, passando a url http://localhost:8081/product/2. Agora se tentarmos buscar o produto de Id 2, a aplicação irá exibir erro 500.

Legal! Tudo funcionando até aqui! Mas será que pode melhorar? Sempre!

Essas chamadas ao repositório direto na controller pode não ser um problema nesse momento mas caso nossa aplicação precise de algumas regras dentro das funções o controller vai acabar ficando grande e isso acaba não sendo uma boa prática. Uma solução, seria criarmos uma camada de Serviço, onde podemos criar funções específicas com algumas regras dentro. Por exemplo buscarProdutoPorNome ou outra função específica. Vamos então ao nosso próximo passo, criando o Service.

Crie um package chamado service e vamos criar um contrato para nosso serviço, vamos criar uma Interface do Kotlin chamada ProductService. Vamos anotar a nossa interface com @Service

package com.kotlin.crudexample.service

import com.kotlin.crudexample.model.Product
import org.springframework.stereotype.Service

@Service
interface ProductService {
    fun getAll(): List<Product>
    fun getById(id: Long): Product
    fun create(product: Product): Product
    fun update(product: Product): String
    fun delete(id: Long): String
}

Agora vamos para a implementação dessa interface, vamos criar nesse mesmo package a classe kotlin ProductServiceImpl. Aqui, nossa IDE pode nos ajudar ao implementar a interface class ProductServiceImpl : ProductService, podemos implementar os metodos da interface, ficando assim:

package com.kotlin.crudexample.service

import com.kotlin.crudexample.model.Product

class ProductServiceImpl : ProductService {
    override fun getAll(): List<Product> {
        TODO("Not yet implemented")
    }

    override fun getById(id: Long): Product {
        TODO("Not yet implemented")
    }

    override fun save(product: Product): Product {
        TODO("Not yet implemented")
    }

    override fun update(product: Product): String {
        TODO("Not yet implemented")
    }

    override fun delete(id: Long): String {
        TODO("Not yet implemented")
    }
}

Agora vamos injetar o repositório e fazer as chamadas nas funções


package com.kotlin.crudexample.service

import com.kotlin.crudexample.model.Product
import com.kotlin.crudexample.repository.ProductRepository
import org.springframework.stereotype.Service

@Service
class ProductServiceImpl(val productRepository: ProductRepository) : ProductService {

    override fun getAll(): List<Product> = productRepository.findAll()

    override fun getById(id: Long): Product = productRepository.getById(id)

    override fun create(product: Product): Product = productRepository.save(product)

    override fun update(product: Product): String {
        if (!productRepository.existsById(product.id))
            return "Produto não existe na base de dados"

        productRepository.save(product)

        return "Produto id ${product.id} alterado com sucesso"
    }

    override fun delete(id: Long): String {
        if (!productRepository.existsById(id))
            return "Produto não existe na base de dados"

        productRepository.deleteById(id)

        return "Produto removido com sucesso!"
    }
}

Repare que para obtermos os produtos ou criarmos, não colocamos regra, já para update e delete, coloquei uma validação e um retorno, informando se o Produto não existir na base ou se alterado/deletado com sucesso.

Você vai perceber que o código poderá quebrar aqui, pois teremos que ajustar nossa controller para ficar dessa maneira:

@RestController
class ProductController(val productService: ProductService) {

    @GetMapping("/products")
    fun getAll(): List<Product> = productService.getAll()

    @GetMapping("/product/{id}")
    fun getById(@PathVariable(value = "id") id: Long): Product = productService.getById(id)

    @PutMapping("/product")
    fun update(@RequestBody product: Product) = productService.update(product)

    @PostMapping("/product")
    fun create(@RequestBody product: Product) = productService.create(product)

    @DeleteMapping("/product/{id}")
    fun delete(@PathVariable(value = "id") id: Long) = productService.delete(id)
}

Agora nossa aplicação já tem as rotas necessárias para criar, ler, alterar ou deletar produtos. Em nosso controller, fizemos a chamada para nosso service, que por sua vez poderá conter algumas regras antes de chamar o repository. No exemplo mostrado, o update e o delete no service, retorna uma mensagem fixa mas podemos retornar um Status Http ou alguma outra mensagem parametrizada (podemos deixar em variaveis separadas), enfim, dá para alterar de acordo com a nossa necessidade. Aqui, tentei deixar o mais simples possível, mas em um post futuro podemos dar uma turbinada nessa aplicação.

Agora partiremos para a documentação da api com o Swagger, então nos vemos no próximo post que vc pode acessar clicando aqui https://rafaelmoura.dev/kotlin-rest-api-completa-03

E lembre-se, irei postando o código no Github, acesse aqui .

Dúvidas até aqui? Reclamações, sugestões? Pode mandar uma mensagem :D

Did you find this article valuable?

Support Rafael Moura by becoming a sponsor. Any amount is appreciated!