---
title: "Kotlin Exposed ORM: Como Usar o Framework SQL da JetBrains | Kotlin Brasil"
url: "https://kotlin.dev.br/blog/kotlin-exposed-orm-framework-sql/"
markdown_url: "https://kotlin.dev.br/blog/kotlin-exposed-orm-framework-sql.MD"
description: "Aprenda a usar o Exposed, o framework SQL da JetBrains para Kotlin. Tutorial com DSL, DAO, CRUD, transações e exemplos práticos."
date: "2026-03-31"
author: "Karina Melo"
---

# Kotlin Exposed ORM: Como Usar o Framework SQL da JetBrains | Kotlin Brasil

Aprenda a usar o Exposed, o framework SQL da JetBrains para Kotlin. Tutorial com DSL, DAO, CRUD, transações e exemplos práticos.


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](/blog/kotlin-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. Para uma visão mais aplicada com PostgreSQL, pool de conexões e migrations, leia também [Kotlin com PostgreSQL no backend](/tutoriais/kotlin-postgresql-backend/).

## O que é o Exposed?

O [Exposed](https://github.com/JetBrains/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](/blog/coroutines-kotlin/).

## Configuração do projeto

Adicione as dependências no `build.gradle.kts`:

```kotlin
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`:

```kotlin
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 <a href="https://golang.com.br/blog/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> também buscam alcançar, mas o sistema de tipos do Kotlin torna a experiência muito mais fluida.

## Conectando ao banco

```kotlin
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

```kotlin
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:

```kotlin
val novoId = transaction {
    Usuarios.insertAndGetId {
        it[nome] = "Maria Santos"
        it[email] = "maria@email.com"
        it[criadoEm] = LocalDateTime.now()
    }
}
```

### Read — Consultando dados

```kotlin
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

```kotlin
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

```kotlin
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:

```kotlin
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:

```kotlin
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](/blog/ktor-criando-apis-kotlin/), o framework web da JetBrains:

```kotlin
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](/blog/kotlin-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:

```kotlin
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](/blog/coroutines-kotlin/) e [Flow](/blog/kotlin-flow/), o Exposed oferece `newSuspendedTransaction`:

```kotlin
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

1. **Use HikariCP** para connection pooling — nunca conecte diretamente ao banco em produção
2. **Prefira DSL para queries complexas** — a type safety evita erros em runtime
3. **Separe definições de tabelas** em um pacote `database.tables`
4. **Use `newSuspendedTransaction`** em projetos com coroutines para não bloquear threads
5. **Teste com banco em memória** (H2) para testes unitários rápidos — veja nosso guia de [testes em Kotlin](/blog/kotlin-testes-junit5-mockk-guia/)

## 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](/blog/ktor-criando-apis-kotlin/) 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](/guias/kotlin-para-backend/) e o [Glossário de Kotlin](/glossario/).

Se você está usando Exposed dentro de uma API web, veja também o [tutorial de Ktor com Exposed](/tutoriais/kotlin-ktor-tutorial/) para conectar routing, serialização e persistência no mesmo projeto. Se você trabalha com múltiplas linguagens no backend, vale comparar abordagens de acesso a banco: <a href="https://python.dev.br/blog/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python tem o SQLAlchemy como ORM de referência</a>, enquanto <a href="https://rustlang.com.br/blog/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust aposta no Diesel e SQLx para acesso type-safe a bancos</a>.
