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.
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 bancobytebank
. 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 valor2
. 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!