Top Related Projects
SQLDelight - Generates typesafe Kotlin APIs from SQL
requery - modern SQL based query & persistence for Java / Kotlin / Android
Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin
jOOQ is the best way to write SQL in Java
Quick Overview
JetBrains/Exposed is an open-source ORM (Object-Relational Mapping) framework for Kotlin. It provides a DSL for type-safe SQL queries and schema definitions, making database interactions more intuitive and less error-prone for Kotlin developers.
Pros
- Type-safe SQL DSL, reducing runtime errors and improving code maintainability
- Seamless integration with Kotlin language features, including coroutines
- Support for multiple database dialects (e.g., MySQL, PostgreSQL, SQLite)
- Lightweight and performant compared to some other ORM solutions
Cons
- Learning curve for developers new to Kotlin or ORM concepts
- Limited documentation compared to more established ORM frameworks
- Fewer advanced features compared to some full-featured ORM solutions
- Potential performance overhead for very complex queries or large-scale applications
Code Examples
- Defining a table:
object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 100)
override val primaryKey = PrimaryKey(id)
}
- Inserting data:
transaction {
Users.insert {
it[name] = "John Doe"
it[email] = "john@example.com"
}
}
- Querying data:
transaction {
Users.select { Users.name like "J%" }
.forEach { println("${it[Users.name]} - ${it[Users.email]}") }
}
- Updating data:
transaction {
Users.update({ Users.id eq 1 }) {
it[email] = "newemail@example.com"
}
}
Getting Started
- Add the dependency to your
build.gradle.kts
:
dependencies {
implementation("org.jetbrains.exposed:exposed-core:0.40.1")
implementation("org.jetbrains.exposed:exposed-dao:0.40.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.40.1")
}
- Connect to the database and perform operations:
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Users)
val userId = Users.insert {
it[name] = "Alice"
it[email] = "alice@example.com"
} get Users.id
println("Inserted user id: $userId")
val user = Users.select { Users.id eq userId }.single()
println("User: ${user[Users.name]} (${user[Users.email]})")
}
}
Competitor Comparisons
SQLDelight - Generates typesafe Kotlin APIs from SQL
Pros of SQLDelight
- Type-safe SQL queries with compile-time verification
- Generates Kotlin code from SQL statements, ensuring consistency between database and application
- Supports multiplatform projects, including iOS and Android
Cons of SQLDelight
- Limited to SQLite databases
- Steeper learning curve for developers unfamiliar with raw SQL
- Less flexibility in query construction compared to ORM-style APIs
Code Comparison
SQLDelight:
CREATE TABLE Person (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
selectAll:
SELECT * FROM Person;
Exposed:
object Persons : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
override val primaryKey = PrimaryKey(id)
}
val allPersons = Persons.selectAll()
SQLDelight generates Kotlin code from SQL statements, while Exposed uses a DSL to define tables and queries. SQLDelight offers stronger type safety and compile-time verification, but Exposed provides a more Kotlin-idiomatic approach to database interactions. SQLDelight is better suited for projects requiring SQLite compatibility across multiple platforms, while Exposed offers more flexibility in terms of supported databases and query construction.
requery - modern SQL based query & persistence for Java / Kotlin / Android
Pros of requery
- Supports both SQL and NoSQL databases, offering more flexibility
- Provides a powerful code generation tool for entity classes
- Offers reactive extensions for asynchronous operations
Cons of requery
- Less Kotlin-specific features compared to Exposed
- Steeper learning curve due to its broader scope
- Smaller community and fewer resources compared to Exposed
Code Comparison
Exposed query example:
val query = Users.select { Users.age greater 18 }
.orderBy(Users.name)
.limit(10)
requery query example:
val query = select(Person::class)
.where(Person::age.gt(18))
.orderBy(Person::name.asc())
.limit(10)
Both libraries offer a type-safe DSL for querying databases, but Exposed's syntax is more concise and Kotlin-idiomatic. requery's syntax is closer to SQL, which may be more familiar to developers with SQL background.
Exposed is specifically designed for Kotlin and integrates well with other Kotlin libraries, while requery is a Java library with Kotlin support. This makes Exposed more natural to use in Kotlin projects, but requery might be a better choice for mixed Java/Kotlin codebases or when working with multiple database types.
Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin
Pros of jasync-sql
- Asynchronous, non-blocking database operations
- Supports multiple database types (MySQL, PostgreSQL)
- Lightweight and focused on performance
Cons of jasync-sql
- Less abstraction and higher-level features compared to Exposed
- Steeper learning curve for developers used to ORM-style APIs
- Smaller community and ecosystem
Code Comparison
Exposed (ORM-style query):
val users = Users.select { Users.age greaterEq 18 }
.orderBy(Users.name)
.limit(10)
jasync-sql (raw SQL query):
connection.sendPreparedStatement("""
SELECT * FROM users
WHERE age >= ?
ORDER BY name
LIMIT 10
""", listOf(18))
Exposed provides a more Kotlin-idiomatic, type-safe approach with its DSL, while jasync-sql offers lower-level control and potentially better performance for complex queries. Exposed is generally easier for developers familiar with ORMs, while jasync-sql may be preferred for those who want more direct database access and async capabilities.
jOOQ is the best way to write SQL in Java
Pros of jOOQ
- Supports a wide range of databases, including Oracle, SQL Server, and PostgreSQL
- Offers type-safe SQL querying with code generation
- Provides advanced SQL features like window functions and common table expressions
Cons of jOOQ
- Steeper learning curve due to its comprehensive API
- Commercial licensing required for some databases
- More verbose syntax compared to Exposed's DSL
Code Comparison
Exposed query:
val query = Users.select { Users.age greaterEq 18 }
.orderBy(Users.name to SortOrder.ASC)
.limit(10)
jOOQ query:
Result<Record> result = create.select()
.from(USERS)
.where(USERS.AGE.greaterOrEqual(18))
.orderBy(USERS.NAME.asc())
.limit(10)
.fetch();
Key Differences
- Exposed is Kotlin-first, while jOOQ is Java-based with Kotlin support
- jOOQ offers more advanced SQL features and database-specific optimizations
- Exposed provides a more concise syntax, especially for simple queries
- jOOQ has a larger community and more extensive documentation
Both libraries aim to provide type-safe database access, but they cater to different use cases and preferences. Exposed is often favored for Kotlin projects and simpler database interactions, while jOOQ is chosen for complex SQL operations and multi-database support.
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME

Welcome to Exposed, an ORM framework for Kotlin.
Exposed is a lightweight SQL library on top of a database connectivity driver for the Kotlin programming language, with support for both JDBC and R2DBC (since version 1.0.0-*) drivers. It offers two approaches for database access: a typesafe SQL-wrapping Domain-Specific Language (DSL) and a lightweight Data Access Object (DAO) API.
Our official mascot is the cuttlefish, which is well-known for its outstanding mimicry ability that enables it to blend seamlessly into any environment. Similar to our mascot, Exposed can be used to mimic a variety of database engines, which helps you to build applications without dependencies on any specific database engine and to switch between them with very little or no changes.
Supported Databases
- H2 (versions 2.x)
(Also, PostgreSQL using the pgjdbc-ng JDBC driver)
Dependencies
Releases of Exposed are available in the Maven Central repository. For details on how to configure this repository and how to add Exposed dependencies to an existing Gradle/Maven project, see the full guide on modules.
Exposed modules
Exposed
consists of the following core modules:
Module | Function |
---|---|
exposed-core | Provides the foundational components and abstractions needed to work with databases in a type-safe manner and includes the Domain-Specific Language (DSL) API |
exposed-dao | (Optional) Allows you to work with the Data Access Object (DAO) API. It is only compatible with exposed-jdbc and does not work with exposed-r2dbc . |
exposed-jdbc | Provides support for Java Database Connectivity (JDBC) with a transport-level implementation based on the Java JDBC API |
exposed-r2dbc | Provides support for Reactive Relational Database Connectivity (R2DBC) |
As well as the following extension modules:
Module | Function |
---|---|
exposed-crypt | Provides additional column types to store encrypted data in the database and encode/decode it on the client-side |
exposed-java-time | Date-time extensions based on the Java 8 Time API |
exposed-jodatime | Date-time extensions based on the Joda-Time library |
exposed-json | JSON and JSONB data type extensions |
exposed-kotlin-datetime | Date-time extensions based on the kotlinx-datetime library |
exposed-migration | Provides utilities to support database schema migrations |
exposed-money | Extensions to support MonetaryAmount from the JavaMoney API |
exposed-spring-boot-starter | A starter for Spring Boot to utilize Exposed as the ORM instead of Hibernate |
spring-transaction | Transaction manager that builds on top of Spring's standard transaction workflow |
Samples using Exposed
Follow the Getting Started with DSL tutorial for a quick start or check out the samples for more in-depth projects.
Documentation
For complete documentation, samples, and tutorials, see the following links:
Contributing
Reporting issues
We encourage your feedback in any form, such as feature requests, bug reports, documentation updates, and questions.
Please use our issue tracker to report any issues or to log new requests.
While issues are visible publicly, either creating a new issue or commenting on an existing one does require logging in to YouTrack.
Submitting pull requests
We actively welcome your pull requests and encourage you to link your work to an existing issue.
See the full contribution guide for more details.
By contributing to the Exposed project, you agree that your contributions will be licensed under Apache License, Version 2.0.
Support
Have questions or want to contribute to the discussion? Join us in the #exposed channel on the Kotlin Slack. If you're not a member yet, you can request an invitation.
Examples
SQL DSL
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.like
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
object Cities : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
override val primaryKey = PrimaryKey(id)
}
object Users : Table() {
val id = varchar("id", 10)
val name = varchar("name", length = 50)
val cityId = integer("city_id").references(Cities.id).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_User_ID")
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
transaction {
addLogger(StdOutSqlLogger)
SchemaUtils.create(Cities, Users)
val saintPetersburgId = Cities.insert {
it[name] = "St. Petersburg"
} get Cities.id
val munichId = Cities.insert {
it[name] = "Munich"
} get Cities.id
val pragueId = Cities.insert {
it.update(name, stringLiteral(" Prague ").trim().substring(1, 2))
}[Cities.id]
val pragueName = Cities
.selectAll()
.where { Cities.id eq pragueId }
.single()[Cities.name]
println("pragueName = $pragueName")
Users.insert {
it[id] = "andrey"
it[name] = "Andrey"
it[cityId] = saintPetersburgId
}
Users.insert {
it[id] = "sergey"
it[name] = "Sergey"
it[cityId] = munichId
}
Users.insert {
it[id] = "eugene"
it[name] = "Eugene"
it[cityId] = munichId
}
Users.insert {
it[id] = "alex"
it[name] = "Alex"
it[cityId] = null
}
Users.insert {
it[id] = "smth"
it[name] = "Something"
it[cityId] = null
}
Users.update(where = { Users.id eq "alex" }) {
it[name] = "Alexey"
}
Users.deleteWhere { Users.name like "%thing" }
println("All cities:")
Cities
.selectAll()
.forEach { result ->
println("${result[Cities.id]}: ${result[Cities.name]}")
}
println("Manual join:")
(Users innerJoin Cities)
.select(Users.name, Cities.name)
.where {
(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
Users.id.eq("sergey") and Users.cityId.eq(Cities.id)
}.forEach { result ->
println("${result[Users.name]} lives in ${result[Cities.name]}")
}
println("Join with foreign key:")
(Users innerJoin Cities)
.select(Users.name, Users.cityId, Cities.name)
.where { Cities.name.eq("St. Petersburg") or Users.cityId.isNull() }
.forEach { result ->
if (result[Users.cityId] != null) {
println("${result[Users.name]} lives in ${result[Cities.name]}")
} else {
println("${result[Users.name]} lives nowhere")
}
}
println("Functions and group by:")
(Cities innerJoin Users)
.select(Cities.name, Users.id.count())
.groupBy(Cities.name)
.forEach { result ->
val cityName = result[Cities.name]
val userCount = result[Users.id.count()]
if (userCount > 0) {
println("$userCount user(s) live(s) in $cityName")
} else {
println("Nobody lives in $cityName")
}
}
SchemaUtils.drop(Users, Cities)
}
}
Generated SQL:
SQL: CREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS USERS (ID VARCHAR(10), "name" VARCHAR(50) NOT NULL, CITY_ID INT NULL, CONSTRAINT PK_User_ID PRIMARY KEY (ID), CONSTRAINT FK_USERS_CITY_ID__ID FOREIGN KEY (CITY_ID) REFERENCES CITIES(ID) ON DELETE RESTRICT ON UPDATE RESTRICT)
SQL: INSERT INTO CITIES ("name") VALUES ('St. Petersburg')
SQL: INSERT INTO CITIES ("name") VALUES ('Munich')
SQL: INSERT INTO CITIES ("name") VALUES (SUBSTRING(TRIM(' Prague '), 1, 2))
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES WHERE CITIES.ID = 3
pragueName = Pr
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('andrey', 'Andrey', 1)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('sergey', 'Sergey', 2)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('eugene', 'Eugene', 2)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('alex', 'Alex', NULL)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('smth', 'Something', NULL)
SQL: UPDATE USERS SET "name"='Alexey' WHERE USERS.ID = 'alex'
SQL: DELETE FROM USERS WHERE USERS."name" LIKE '%thing'
All cities:
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES
1: St. Petersburg
2: Munich
3: Pr
Manual join:
SQL: SELECT USERS."name", CITIES."name" FROM USERS INNER JOIN CITIES ON CITIES.ID = USERS.CITY_ID WHERE ((USERS.ID = 'andrey') OR (USERS."name" = 'Sergey')) AND (USERS.ID = 'sergey') AND (USERS.CITY_ID = CITIES.ID)
Sergey lives in Munich
Join with foreign key:
SQL: SELECT USERS."name", USERS.CITY_ID, CITIES."name" FROM USERS INNER JOIN CITIES ON CITIES.ID = USERS.CITY_ID WHERE (CITIES."name" = 'St. Petersburg') OR (USERS.CITY_ID IS NULL)
Andrey lives in St. Petersburg
Functions and group by:
SQL: SELECT CITIES."name", COUNT(USERS.ID) FROM CITIES INNER JOIN USERS ON CITIES.ID = USERS.CITY_ID GROUP BY CITIES."name"
2 user(s) live(s) in Munich
1 user(s) live(s) in St. Petersburg
SQL: DROP TABLE IF EXISTS USERS
SQL: DROP TABLE IF EXISTS CITIES
DAO
import org.jetbrains.exposed.v1.core.StdOutSqlLogger
import org.jetbrains.exposed.v1.core.dao.id.*
import org.jetbrains.exposed.v1.dao.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
object Cities: IntIdTable() {
val name = varchar("name", 50)
}
object Users : IntIdTable() {
val name = varchar("name", length = 50).index()
val city = reference("city", Cities)
val age = integer("age")
}
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
val users by User referrersOn Users.city
}
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)
var name by Users.name
var city by City referencedOn Users.city
var age by Users.age
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
transaction {
addLogger(StdOutSqlLogger)
val saintPetersburg = City.new {
name = "St. Petersburg"
}
val munich = City.new {
name = "Munich"
}
User.new {
name = "Andrey"
city = saintPetersburg
age = 5
}
User.new {
name = "Sergey"
city = saintPetersburg
age = 27
}
User.new {
name = "Eugene"
city = munich
age = 42
}
val alex = User.new {
name = "alex"
city = munich
age = 11
}
alex.name = "Alexey"
println("Cities: ${City.all().joinToString { it.name }}")
println("Users in ${saintPetersburg.name}: ${saintPetersburg.users.joinToString { it.name }}")
println("Adults: ${User.find { Users.age greaterEq 18 }.joinToString { it.name }}")
SchemaUtils.drop(Users, Cities)
}
}
Generated SQL:
SQL: CREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS USERS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL, CITY INT NOT NULL, AGE INT NOT NULL, CONSTRAINT FK_USERS_CITY__ID FOREIGN KEY (CITY) REFERENCES CITIES(ID) ON DELETE RESTRICT ON UPDATE RESTRICT)
SQL: CREATE INDEX USERS_NAME ON USERS ("name")
SQL: INSERT INTO CITIES ("name") VALUES ('St. Petersburg')
SQL: INSERT INTO CITIES ("name") VALUES ('Munich')
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES
Cities: St. Petersburg, Munich
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Andrey', 1, 5)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Sergey', 1, 27)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Eugene', 2, 42)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Alexey', 2, 11)
SQL: SELECT USERS.ID, USERS."name", USERS.CITY, USERS.AGE FROM USERS WHERE USERS.CITY = 1
Users in St. Petersburg: Andrey, Sergey
SQL: SELECT USERS.ID, USERS."name", USERS.CITY, USERS.AGE FROM USERS WHERE USERS.AGE >= 18
Adults: Sergey, Eugene
SQL: DROP TABLE IF EXISTS USERS
SQL: DROP TABLE IF EXISTS CITIES
Top Related Projects
SQLDelight - Generates typesafe Kotlin APIs from SQL
requery - modern SQL based query & persistence for Java / Kotlin / Android
Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin
jOOQ is the best way to write SQL in Java
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot