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:

BancoDriver R2DBC
PostgreSQLr2dbc-postgresql
MySQLr2dbc-mysql
MariaDBr2dbc-mariadb
H2r2dbc-h2
Oracler2dbc-oracle
SQL Serverr2dbc-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árioRecomendação
API REST com Spring BootJDBC (mais simples, suficiente para a maioria dos casos)
API com alta concorrênciaR2DBC (menos threads bloqueadas)
Aplicação com Ktor e coroutinesR2DBC (integração natural com suspend)
Projeto legado migrando para KotlinJDBC (compatibilidade com drivers existentes)
Microserviços reativosR2DBC (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.