Persistência com Kotlin utilizando JDBC

Persistência com Kotlin utilizando JDBC

Neste artigo, vamos aprender como persistir dados com o Kotlin utilizando o JDBC (Java Database Connectivity ou Conectividade com banco de dados Java).

Projeto de exemplo

Como exemplo vou utilizar o Bytebank, um projeto Gradle que simula um banco digital, para cadastrar contas. O cadastro da conta exige número de conta, nome do cliente e saldo:

data class Conta(
    val numero: Int,
    val cliente: String,
    val saldo: Double
)

Na classe Conta representamos a nossa conta que pode ser criada da seguinte forma:

fun main() {
println("bem-vindo ao Bytebank")
    val contaAlex = Conta(1, "Alex", 1000.0)
println("informações da conta $contaAlex")
}

E chegamos a este resultado ao executar o programa:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Alex, saldo=1000.0)

Se quiser acompanhar os exemplos do artigo, você pode baixar o projeto inicial do Bytebank. Ele foi desenvolvido com o JDK 13, portanto, utilize a mesma versão para evitar incompatibilidades.

Com a introdução do projeto e a criação de conta, vamos começar com a configuração do JDBC.

Banner de divulgação da Imersão IA da Alura em colaboração com o Google. Mergulhe em Inteligência artificial com a Alura e o Google. Serão cinco aulas gratuitas para você aprender a usar IA na prática e desenvolver habilidades essenciais para o mercado de trabalho. Inscreva-se gratuitamente agora!

Adicionando o conector do banco de dados MySQL

Como primeiro passo, precisamos criar uma conexão entre o nosso aplicativo e o banco de dados. Neste exemplo, vou utilizar o MySQL, mas é possível realizar essa mesma conexão utilizando outros bancos de dados comuns no mercado, pois cada um deles oferece um conector capaz de realizar a comunicação. O MySQL, por exemplo, tem uma página exclusiva com todos os drivers disponíveis, até mesmo de outras aplicações.

Na página do MySQL temos a opção de fazer o download do conector, porém, considerando o uso de uma build tool (o Gradle), podemos adicionar a dependência no arquivo de build, o build.gradle.kts:

dependencies{
        implementation(kotlin("stdlib"))
        implementation("mysql:mysql-connector-java:8.0.15")
        testImplementation("junit", "junit", "4.12")
}

No projeto inicial a dependência está comentada. Remova o comentário e sincronize o projeto.

Em seguida, basta apenas sincronizar o projeto para que o Gradle faça o download e disponibilize o uso do driver. Então, podemos começar a configurar a conexão.

Criando a conexão

Com o driver acessível, podemos criar a conexão com o MySQL da seguinte maneira:

try {
    Class.forName("com.mysql.cj.jdbc.Driver")
    DriverManager.getConnection("jdbc:mysql://localhost/bytebank", "root", "")
    println("Conexão realizada com sucesso")
} catch (e: Exception) {
    e.printStackTrace()
    println("não foi possível conectar com o banco")
}

Temos bastante código aqui! Não se assuste, pois vamos entender o que cada linha de código significa:

  • Class.forName(): configura o conector que vamos utilizar na conexão com o JDBC. Se você estivesse com outro driver, teria de colocar a classe correspondente ao driver que está utilizando, que nesse caso é o do MySQL ("com.mysql.cj.jdbc.Driver").
  • DriverManager.getConnection(): tenta fazer a conexão com o banco de dados por meio do JDBC, como argumento recebe o endereço de conexão, usuário e senha.

Nesta chamada precisamos nos atentar principalmente ao endereço! Por exemplo, temos o padrão jdbc:mysql, que indica que vamos fazer uma conexão com o JDBC no banco de dados MySQL, ou seja, se você estiver fazendo a configuração para um outro banco de dados, o valor será diferente!

Observações: um outro ponto a se notar é que estou utilizando um banco de dados no meu computador, na porta 3306 (porta padrão do MySQL), por isso o localhost é o suficiente. Porém, em uma integração com um banco de dados externo, o endereço será diferente! Note também que indicamos o banco de dados que queremos acessar e, nesse caso, criei o banco bytebank. Fique à vontade para usar esse mesmo exemplo ou utilize outro de sua preferência.

Então, como segundo e terceiro argumento, precisamos enviar o usuário e a senha. Durante esse teste, vou utilizar o usuário root com uma senha vazia.

"Beleza! Entendi a configuração do conector e como criamos uma conexão, mas por que utilizamos um try catch?"

O try catch é necessário para que a nossa aplicação consiga identificar os problemas comuns durante a tentativa de conexão, por exemplo, um endereço inválido, uma falha na autenticação ou qualquer outro problema. Este é o motivo de apresentar a stack trace da exception e uma mensagem abaixo indicando que não foi possível conectar com o banco de dados.

Após a introdução de cada linha de código, ao executar o programa, temos o seguinte resultado:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Alex, saldo=1000.0)
Conexão realizada com sucesso

Pronto! Podemos começar a executar instruções com o MySQL.

Criando tabelas

Entre as possibilidades, a nossa primeira instrução com o MySQL será a criação da tabela que vai armazenar as informações das contas! Para isso vamos considerar a seguinte instrução SQL:

val sql = """
      CREATE TABLE contas (
          id INT PRIMARY KEY AUTO_INCREMENT,
          cliente VARCHAR(255),
          saldo DOUBLE
          );
      """.trimIndent()

Para criar a query, precisamos envolver toda a instrução em uma String (podemos utilizar string literal ou raw string). Então, precisamos preparar a nossa query a partir do método prepareStatement() da conexão e executá-la com o execute():

try {
    Class.forName("com.mysql.cj.jdbc.Driver")
    val conexao = DriverManager.getConnection("jdbc:mysql://localhost/bytebank", "root", "")
println("Conexão realizada com sucesso")

    val sql = """
        CREATE TABLE contas (
            id INT PRIMARY KEY AUTO_INCREMENT,
            cliente VARCHAR(255),
            saldo DOUBLE
            );
        """.trimIndent()

    val query = conexao.prepareStatement(sql)
    query.execute()

println("Tabela de contas criada")
} catch (e: Exception) {
    e.printStackTrace()
println("não foi possível conectar com o banco")
}

Ao executar o programa, temos o seguinte resultado no console:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Alex, saldo=1000.0)
Conexão realizada com sucesso
Tabela de contas criada

Uma mensagem de sucesso! Ao verificar o banco de dados a partir da instrução DESC contas:

mysql> DESC contas;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | int          | NO   | PRI | NULL    | auto_increment |
| cliente | varchar(255) | YES  |     | NULL    |                |
| saldo   | double       | YES  |     | NULL    |                |
+---------+--------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

Temos a nossa tabela! Agora podemos inserir contas no banco de dados!

Inserindo contas

Para inserir uma conta na tabela de contas, a princípio, realizamos passos similares ao que fizemos para criar a tabela, declaramos e preparamos a query e depois executamos:

val insereContaSql = "INSERT INTO contas (cliente, saldo) VALUES (?, ?);"

val queryInsereConta = conexao.prepareStatement(insereContaSql)
queryInsereConta.execute()

println("conta registrada: $contaAlex")

A grande diferença é que precisamos realizar o processo de binding para vincular os dados do nosso objeto com a query. Para isso utilizamos os setters do PrepareStatement:

val insereContaSql = "INSERT INTO contas (cliente, saldo) VALUES (?, ?);"

val queryInsereConta = conexao.prepareStatement(insereContaSql)
queryInsereConta.setString(1, contaAlex.cliente)
queryInsereConta.setDouble(2, contaAlex.saldo)
queryInsereConta.execute()

println("conta registrada: $contaAlex")

"Mas por que não concatenamos direto as informações do objeto na instrução SQL?"

É bem comum surgir esse tipo de dúvida ao ver essa solução quando utilizamos a técnica de vínculo de dados, para evitar problemas de segurança, como o SQL Injection. Note que nessa técnica utilizamos o valor 1 para o setString(), que recebe o cliente como argumento, e o valor 2 para o setDouble(), que recebe o saldo.

Isso significa que o valor 1 representa a primeira coluna (cliente) e o 2 a segunda coluna (saldo), em casos de mais colunas, basta adicionar os demais números sucessivamente, como 3, 4...

Após realizar o processo de binding, podemos executar o programa, porém, é importante comentar ou remover o código de criação de tabela, pois se não temos a seguinte exception:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Flex, saldo=2000.0)
Conexão realizada com sucesso
java.sql.SQLSyntaxErrorException: Table 'contas' already exists
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970)
    at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:387)
    at br.com.alura.bytebank.MainKt.main(Main.kt:25)
    at br.com.alura.bytebank.MainKt.main(Main.kt)
não foi possível conectar com o banco

Note que é a java.sql.SQLSyntaxErrorException indicando que a tabela contas já foi criada… Podemos utilizar algumas técnicas para evitar o problema, por exemplo, usar um if na instrução de criação de tabela:

val sql = """
      CREATE TABLE NOT IF EXISTS contas (
          id INT PRIMARY KEY AUTO_INCREMENT,
          cliente VARCHAR(255),
          saldo DOUBLE
          );
      """.trimIndent() 

Ao testar o programa novamente, a nossa conta é inserida na tabela:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Alex, saldo=1000.0)
Conexão realizada com sucesso
conta registrada: Conta(numero=1, cliente=Alex, saldo=1000.0)

Podemos até mesmo conferir o resultado direto no MySQL:

mysql> SELECT * FROM contas;
+----+---------+-------+
| id | cliente | saldo |
+----+---------+-------+
|  1 | Alex    |  1000 |
+----+---------+-------+
1 row in set (0.00 sec)

Agora que aprendemos a salvar contas, podemos começar a implementação da busca de contas.

Buscando contas

Para buscar as contas, realizamos o mesmo procedimento, mas a diferença é que agora utilizamos o executeQuery() que devolve um ResultSet, que representa uma tabela do banco de dados de acordo com a query executada.

val buscaContas = "SELECT * FROM contas;"
val buscaContasQuery = conexao.prepareStatement(buscaContas)
val resultado = buscaContasQuery.executeQuery()

Nessa query, especificamente, temos acesso a todas as contas cadastradas!

Para que seja possível pegar cada conta, precisamos fazer uma iteração em cada linha do ResultSet, podemos fazer essa iteração com o método next() que vai para a próxima linha do ResultSet e devolve true, se existirem dados, ou false, se não existirem:

val buscaContas = "SELECT * FROM contas;"
val buscaContasQuery = conexao.prepareStatement(buscaContas)
val resultado = buscaContasQuery.executeQuery()
while (resultado.next()){
    val numero = resultado.getInt(1)
    val cliente = resultado.getString(2)
    val saldo = resultado.getDouble(3)
    val conta = Conta(numero, cliente, saldo)
        println("conta devolvida $conta")
}

Dessa forma, para cada linha, podemos pegar o valor das colunas a partir dos getters, por exemplo, na primeira coluna que representa o id do tipo Int, utilizamos o getInt() com o argumento 1 indicando ser a primeira coluna, na segunda utilizamos o getString() com o argumento 2 para pegar a segunda coluna que é o cliente e assim sucessivamente…

Antes de executar o programa, podemos até mesmo salvar uma nova conta para o resultado apresentar mais de uma conta:

bem-vindo ao Bytebank
informações da conta Conta(numero=1, cliente=Fran, saldo=2000.0)
Conexão realizada com sucesso
conta devolvida Conta(numero=1, cliente=Alex, saldo=1000.0)
conta devolvida Conta(numero=2, cliente=Fran, saldo=2000.0)

Note que mesmo com o número de conta 1, na conta da Fran foi registrado com o valor 2. Isso acontece por não enviar o número da conta no processo de binding e por manter a configuração de incremento automático na tabela.

Um dos problemas com o JDBC

O grande detalhe desta solução é que precisamos saber exatamente o tipo de valor que desejamos pegar para cada coluna, pois se fizermos um getInt() e o valor da coluna for um texto (string), temos uma exception na conversão:

java.lang.NumberFormatException: For input string: "Alex"
    at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
    at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.base/java.lang.Double.parseDouble(Double.java:549)
    at com.mysql.cj.protocol.a.MysqlTextValueDecoder.decodeDouble(MysqlTextValueDecoder.java:228)
    at com.mysql.cj.result.StringConverter.createFromBytes(StringConverter.java:114)
    at com.mysql.cj.protocol.a.MysqlTextValueDecoder.decodeByteArray(MysqlTextValueDecoder.java:238)
    at com.mysql.cj.protocol.result.AbstractResultsetRow.decodeAndCreateReturnValue(AbstractResultsetRow.java:143)
    at com.mysql.cj.protocol.result.AbstractResultsetRow.getValueFromBytes(AbstractResultsetRow.java:250)
    at com.mysql.cj.protocol.a.result.ByteArrayRow.getValue(ByteArrayRow.java:91)
    at com.mysql.cj.jdbc.result.ResultSetImpl.getNonStringValueFromRow(ResultSetImpl.java:656)
    at com.mysql.cj.jdbc.result.ResultSetImpl.getInt(ResultSetImpl.java:896)
    at br.com.alura.bytebank.MainKt.main(Main.kt:43)
    at br.com.alura.bytebank.MainKt.main(Main.kt)

Você pode fazer essa simulação ao tentar pegar a coluna de cliente como se fosse um inteiro val teste = resultado.getInt(2).

Este é um dos detalhes/problemas do JDBC. Note que ele mostra a exception NumberFormatException indicando a entrada em string com o valor Alex.

Conclusão

Conforme apresentado neste artigo, podemos utilizar as mesmas soluções em Java usando o Kotlin. Se você já teve contato com o JDBC, provavelmente não deve ter notado tanta diferença com a implementação em Java, pois com o Kotlin podemos explorar todo o conceito de interoperabilidade com o Java, o que permite utilizar as bibliotecas em Java no Kotlin! Isso significa que você também pode ir além e até mesmo utilizar a JPA com o Hibernate, por exemplo.

Projeto final

Você pode acessar o código do projeto final a partir deste repositório do GitHub. A grande diferença é que os comportamentos foram extraídos para funções, indicando as ações de criação de tabela, inserção e busca de contas.

Caso seja o seu primeiro contato com o JDBC e você tenha interesse em aprender mais sobre essa biblioteca, aqui na Alura temos o curso de JDBC em Java, que além de apresentar uma introdução, também explica boas práticas, como DAO, connection pool, Data Sources e outros recursos importantes!

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.

Veja outros artigos sobre Programação