Exposed 1.5.0 Help

Working with Databases

In Exposed, the Database and R2dbcDatabase classes represent a database instance, and encapsulates the necessary connection details and configuration required to interact with a specific database.

Choosing between JDBC and R2DBC

Exposed supports both JDBC and R2DBC as transport layers for database connectivity. Understanding their differences will help you choose the right approach for your application:

JDBC

JDBC (Java Database Connectivity) is the traditional, synchronous and blocking API used for interacting with relational databases. Exposed's JDBC integration is well-established with broad database support and extensive tooling. It’s ideal for:

  • Traditional applications where simplicity is preferred over scalability.

  • Projects using connection pools and transaction management in a synchronous context.

JDBC is well-supported and works seamlessly with most existing database drivers and tools.

R2DBC

R2DBC (Reactive Relational Database Connectivity) is a non-blocking, asynchronous alternative to JDBC. Exposed's R2DBC support enables integration with reactive frameworks and Kotlin coroutines. Use R2DBC if:

  • You're building a high-concurrency, I/O-bound application.

  • You want to avoid thread-blocking and leverage Kotlin coroutines end-to-end.

R2DBC is still evolving, and not all databases or features are supported equally compared to JDBC.

Choose JDBC when you need simplicity, broad database compatibility, or are building a traditional application with moderate concurrency requirements. Choose R2DBC when building reactive applications, working with Kotlin coroutines, or needing to handle many concurrent connections efficiently with limited resources.

Connecting to a Database

Every database access in Exposed begins by establishing a connection and creating a transaction.

To connect to a database, you first need to tell Exposed of the connection details. You have two options:

These functions do not immediately establish a connection. Instead, they provide a descriptor for future usage. An actual connection is only established when a transaction is initiated.

To get a database instance using simple connection parameters, use the following approach:

val h2db = Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
val h2db = R2dbcDatabase.connect("r2dbc:h2:mem:///test")

H2

In order to use H2, you need to add the H2 driver dependency:

implementation("com.h2database:h2:2.4.240")
implementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")

Then connect to a database:

val h2dbFromFile = Database.connect("jdbc:h2:./myh2file", driver = "org.h2.Driver")
val h2dbFromFile = R2dbcDatabase.connect("r2dbc:h2:file///./myh2file")

Or in-memory database:

val h2db = Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
val h2db = R2dbcDatabase.connect("r2dbc:h2:mem:///test")

By default, H2 closes the database when the last connection is closed. If you want to keep the database open, you can use the DB_CLOSE_DELAY=-1 option:

Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver")

MariaDB

Add the required dependency:

implementation("org.mariadb.jdbc:mariadb-java-client:3.5.6")
implementation("org.mariadb:r2dbc-mariadb:1.3.0")

Connect to a database:

val mariadb = Database.connect( "jdbc:mariadb://localhost:3306/test", driver = "org.mariadb.jdbc.Driver", user = "root", password = "your_pwd" )
val mariadb = R2dbcDatabase.connect( "r2dbc:mariadb://localhost:3306/test", driver = "mariadb", user = "root", password = "your_pwd" )

MySQL

Add the required dependency:

implementation("mysql:mysql-connector-j:8.4.0")
implementation("io.asyncer:r2dbc-mysql:1.3.2")

Connect to a database:

val mysqldb = Database.connect( "jdbc:mysql://localhost:3306/test", driver = "com.mysql.cj.jdbc.Driver", user = "user", password = "password" )
val mysqldb = R2dbcDatabase.connect( "r2dbc:mysql://localhost:3306/test", driver = "mysql", user = "user", password = "password" )

Oracle

Add the required dependency:

implementation("com.oracle.database.jdbc:ojdbc8:23.26.0.0.0")
implementation("com.oracle.database.r2dbc:oracle-r2dbc:1.3.0")

Connect to a database:

val oracledb = Database.connect( "jdbc:oracle:thin:@//localhost:1521/test", driver = "oracle.jdbc.OracleDriver", user = "user", password = "password" )
val oracledb = R2dbcDatabase.connect( "r2dbc:oracle://localhost:3306/test", driver = "oracle", user = "user", password = "password" )

PostgreSQL

Add the required dependency:

implementation("org.postgresql:postgresql:42.7.8")
implementation("org.postgresql:r2dbc-postgresql:1.1.1.RELEASE")

Connect to a database:

val postgresqldb = Database.connect( "jdbc:postgresql://localhost:12346/test", driver = "org.postgresql.Driver", user = "user", password = "password" )
val postgresqldb = R2dbcDatabase.connect( url = "r2dbc:postgresql://db:5432/test", driver = "postgresql", user = "user", password = "password" )

PostgreSQL using the pgjdbc-ng JDBC driver

Add the required dependency:

implementation("com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9")

Connect to a database:

val postgresqldbNG = Database.connect( "jdbc:pgsql://localhost:12346/test", driver = "com.impossibl.postgres.jdbc.PGDriver", user = "user", password = "password" )

Amazon Redshift

Amazon Redshift support is available through JDBC only. Add the Amazon Redshift JDBC driver:

implementation("com.amazon.redshift:redshift-jdbc42:2.1.0.30")

Then connect using a Redshift JDBC URL:

Database.connect( url = "jdbc:redshift://example-cluster.region.redshift.amazonaws.com:5439/database", driver = "com.amazon.redshift.Driver", user = "username", password = "password" )

Keep the following limitations in mind when connecting to Amazon Redshift using Exposed:

  • Amazon Redshift does not natively support returning generated identity keys as part of an insert operation. Following an Exposed insert {}, attempting to access the statement's generated identity value will fail. Use explicit or client-generated IDs when the application needs the inserted ID.

  • Amazon Redshift treats foreign key constraints as informational only and does not recognize CHECK constraint syntax. Exposed therefore does not create ON DELETE, ON UPDATE, or CHECK constraints. As a result, byte, ubyte, and ushort columns do not have range-enforcing constraints.

  • Amazon Redshift does not enforce traditional secondary indexes. Exposed therefore skips non-unique, partial, functional, and typed indexes. Define Redshift sort and distribution keys outside the Exposed index API.

  • Amazon Redshift limits VARCHAR(MAX) values to 65,535 bytes and VARBYTE values to 16,777,216 bytes. Text columns map to VARCHAR(MAX), while binary and blob columns map to VARBYTE(16777216).

  • Amazon Redshift has no native UUID data type, so UUID columns use VARCHAR(36). Exposed writes the standard dashed representation and reads either dashed or undashed hexadecimal text.

  • Amazon Redshift supports a limited set of column modifications that do not map to Exposed's general column-modification API. Requests to build a column-modification statement fail before any SQL is sent to the database.

SQL Server

Add the required dependency:

implementation("com.microsoft.sqlserver:mssql-jdbc:13.2.1.jre11")
implementation("io.r2dbc:r2dbc-mssql:1.0.3.RELEASE")

Connect to a database:

val sqlserverdb = Database.connect( "jdbc:sqlserver://localhost:32768;databaseName=test", "com.microsoft.sqlserver.jdbc.SQLServerDriver", user = "user", password = "password" )
val sqlserverdb = R2dbcDatabase.connect( "r2dbc:mssql://localhost:32768;databaseName=test", driver = "sqlserver", user = "user", password = "password" )

SQLite

Add the required dependency:

implementation("org.xerial:sqlite-jdbc:3.50.2.0")

Connect to a database:

Database.connect("jdbc:sqlite:/data/data.db", "org.sqlite.JDBC")

Or an in-memory database:

Database.connect("jdbc:sqlite:file:test?mode=memory&cache=shared", "org.sqlite.JDBC")

Set SQLite compatible isolation level:

TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE // or Connection.TRANSACTION_READ_UNCOMMITTED
26 August 2026