Skip to content

Exposed

Databases & CachingDatabase/ORMKotlin

What it is

Exposed is JetBrains' SQL library for Kotlin, offering both a typed DSL that mirrors SQL and a lightweight DAO layer.

Declare tables as objects. The DSL builds queries from those column references, so names and types are checked at compile time.

Installation

implementation("org.jetbrains.exposed:exposed-core:0.55.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Table objects and DSL queries
object Books : IntIdTable() {
    val title = varchar("title", 200).index()
    val year = integer("year")
    val authorId = reference("author_id", Authors)
}

transaction {
    val recent = Books
        .innerJoin(Authors)
        .select(Books.title, Authors.name)
        .where { (Books.year greaterEq 1990) and (Authors.country eq "US") }
        .orderBy(Books.year to SortOrder.DESC)
        .limit(10)
        .map { it[Books.title] to it[Authors.name] }

    Books.insert {
        it[title] = "Dune"
        it[year] = 1965
        it[authorId] = herbertId
    }
}
Every query must be inside a transaction block — Exposed uses a thread-local to find the connection, and calls outside one fail at runtime.

Advanced usage

Where the library earns its place over a simpler alternative.

Suspending transactions and batch inserts
// A plain transaction blocks the thread — wrong inside a coroutine.
suspend fun findBooks(): List<Book> = newSuspendedTransaction(Dispatchers.IO) {
    Books.selectAll().map { it.toBook() }
}

// One statement instead of N.
transaction {
    Books.batchInsert(incoming) { book ->
        this[Books.title] = book.title
        this[Books.year] = book.year
    }
}

// Bulk update in SQL, not in memory.
transaction {
    Books.update({ Books.year less 1900 }) { it[archived] = true }
}
newSuspendedTransaction is the detail that catches Ktor users: a regular transaction inside a coroutine blocks a dispatcher thread and undermines the whole async model.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

No transaction in context
The query ran outside a transaction block, or on a different thread than the one that opened it.
Lazy relation access fails
The DAO loaded the entity outside the transaction that is now closed. Map to a plain data class inside the transaction.

Best practices

  • Use newSuspendedTransaction inside coroutines; a plain transaction blocks the thread.
  • Prefer the DSL over the DAO for anything with joins or complex filtering.
  • Use batchInsert for bulk writes rather than looping single inserts.
  • Configure the HikariCP pool explicitly; the defaults are not tuned for production.

Background

Why it exists, and what it was reacting to.

Exposed lets you choose your level of abstraction: the DSL keeps SQL visible and type-checked, while the DAO API provides active-record convenience for simpler models.