Depois de anos em desenvolvimento, o Exposed 1.0 finalmente chegou — e é uma release que muda o jogo para quem trabalha com banco de dados em Kotlin. O framework SQL da JetBrains agora oferece suporte oficial a R2DBC, uma API estável com garantia de compatibilidade, e melhorias significativas de performance.
Se você já conhece o Exposed pelo nosso tutorial completo do framework, prepare-se: a versão 1.0 traz tudo que a comunidade vinha pedindo. Vamos ver o que mudou na prática.
O que há de novo no Exposed 1.0?
1. Suporte a R2DBC (Reactive Relational Database Connectivity)
Essa era a feature mais pedida pela comunidade. Até a versão 0.x, o Exposed trabalhava exclusivamente com JDBC, o que significava acesso bloqueante ao banco. Agora, com o módulo exposed-r2dbc, você pode usar drivers reativos para acesso não-bloqueante.
Os bancos suportados via R2DBC são:
| Banco | Driver R2DBC |
|---|---|
| PostgreSQL | r2dbc-postgresql |
| MySQL | r2dbc-mysql |
| MariaDB | r2dbc-mariadb |
| H2 | r2dbc-h2 |
| Oracle | r2dbc-oracle |
| SQL Server | r2dbc-mssql |
2. API estável
Com a versão 1.0, a JetBrains garante que não haverá breaking changes até a próxima major release. Isso significa que você pode adotar o Exposed em produção com confiança de que atualizações menores não vão quebrar seu código.
3. Melhorias de performance
A 1.0 traz otimizações internas no processamento de queries e no gerenciamento de conexões, resultando em menor overhead por transação.
Setup com R2DBC
Primeiro, adicione as dependências no build.gradle.kts:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.20"
}
repositories {
mavenCentral()
}
dependencies {
// Módulo R2DBC do Exposed
implementation("org.jetbrains.exposed:exposed-r2dbc:1.0.0")
// Driver R2DBC para PostgreSQL
implementation("org.postgresql:r2dbc-postgresql:1.0.7.RELEASE")
// Coroutines (necessário para R2DBC)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.10.1")
}
Se quiser manter o acesso JDBC tradicional junto com R2DBC, basta adicionar ambos os módulos — eles coexistem sem conflito. Para uma aplicação web completa, use o tutorial de Ktor com Exposed como base e depois substitua a camada JDBC por R2DBC quando fizer sentido.
Conectando com R2DBC
A conexão R2DBC usa a classe R2dbcDatabase:
import org.jetbrains.exposed.sql.r2dbc.R2dbcDatabase
import org.jetbrains.exposed.sql.r2dbc.R2dbcDatabaseConfig
import io.r2dbc.spi.ConnectionFactoryOptions
import io.r2dbc.spi.IsolationLevel
val database = R2dbcDatabase.connect(
url = "r2dbc:postgresql://localhost:5432/kotlinbrasil",
databaseConfig = R2dbcDatabaseConfig {
defaultMaxAttempts = 3
defaultR2dbcIsolationLevel = IsolationLevel.READ_COMMITTED
connectionFactoryOptions {
option(ConnectionFactoryOptions.USER, "dev_kotlin")
option(ConnectionFactoryOptions.PASSWORD, "senha_segura")
}
}
)
Note que a URL segue o padrão r2dbc:driver://host:porta/banco, diferente do jdbc: tradicional.
Definindo tabelas (mesmo esquema de antes)
A boa notícia é que a definição de tabelas não muda. Se você já tem tabelas definidas com Exposed DSL, elas funcionam tanto com JDBC quanto com R2DBC:
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.datetime
import java.time.LocalDateTime
object Desenvolvedores : Table("desenvolvedores") {
val id = integer("id").autoIncrement()
val nome = varchar("nome", 100)
val linguagem = varchar("linguagem", 50).default("Kotlin")
val senioridade = varchar("senioridade", 20)
val salario = decimal("salario", 10, 2)
val criadoEm = datetime("criado_em").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}
object Projetos : Table("projetos") {
val id = integer("id").autoIncrement()
val nome = varchar("nome", 200)
val descricao = text("descricao")
val desenvolvedorId = integer("desenvolvedor_id").references(Desenvolvedores.id)
val ativo = bool("ativo").default(true)
override val primaryKey = PrimaryKey(id)
}
Se termos como Table, varchar ou autoIncrement parecem novos, confira nosso glossário de termos Kotlin e o tutorial de classes e objetos.
CRUD reativo com R2DBC
Agora vem a parte interessante: todas as operações com R2DBC são suspend functions, integrando perfeitamente com coroutines do Kotlin.
Criando tabelas e inserindo dados
import org.jetbrains.exposed.sql.r2dbc.suspendTransaction
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.batchInsert
suspend fun inicializarBanco() {
suspendTransaction(db = database) {
SchemaUtils.create(Desenvolvedores, Projetos)
}
}
suspend fun inserirDesenvolvedores() {
suspendTransaction(db = database) {
// Inserção individual
Desenvolvedores.insert {
it[nome] = "Ana Silva"
it[linguagem] = "Kotlin"
it[senioridade] = "senior"
it[salario] = 22000.toBigDecimal()
}
// Inserção em lote — mais eficiente
val devs = listOf(
Triple("Carlos Lima", "pleno", 12000),
Triple("Maria Santos", "junior", 5500),
Triple("Pedro Costa", "senior", 20000),
)
Desenvolvedores.batchInsert(devs) { (nome, nivel, sal) ->
this[Desenvolvedores.nome] = nome
this[Desenvolvedores.senioridade] = nivel
this[Desenvolvedores.salario] = sal.toBigDecimal()
}
}
}
Consultas reativas
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.andWhere
suspend fun buscarDevsSenior(): List<String> {
return suspendTransaction(db = database) {
Desenvolvedores
.selectAll()
.where { Desenvolvedores.senioridade eq "senior" }
.andWhere { Desenvolvedores.salario greaterEq 15000.toBigDecimal() }
.map { row -> "${row[Desenvolvedores.nome]} - R$ ${row[Desenvolvedores.salario]}" }
}
}
suspend fun buscarDevsComProjetos(): List<Pair<String, String>> {
return suspendTransaction(db = database) {
(Desenvolvedores innerJoin Projetos)
.selectAll()
.where { Projetos.ativo eq true }
.map { row ->
row[Desenvolvedores.nome] to row[Projetos.nome]
}
}
}
A diferença principal para o JDBC é o uso de suspendTransaction em vez de transaction. O código interno é praticamente idêntico.
Atualização e exclusão
import org.jetbrains.exposed.sql.update
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
suspend fun promoverDev(nome: String, novoSalario: Int) {
suspendTransaction(db = database) {
Desenvolvedores.update(
where = { Desenvolvedores.nome eq nome }
) {
it[senioridade] = "senior"
it[salario] = novoSalario.toBigDecimal()
}
}
}
suspend fun removerDevsInativos() {
suspendTransaction(db = database) {
// Remove projetos inativos primeiro (integridade referencial)
Projetos.deleteWhere { Projetos.ativo eq false }
}
}
Integração com Spring Boot
O Exposed 1.0 trouxe melhorias significativas para quem usa Spring Boot com Kotlin. Uma das mais impactantes é que agora você pode usar a DSL do Exposed diretamente em métodos anotados com @Transactional, sem precisar envolver o código em blocos transaction { } explícitos:
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.jetbrains.exposed.sql.selectAll
@Service
class DesenvolvedorService {
@Transactional(readOnly = true)
fun listarTodos(): List<DesenvolvedorDTO> {
return Desenvolvedores
.selectAll()
.map { row ->
DesenvolvedorDTO(
id = row[Desenvolvedores.id],
nome = row[Desenvolvedores.nome],
senioridade = row[Desenvolvedores.senioridade],
salario = row[Desenvolvedores.salario]
)
}
}
@Transactional
fun atualizarSalario(id: Int, novoSalario: Int) {
Desenvolvedores.update(
where = { Desenvolvedores.id eq id }
) {
it[salario] = novoSalario.toBigDecimal()
}
}
}
data class DesenvolvedorDTO(
val id: Int,
val nome: String,
val senioridade: String,
val salario: java.math.BigDecimal
)
O Exposed 1.0 também suporta GraalVM native image, o que permite compilar seu aplicativo Spring Boot com Exposed para uma imagem nativa com startup em milissegundos.
Migrando da versão 0.x para 1.0
Se você já usa o Exposed em projetos existentes, a migração é tranquila na maioria dos casos. Os principais pontos de atenção:
// ANTES (0.61.0) - pacote antigo
import org.jetbrains.exposed.sql.transactions.transaction
// DEPOIS (1.0.0) - mesmo pacote, mas para R2DBC use:
import org.jetbrains.exposed.sql.r2dbc.suspendTransaction
// ANTES - Coordenadas Maven antigas
// org.jetbrains.exposed:exposed-core:0.61.0
// DEPOIS - Coordenadas Maven novas
// org.jetbrains.exposed:exposed-core:1.0.0
// org.jetbrains.exposed:exposed-r2dbc:1.0.0 (novo módulo)
A JetBrains disponibiliza um guia de migração completo na documentação oficial.
JDBC vs R2DBC: quando usar cada um?
Essa é uma dúvida comum. Aqui vai um resumo prático:
| Cenário | Recomendação |
|---|---|
| API REST com Spring Boot | JDBC (mais simples, suficiente para a maioria dos casos) |
| API com alta concorrência | R2DBC (menos threads bloqueadas) |
| Aplicação com Ktor e coroutines | R2DBC (integração natural com suspend) |
| Projeto legado migrando para Kotlin | JDBC (compatibilidade com drivers existentes) |
| Microserviços reativos | R2DBC (alinhado com arquitetura reativa) |
Se você trabalha com microserviços em Kotlin, o R2DBC é especialmente interessante por não bloquear threads durante I/O de banco.
Performance: JDBC vs R2DBC
Em testes internos da JetBrains, o R2DBC mostrou vantagens em cenários de alta concorrência:
// Exemplo: processamento paralelo com R2DBC e coroutines
import kotlinx.coroutines.*
suspend fun processarEmParalelo(ids: List<Int>) = coroutineScope {
ids.map { id ->
async {
suspendTransaction(db = database) {
Desenvolvedores
.selectAll()
.where { Desenvolvedores.id eq id }
.firstOrNull()
}
}
}.awaitAll()
}
Com JDBC tradicional, cada consulta paralela ocuparia uma thread do pool. Com R2DBC e coroutines, as mesmas operações compartilham um número muito menor de threads, liberando recursos para outras tarefas.
Para mais sobre concorrência em Kotlin, confira nosso guia completo de coroutines e o artigo sobre Kotlin Flow.
Conclusão
O Exposed 1.0 é um marco importante para o ecossistema Kotlin. Com API estável, suporte a R2DBC e integração melhorada com Spring Boot, ele se consolida como a opção mais idiomática para acesso a banco de dados em Kotlin — sem a complexidade do Hibernate e com toda a segurança de tipos que a linguagem oferece.
Se você está começando com backend em Kotlin, nosso guia de REST APIs é um ótimo ponto de partida. Para quem já trabalha com Exposed, a documentação oficial da versão 1.0 e o repositório no GitHub têm tudo que você precisa para migrar.
Se você trabalha com backend em múltiplas linguagens, vale comparar como ORMs funcionam em outros ecossistemas: Go usa GORM e sqlx com uma abordagem mais explícita, enquanto Python tem SQLAlchemy como referência. O Exposed combina o melhor dos dois mundos: type safety forte com APIs expressivas.
Quer acompanhar o mercado Kotlin no Brasil? Veja as vagas abertas e os salários por senioridade.