Persistência com Kotlin utilizando JDBC

Alex Felipe Victor Vieira
Alex Felipe Victor Vieira

Compartilhe

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.

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 Victor Vieira
Alex Felipe Victor Vieira

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Criador de mais de 40 cursos, como Kotlin, Flutter, Android, persistência de dados, comunicação com Web API, personalização de telas, testes automatizados, arquitetura de Apps e Firebase. É expert em Programação Orientada a Objetos, visando sempre compartilhar as boas práticas e tendências do mercado de desenvolvimento de software. Atuou 2 anos como editor de conteúdo no blog da Alura e hoje ainda escreve artigos técnicos.

Veja outros artigos sobre Programação