Quando se fala em acesso a banco de dados no ecossistema Kotlin, o Hibernate costuma ser a primeira opção por causa da integração com Spring Boot. Mas existe uma alternativa 100% Kotlin, criada pela própria JetBrains: o Exposed. Neste tutorial, vamos explorar como ele funciona, suas duas abordagens (DSL e DAO), e por que ele pode ser a melhor escolha para o seu próximo projeto.
O que é o Exposed?
O Exposed é um framework SQL leve para Kotlin, mantido pela JetBrains. Ele oferece duas formas de trabalhar com bancos de dados:
- DSL (Domain Specific Language): escrita de queries com sintaxe type-safe, parecida com SQL
- DAO (Data Access Object): mapeamento objeto-relacional no estilo ORM tradicional
A grande vantagem? Tudo é escrito em Kotlin puro, com inferência de tipos, null safety e suporte nativo a coroutines.
Configuração do projeto
Adicione as dependências no build.gradle.kts:
dependencies {
// Exposed core
implementation("org.jetbrains.exposed:exposed-core:0.56.0")
implementation("org.jetbrains.exposed:exposed-dao:0.56.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.56.0")
implementation("org.jetbrains.exposed:exposed-java-time:0.56.0")
// Driver do banco (exemplo com PostgreSQL)
implementation("org.postgresql:postgresql:42.7.4")
// Connection pool
implementation("com.zaxxer:HikariCP:6.2.1")
}
Definindo tabelas com DSL
No Exposed, tabelas são objetos Kotlin que herdam de Table:
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.datetime
object Usuarios : Table("usuarios") {
val id = integer("id").autoIncrement()
val nome = varchar("nome", 100)
val email = varchar("email", 255).uniqueIndex()
val ativo = bool("ativo").default(true)
val criadoEm = datetime("criado_em")
override val primaryKey = PrimaryKey(id)
}
object Pedidos : Table("pedidos") {
val id = integer("id").autoIncrement()
val usuarioId = integer("usuario_id").references(Usuarios.id)
val total = decimal("total", 10, 2)
val status = varchar("status", 50)
val criadoEm = datetime("criado_em")
override val primaryKey = PrimaryKey(id)
}
Note como as colunas são type-safe — o compilador garante que você não vai comparar um integer com um varchar por engano. Isso é algo que frameworks ORM em linguagens como Go também buscam alcançar, mas o sistema de tipos do Kotlin torna a experiência muito mais fluida.
Conectando ao banco
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
fun inicializarBanco() {
val config = HikariConfig().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/meu_app"
driverClassName = "org.postgresql.Driver"
username = "postgres"
password = "senha_segura"
maximumPoolSize = 10
}
Database.connect(HikariDataSource(config))
// Criar tabelas automaticamente
transaction {
SchemaUtils.create(Usuarios, Pedidos)
}
}
Operações CRUD com DSL
Create — Inserindo registros
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.LocalDateTime
transaction {
Usuarios.insert {
it[nome] = "João Silva"
it[email] = "joao@email.com"
it[ativo] = true
it[criadoEm] = LocalDateTime.now()
}
}
Para inserir e obter o ID gerado:
val novoId = transaction {
Usuarios.insertAndGetId {
it[nome] = "Maria Santos"
it[email] = "maria@email.com"
it[criadoEm] = LocalDateTime.now()
}
}
Read — Consultando dados
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.select
// Todos os usuários ativos
val usuariosAtivos = transaction {
Usuarios.selectAll()
.where { Usuarios.ativo eq true }
.map { row ->
UsuarioDTO(
id = row[Usuarios.id],
nome = row[Usuarios.nome],
email = row[Usuarios.email]
)
}
}
// Busca com join
val pedidosComUsuario = transaction {
(Pedidos innerJoin Usuarios)
.selectAll()
.where { Pedidos.status eq "PENDENTE" }
.map { row ->
PedidoResumo(
pedidoId = row[Pedidos.id],
nomeUsuario = row[Usuarios.nome],
total = row[Pedidos.total]
)
}
}
Update — Atualizando registros
import org.jetbrains.exposed.sql.update
transaction {
Usuarios.update({ Usuarios.id eq 1 }) {
it[nome] = "João Silva Junior"
it[ativo] = false
}
}
Delete — Removendo registros
import org.jetbrains.exposed.sql.deleteWhere
transaction {
Pedidos.deleteWhere { Pedidos.status eq "CANCELADO" }
}
Abordagem DAO — Estilo ORM
Se você prefere trabalhar com objetos no estilo ORM tradicional, o Exposed oferece a camada DAO:
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
// Tabela precisa usar IntIdTable para DAO
object UsuariosTable : IntIdTable("usuarios") {
val nome = varchar("nome", 100)
val email = varchar("email", 255).uniqueIndex()
val ativo = bool("ativo").default(true)
val criadoEm = datetime("criado_em")
}
class Usuario(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Usuario>(UsuariosTable)
var nome by UsuariosTable.nome
var email by UsuariosTable.email
var ativo by UsuariosTable.ativo
var criadoEm by UsuariosTable.criadoEm
}
Com DAO, as operações são mais orientadas a objetos:
transaction {
// Criar
val usuario = Usuario.new {
nome = "Pedro Costa"
email = "pedro@email.com"
ativo = true
criadoEm = LocalDateTime.now()
}
// Ler
val encontrado = Usuario.findById(1)
val ativos = Usuario.find { UsuariosTable.ativo eq true }
// Atualizar
encontrado?.nome = "Pedro Costa Junior"
// Deletar
encontrado?.delete()
}
DSL vs DAO: Quando usar cada um?
| Aspecto | DSL | DAO |
|---|---|---|
| Estilo | Funcional, parecido com SQL | Orientado a objetos |
| Performance | Ligeiramente mais rápido | Overhead do mapeamento |
| Queries complexas | Excelente | Limitado |
| CRUD simples | Verboso | Conciso |
| Lazy loading | Não | Sim |
| Melhor para | Relatórios, queries complexas | CRUDs, domínios ricos |
Na prática, muitos projetos combinam as duas abordagens — DAO para operações simples e DSL para queries complexas.
Integração com Ktor
O Exposed combina perfeitamente com Ktor, o framework web da JetBrains:
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.transactions.transaction
fun Application.configurarRotas() {
routing {
get("/usuarios") {
val usuarios = transaction {
Usuarios.selectAll()
.where { Usuarios.ativo eq true }
.map { row ->
mapOf(
"id" to row[Usuarios.id],
"nome" to row[Usuarios.nome],
"email" to row[Usuarios.email]
)
}
}
call.respond(usuarios)
}
}
}
Para projetos maiores com Spring Boot, confira nosso tutorial completo de Kotlin com Spring Boot.
Transações e tratamento de erros
O Exposed exige que todas as operações de banco sejam executadas dentro de um bloco transaction. Isso garante atomicidade:
import org.jetbrains.exposed.sql.transactions.transaction
try {
transaction {
// Todas as operações aqui são atômicas
val pedidoId = Pedidos.insertAndGetId {
it[usuarioId] = 1
it[total] = 299.90.toBigDecimal()
it[status] = "CRIADO"
it[criadoEm] = LocalDateTime.now()
}
Usuarios.update({ Usuarios.id eq 1 }) {
it[ativo] = true
}
// Se algo falhar aqui, tudo acima é revertido
}
} catch (e: Exception) {
println("Erro na transação: ${e.message}")
}
Para operações assíncronas com coroutines e Flow, o Exposed oferece newSuspendedTransaction:
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
suspend fun buscarUsuarioAsync(id: Int): UsuarioDTO? {
return newSuspendedTransaction {
Usuarios.selectAll()
.where { Usuarios.id eq id }
.firstOrNull()
?.let {
UsuarioDTO(it[Usuarios.id], it[Usuarios.nome], it[Usuarios.email])
}
}
}
Exposed vs Hibernate/JPA
Se você vem do mundo Java, provavelmente já usou Hibernate. Veja como o Exposed se compara:
| Aspecto | Exposed | Hibernate/JPA |
|---|---|---|
| Linguagem | Kotlin nativo | Java (funciona em Kotlin) |
| Configuração | Mínima, tudo em código | XML ou anotações extensas |
| Type safety | Total (verificado em compilação) | Parcial (JPQL é string) |
| Curva de aprendizado | Baixa para devs Kotlin | Moderada a alta |
| Ecossistema | Crescente | Maduro e vasto |
| Null safety | Integrado | Requer cuidado extra |
| Coroutines | Suporte nativo | Requer adaptação |
Boas práticas
- Use HikariCP para connection pooling — nunca conecte diretamente ao banco em produção
- Prefira DSL para queries complexas — a type safety evita erros em runtime
- Separe definições de tabelas em um pacote
database.tables - Use
newSuspendedTransactionem projetos com coroutines para não bloquear threads - Teste com banco em memória (H2) para testes unitários rápidos — veja nosso guia de testes em Kotlin
Conclusão
O Exposed é uma excelente alternativa ao Hibernate para projetos Kotlin. Com sua DSL type-safe, suporte a coroutines e manutenção pela JetBrains, ele oferece uma experiência de desenvolvimento muito mais natural para quem trabalha com Kotlin.
Se você está começando um novo projeto backend com Ktor ou quer uma alternativa mais leve para o Spring Boot, o Exposed merece um lugar na sua lista de ferramentas.
Para continuar aprendendo, explore nosso Guia de Kotlin para Backend e o Glossário de Kotlin.
Se você trabalha com múltiplas linguagens no backend, vale comparar abordagens de acesso a banco: Python tem o SQLAlchemy como ORM de referência, enquanto Rust aposta no Diesel e SQLx para acesso type-safe a bancos.