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 JDBC driver for the Kotlin language. Exposed has two flavors of database access: typesafe SQL wrapping DSL and lightweight Data Access Objects (DAO).
With Exposed, you have two options for database access: wrapping DSL and a lightweight DAO. 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; 1.x version is deprecated and will be removed in future releases)
- (Also, PostgreSQL using the pgjdbc-ng JDBC driver)
Dependencies
Maven Central configuration
Releases of Exposed are available in the Maven Central repository. You can declare this repository in your build script as follows:
Gradle Groovy and Kotlin DSL
Warning: You might need to set your Kotlin JVM target to 8, and when using Spring to 17, in order for this to work properly:
repositories {
// Versions after 0.30.1
// Versions before 0.30.1 is unavailable for now
mavenCentral()
}
Maven
The Maven Central repository is enabled by default for Maven users.
Exposed modules
Exposed
consists of the following modules:
- exposed-core - base module, which contains both DSL api along with mapping
- exposed-crypt - provides additional column types to store encrypted data in DB and encode/decode it on client-side
- exposed-dao - DAO api
- exposed-java-time - date-time extensions based on Java8 Time API
- exposed-jdbc - transport level implementation based on Java JDBC API
- exposed-jodatime - date-time extensions based on JodaTime library
- exposed-json - JSON and JSONB data type extensions
- exposed-kotlin-datetime - date-time extensions based on kotlinx-datetime
- exposed-money - extensions to support MonetaryAmount from "javax.money:money-api"
- exposed-spring-boot-starter - a starter for Spring Boot to utilize Exposed as the ORM instead of Hibernate
<dependencies>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-core</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-crypt</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-dao</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-java-time</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jdbc</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jodatime</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-json</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-kotlin-datetime</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-money</artifactId>
<version>0.55.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-spring-boot-starter</artifactId>
<version>0.55.0</version>
</dependency>
</dependencies>
Gradle Groovy
dependencies {
implementation 'org.jetbrains.exposed:exposed-core:0.55.0'
implementation 'org.jetbrains.exposed:exposed-crypt:0.55.0'
implementation 'org.jetbrains.exposed:exposed-dao:0.55.0'
implementation 'org.jetbrains.exposed:exposed-jdbc:0.55.0'
implementation 'org.jetbrains.exposed:exposed-jodatime:0.55.0'
// or
implementation 'org.jetbrains.exposed:exposed-java-time:0.55.0'
// or
implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.55.0'
implementation 'org.jetbrains.exposed:exposed-json:0.55.0'
implementation 'org.jetbrains.exposed:exposed-money:0.55.0'
implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.55.0'
}
Gradle Kotlin DSL
In build.gradle.kts
:
val exposedVersion: String by project
dependencies {
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-crypt:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jodatime:$exposedVersion")
// or
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// or
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-json:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-money:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion")
}
and in gradle.properties
exposedVersion=0.55.0
Samples
Check out the samples for a quick start.
Links
Currently, Exposed is available for maven/gradle builds. Check the Maven Central and read Getting Started to get an insight on setting up Exposed.
For more information visit the links below:
- Documentation with examples and docs
- Contributing to Exposed
- Migration Guide
- Breaking changes and any migration details
- Slack Channel
- Issue Tracker
Filing issues
Please note that we are moving away from GitHub Issues for reporting of bugs and features. Please log any new requests on YouTrack. You must be logged in to view and log issues, otherwise you will be met with a 404.
Community
Do you have questions? Feel free to request an invitation for the kotlinlang slack and join the project conversation at our #exposed channel.
Pull requests
We actively welcome your pull requests. However, linking your work to an existing issue is preferred.
- Fork the repo and create your branch from main.
- Name your branch something that is descriptive to the work you are doing. i.e. adds-new-thing.
- If you've added code that should be tested, add tests and ensure the test suite passes.
- Make sure you address any lint warnings.
- If you make the existing code better, please let us know in your PR description.
See the contribution guidelines for more details.
Examples
SQL DSL
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.transactions.transaction
object Users : Table() {
val id: Column<String> = varchar("id", 10)
val name: Column<String> = varchar("name", length = 50)
val cityId: Column<Int?> = (integer("city_id") references Cities.id).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}
object Cities : Table() {
val id: Column<Int> = integer("id").autoIncrement()
val name: Column<String> = varchar("name", 50)
override val primaryKey = PrimaryKey(id, name = "PK_Cities_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[Users.cityId] = saintPetersburgId
}
Users.insert {
it[id] = "sergey"
it[name] = "Sergey"
it[Users.cityId] = munichId
}
Users.insert {
it[id] = "eugene"
it[name] = "Eugene"
it[Users.cityId] = munichId
}
Users.insert {
it[id] = "alex"
it[name] = "Alex"
it[Users.cityId] = null
}
Users.insert {
it[id] = "smth"
it[name] = "Something"
it[Users.cityId] = null
}
Users.update({ Users.id eq "alex" }) {
it[name] = "Alexey"
}
Users.deleteWhere{ Users.name like "%thing" }
println("All cities:")
for (city in Cities.selectAll()) {
println("${city[Cities.id]}: ${city[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 {
println("${it[Users.name]} lives in ${it[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 {
if (it[Users.cityId] != null) {
println("${it[Users.name]} lives in ${it[Cities.name]}")
}
else {
println("${it[Users.name]} lives nowhere")
}
}
println("Functions and group by:")
((Cities innerJoin Users)
.select(Cities.name, Users.id.count())
.groupBy(Cities.name)
).forEach {
val cityName = it[Cities.name]
val userCount = it[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, name VARCHAR(50) NOT NULL, CONSTRAINT PK_Cities_ID PRIMARY KEY (id))
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
1 user(s) live(s) in St. Petersburg
2 user(s) live(s) in Munich
SQL: DROP TABLE IF EXISTS Users
SQL: DROP TABLE IF EXISTS Cities
DAO
import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Users : IntIdTable() {
val name = varchar("name", 50).index()
val city = reference("city", Cities)
val age = integer("age")
}
object Cities: IntIdTable() {
val name = varchar("name", 50)
}
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
}
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
val users by User referrersOn Users.city
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
transaction {
addLogger(StdOutSqlLogger)
SchemaUtils.create(Cities, Users)
val stPete = City.new {
name = "St. Petersburg"
}
val munich = City.new {
name = "Munich"
}
User.new {
name = "a"
city = stPete
age = 5
}
User.new {
name = "b"
city = stPete
age = 27
}
User.new {
name = "c"
city = munich
age = 42
}
println("Cities: ${City.all().joinToString { it.name }}")
println("Users in ${stPete.name}: ${stPete.users.joinToString { it.name }}")
println("Adults: ${User.find { Users.age greaterEq 18 }.joinToString { it.name }}")
}
}
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 ('a', 1, 5)
SQL: INSERT INTO Users (name, city, age) VALUES ('b', 1, 27)
SQL: INSERT INTO Users (name, city, age) VALUES ('c', 2, 42)
SQL: SELECT Users.id, Users.name, Users.city, Users.age FROM Users WHERE Users.city = 1
Users in St. Petersburg: a, b
SQL: SELECT Users.id, Users.name, Users.city, Users.age FROM Users WHERE Users.age >= 18
Adults: b, c
Contributing
Please see the contribution guide before contributing.
By contributing to the Exposed project, you agree that your contributions will be licensed under Apache License, Version 2.0.
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