Convert Figma logo to code with AI

JetBrains logoExposed

Kotlin SQL Framework

8,915
743
8,915
187

Top Related Projects

SQLDelight - Generates typesafe Kotlin APIs from SQL

3,126

requery - modern SQL based query & persistence for Java / Kotlin / Android

Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin

6,477

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

  1. 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)
}
  1. Inserting data:
transaction {
    Users.insert {
        it[name] = "John Doe"
        it[email] = "john@example.com"
    }
}
  1. Querying data:
transaction {
    Users.select { Users.name like "J%" }
        .forEach { println("${it[Users.name]} - ${it[Users.email]}") }
}
  1. Updating data:
transaction {
    Users.update({ Users.id eq 1 }) {
        it[email] = "newemail@example.com"
    }
}

Getting Started

  1. 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")
}
  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.

3,126

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.

6,477

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 Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

Exposed logo


JetBrains team project Slack Channel TC Build status Maven Central GitHub License

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)
  • MariaDB
  • MySQL
  • Oracle
  • Postgres (Also, PostgreSQL using the pgjdbc-ng JDBC driver)
  • MicrosoftSQLServer
  • SQLite

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:

ModuleFunction
exposed-coreProvides 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-jdbcProvides support for Java Database Connectivity (JDBC) with a transport-level implementation based on the Java JDBC API
exposed-r2dbcProvides support for Reactive Relational Database Connectivity (R2DBC)

As well as the following extension modules:

ModuleFunction
exposed-cryptProvides additional column types to store encrypted data in the database and encode/decode it on the client-side
exposed-java-timeDate-time extensions based on the Java 8 Time API
exposed-jodatimeDate-time extensions based on the Joda-Time library
exposed-jsonJSON and JSONB data type extensions
exposed-kotlin-datetimeDate-time extensions based on the kotlinx-datetime library
exposed-migrationProvides utilities to support database schema migrations
exposed-moneyExtensions to support MonetaryAmount from the JavaMoney API
exposed-spring-boot-starterA starter for Spring Boot to utilize Exposed as the ORM instead of Hibernate
spring-transactionTransaction 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