Install any skill in seconds. Free to start, no credit card required.
Get Started Free →JetBrains Exposed ORM patterns including DSL queries, DAO pattern, transactions, HikariCP connection pooling, Flyway migrations, and repository pattern.
.claude/skills/loulanyue-kotlin-exposed-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 164% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 166% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 243% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 224% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 275% | 0% |
Comprehensive patterns for database access with JetBrains Exposed ORM, including DSL queries, DAO, transactions, and production-ready configuration.
Exposed provides two query styles: DSL for direct SQL-like expressions and DAO for entity lifecycle management. HikariCP manages a pool of reusable database connections configured via HikariConfig. Flyway runs versioned SQL migration scripts at startup to keep the schema in sync. All database operations run inside newSuspendedTransaction blocks for coroutine safety and atomicity. The repository pattern wraps Exposed queries behind an interface so business logic stays decoupled from the data layer and tests can use an in-memory H2 database.
kotlinsuspend fun findUserById(id: UUID): UserRow? = newSuspendedTransaction { UsersTable.selectAll() .where { UsersTable.id eq id } .map { it.toUser() } .singleOrNull() }
kotlinsuspend fun createUser(request: CreateUserRequest): User = newSuspendedTransaction { UserEntity.new { name = request.name email = request.email role = request.role }.toModel() }
kotlinval hikariConfig = HikariConfig().apply { driverClassName = config.driver jdbcUrl = config.url username = config.username password = config.password maximumPoolSize = config.maxPoolSize isAutoCommit = false transactionIsolation = "TRANSACTION_READ_COMMITTED" validate() }
kotlin// DatabaseFactory.kt object DatabaseFactory { fun create(config: DatabaseConfig): Database { val hikariConfig = HikariConfig().apply { driverClassName = config.driver jdbcUrl = config.url username = config.username password = config.password maximumPoolSize = config.maxPoolSize isAutoCommit = false transactionIsolation = "TRANSACTION_READ_COMMITTED" validate() } return Database.connect(HikariDataSource(hikariConfig)) } } data class DatabaseConfig( val url: String, val driver: String = "org.postgresql.Driver", val username: String = "", val password: String = "", val maxPoolSize: Int = 10, )
kotlin// FlywayMigration.kt fun runMigrations(config: DatabaseConfig) { Flyway.configure() .dataSource(config.url, config.username, config.password) .locations("classpath:db/migration") .baselineOnMigrate(true) .load() .migrate() } // Application startup fun Application.module() { val config = DatabaseConfig( url = environment.config.property("database.url").getString(), username = environment.config.property("database.username").getString(), password = environment.config.property("database.password").getString(), ) runMigrations(config) val database = DatabaseFactory.create(config) // ... }
sql-- src/main/resources/db/migration/V1__create_users.sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, role VARCHAR(20) NOT NULL DEFAULT 'USER', metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_role ON users(role);
kotlin// tables/UsersTable.kt object UsersTable : UUIDTable("users") { val name = varchar("name", 100) val email = varchar("email", 255).uniqueIndex() val role = enumerationByName<Role>("role", 20) val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable() val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone) val updatedAt = timestampWithTimeZone("updated_at").defaultExpression(CurrentTimestampWithTimeZone) } object OrdersTable : UUIDTable("orders") { val userId = uuid("user_id").references(UsersTable.id) val status = enumerationByName<OrderStatus>("status", 20) val totalAmount = long("total_amount") val currency = varchar("currency", 3) val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone) } object OrderItemsTable : UUIDTable("order_items") { val orderId = uuid("order_id").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE) val productId = uuid("product_id") val quantity = integer("quantity") val unitPrice = long("unit_price") }
kotlinobject UserRolesTable : Table("user_roles") { val userId = uuid("user_id").references(UsersTable.id, onDelete = ReferenceOption.CASCADE) val roleId = uuid("role_id").references(RolesTable.id, onDelete = ReferenceOption.CASCADE) override val primaryKey = PrimaryKey(userId, roleId) }
kotlin// Insert suspend fun insertUser(name: String, email: String, role: Role): UUID = newSuspendedTransaction { UsersTable.insertAndGetId { it[UsersTable.name] = name it[UsersTable.email] = email it[UsersTable.role] = role }.value } // Select by ID suspend fun findUserById(id: UUID): UserRow? = newSuspendedTransaction { UsersTable.selectAll() .where { UsersTable.id eq id } .map { it.toUser() } .singleOrNull() } // Select with conditions suspend fun findActiveAdmins(): List<UserRow> = newSuspendedTransaction { UsersTable.selectAll() .where { (UsersTable.role eq Role.ADMIN) } .orderBy(UsersTable.name) .map { it.toUser() } } // Update suspend fun updateUserEmail(id: UUID, newEmail: String): Boolean = newSuspendedTransaction { UsersTable.update({ UsersTable.id eq id }) { it[email] = newEmail it[updatedAt] = CurrentTimestampWithTimeZone } > 0 } // Delete suspend fun deleteUser(id: UUID): Boolean = newSuspendedTransaction { UsersTable.deleteWhere { UsersTable.id eq id } > 0 } // Row mapping private fun ResultRow.toUser() = UserRow( id = this[UsersTable.id].value, name = this[UsersTable.name], email = this[UsersTable.email], role = this[UsersTable.role], metadata = this[UsersTable.metadata], createdAt = this[UsersTable.createdAt], updatedAt = this[UsersTable.updatedAt], )
kotlin// Join queries suspend fun findOrdersWithUser(userId: UUID): List<OrderWithUser> = newSuspendedTransaction { (OrdersTable innerJoin UsersTable) .selectAll() .where { OrdersTable.userId eq userId } .orderBy(OrdersTable.createdAt, SortOrder.DESC) .map { row -> OrderWithUser( orderId = row[OrdersTable.id].value, status = row[OrdersTable.status], totalAmount = row[OrdersTable.totalAmount], userName = row[UsersTable.name], ) } } // Aggregation suspend fun countUsersByRole(): Map<Role, Long> = newSuspendedTransaction { UsersTable .select(UsersTable.role, UsersTable.id.count()) .groupBy(UsersTable.role) .associate { row -> row[UsersTable.role] to row[UsersTable.id.count()] } } // Subqueries suspend fun findUsersWithOrders(): List<UserRow> = newSuspendedTransaction { UsersTable.selectAll() .where { UsersTable.id inSubQuery OrdersTable.select(OrdersTable.userId).withDistinct() } .map { it.toUser() } } // LIKE and pattern matching — always escape user input to prevent wildcard injection private fun escapeLikePattern(input: String): String = input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") suspend fun searchUsers(query: String): List<UserRow> = newSuspendedTransaction { val sanitized = escapeLikePattern(query.lowercase()) UsersTable.selectAll() .where { (UsersTable.name.lowerCase() like "%${sanitized}%") or (UsersTable.email.lowerCase() like "%${sanitized}%") } .map { it.toUser() } }
kotlindata class Page<T>( val data: List<T>, val total: Long, val page: Int, val limit: Int, ) { val totalPages: Int get() = ((total + limit - 1) / limit).toInt() val hasNext: Boolean get() = page < totalPages val hasPrevious: Boolean get() = page > 1 } suspend fun findUsersPaginated(page: Int, limit: Int): Page<UserRow> = newSuspendedTransaction { val total = UsersTable.selectAll().count() val data = UsersTable.selectAll() .orderBy(UsersTable.createdAt, SortOrder.DESC) .limit(limit) .offset(((page - 1) * limit).toLong()) .map { it.toUser() } Page(data = data, total = total, page = page, limit = limit) }
kotlin// Batch insert suspend fun insertUsers(users: List<CreateUserRequest>): List<UUID> = newSuspendedTransaction { UsersTable.batchInsert(users) { user -> this[UsersTable.name] = user.name this[UsersTable.email] = user.email this[UsersTable.role] = user.role }.map { it[UsersTable.id].value } } // Upsert (insert or update on conflict) suspend fun upsertUser(id: UUID, name: String, email: String) { newSuspendedTransaction { UsersTable.upsert(UsersTable.email) { it[UsersTable.id] = EntityID(id, UsersTable) it[UsersTable.name] = name it[UsersTable.email] = email it[updatedAt] = CurrentTimestampWithTimeZone } } }
kotlin// entities/UserEntity.kt class UserEntity(id: EntityID<UUID>) : UUIDEntity(id) { companion object : UUIDEntityClass<UserEntity>(UsersTable) var name by UsersTable.name var email by UsersTable.email var role by UsersTable.role var metadata by UsersTable.metadata var createdAt by UsersTable.createdAt var updatedAt by UsersTable.updatedAt val orders by OrderEntity referrersOn OrdersTable.userId fun toModel(): User = User( id = id.value, name = name, email = email, role = role, metadata = metadata, createdAt = createdAt, updatedAt = updatedAt, ) } class OrderEntity(id: EntityID<UUID>) : UUIDEntity(id) { companion object : UUIDEntityClass<OrderEntity>(OrdersTable) var user by UserEntity referencedOn OrdersTable.userId var status by OrdersTable.status var totalAmount by OrdersTable.totalAmount var currency by OrdersTable.currency var createdAt by OrdersTable.createdAt val items by OrderItemEntity referrersOn OrderItemsTable.orderId }
kotlinsuspend fun findUserByEmail(email: String): User? = newSuspendedTransaction { UserEntity.find { UsersTable.email eq email } .firstOrNull() ?.toModel() } suspend fun createUser(request: CreateUserRequest): User = newSuspendedTransaction { UserEntity.new { name = request.name email = request.email role = request.role }.toModel() } suspend fun updateUser(id: UUID, request: UpdateUserRequest): User? = newSuspendedTransaction { UserEntity.findById(id)?.apply { request.name?.let { name = it } request.email?.let { email = it } updatedAt = OffsetDateTime.now(ZoneOffset.UTC) }?.toModel() }
kotlin// Good: Use newSuspendedTransaction for coroutine support suspend fun performDatabaseOperation(): Result<User> = runCatching { newSuspendedTransaction { val user = UserEntity.new { name = "Alice" email = "alice@example.com" } // All operations in this block are atomic user.toModel() } } // Good: Nested transactions with savepoints suspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) { newSuspendedTransaction { val from = UserEntity.findById(fromId) ?: throw NotFoundException("User $fromId not found") val to = UserEntity.findById(toId) ?: throw NotFoundException("User $toId not found") // Debit from.balance -= amount // Credit to.balance += amount // Both succeed or both fail } }
kotlinsuspend fun readCommittedQuery(): List<User> = newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) { UserEntity.all().map { it.toModel() } } suspend fun serializableOperation() { newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) { // Strictest isolation level for critical operations } }
kotlininterface UserRepository { suspend fun findById(id: UUID): User? suspend fun findByEmail(email: String): User? suspend fun findAll(page: Int, limit: Int): Page<User> suspend fun search(query: String): List<User> suspend fun create(request: CreateUserRequest): User suspend fun update(id: UUID, request: UpdateUserRequest): User? suspend fun delete(id: UUID): Boolean suspend fun count(): Long }
kotlinclass ExposedUserRepository( private val database: Database, ) : UserRepository { override suspend fun findById(id: UUID): User? = newSuspendedTransaction(db = database) { UsersTable.selectAll() .where { UsersTable.id eq id } .map { it.toUser() } .singleOrNull() } override suspend fun findByEmail(email: String): User? = newSuspendedTransaction(db = database) { UsersTable.selectAll() .where { UsersTable.email eq email } .map { it.toUser() } .singleOrNull() } override suspend fun findAll(page: Int, limit: Int): Page<User> = newSuspendedTransaction(db = database) { val total = UsersTable.selectAll().count() val data = UsersTable.selectAll() .orderBy(UsersTable.createdAt, SortOrder.DESC) .limit(limit) .offset(((page - 1) * limit).toLong()) .map { it.toUser() } Page(data = data, total = total, page = page, limit = limit) } override suspend fun search(query: String): List<User> = newSuspendedTransaction(db = database) { val sanitized = escapeLikePattern(query.lowercase()) UsersTable.selectAll() .where { (UsersTable.name.lowerCase() like "%${sanitized}%") or (UsersTable.email.lowerCase() like "%${sanitized}%") } .orderBy(UsersTable.name) .map { it.toUser() } } override suspend fun create(request: CreateUserRequest): User = newSuspendedTransaction(db = database) { UsersTable.insert { it[name] = request.name it[email] = request.email it[role] = request.role }.resultedValues!!.first().toUser() } override suspend fun update(id: UUID, request: UpdateUserRequest): User? = newSuspendedTransaction(db = database) { val updated = UsersTable.update({ UsersTable.id eq id }) { request.name?.let { name -> it[UsersTable.name] = name } request.email?.let { email -> it[UsersTable.email] = email } it[updatedAt] = CurrentTimestampWithTimeZone } if (updated > 0) findById(id) else null } override suspend fun delete(id: UUID): Boolean = newSuspendedTransaction(db = database) { UsersTable.deleteWhere { UsersTable.id eq id } > 0 } override suspend fun count(): Long = newSuspendedTransaction(db = database) { UsersTable.selectAll().count() } private fun ResultRow.toUser() = User( id = this[UsersTable.id].value, name = this[UsersTable.name], email = this[UsersTable.email], role = this[UsersTable.role], metadata = this[UsersTable.metadata], createdAt = this[UsersTable.createdAt], updatedAt = this[UsersTable.updatedAt], ) }
kotlin// Custom column type for JSONB inline fun <reified T : Any> Table.jsonb( name: String, json: Json, ): Column<T> = registerColumn(name, object : ColumnType<T>() { override fun sqlType() = "JSONB" override fun valueFromDB(value: Any): T = when (value) { is String -> json.decodeFromString(value) is PGobject -> { val jsonString = value.value ?: throw IllegalArgumentException("PGobject value is null for column '$name'") json.decodeFromString(jsonString) } else -> throw IllegalArgumentException("Unexpected value: $value") } override fun notNullValueToDB(value: T): Any = PGobject().apply { type = "jsonb" this.value = json.encodeToString(value) } }) // Usage in table @Serializable data class UserMetadata( val preferences: Map<String, String> = emptyMap(), val tags: List<String> = emptyList(), ) object UsersTable : UUIDTable("users") { val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable() }
kotlinclass UserRepositoryTest : FunSpec({ lateinit var database: Database lateinit var repository: UserRepository beforeSpec { database = Database.connect( url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver", ) transaction(database) { SchemaUtils.create(UsersTable) } repository = ExposedUserRepository(database) } beforeTest { transaction(database) { UsersTable.deleteAll() } } test("create and find user") { val user = repository.create(CreateUserRequest("Alice", "alice@example.com")) user.name shouldBe "Alice" user.email shouldBe "alice@example.com" val found = repository.findById(user.id) found shouldBe user } test("findByEmail returns null for unknown email") { val result = repository.findByEmail("unknown@example.com") result.shouldBeNull() } test("pagination works correctly") { repeat(25) { i -> repository.create(CreateUserRequest("User $i", "user$i@example.com")) } val page1 = repository.findAll(page = 1, limit = 10) page1.data shouldHaveSize 10 page1.total shouldBe 25 page1.hasNext shouldBe true val page3 = repository.findAll(page = 3, limit = 10) page3.data shouldHaveSize 5 page3.hasNext shouldBe false } })
kotlin// build.gradle.kts dependencies { // Exposed implementation("org.jetbrains.exposed:exposed-core:1.0.0") implementation("org.jetbrains.exposed:exposed-dao:1.0.0") implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0") implementation("org.jetbrains.exposed:exposed-json:1.0.0") // Database driver implementation("org.postgresql:postgresql:42.7.5") // Connection pooling implementation("com.zaxxer:HikariCP:6.2.1") // Migrations implementation("org.flywaydb:flyway-core:10.22.0") implementation("org.flywaydb:flyway-database-postgresql:10.22.0") // Testing testImplementation("com.h2database:h2:2.3.232") }
| Pattern | Description | |---------|-------------| | object Table : UUIDTable("name") | Define table with UUID primary key | | newSuspendedTransaction { } | Coroutine-safe transaction block | | Table.selectAll().where { } | Query with conditions | | Table.insertAndGetId { } | Insert and return generated ID | | Table.update({ condition }) { } | Update matching rows | | Table.deleteWhere { } | Delete matching rows | | Table.batchInsert(items) { } | Efficient bulk insert | | innerJoin / leftJoin | Join tables | | orderBy / limit / offset | Sort and paginate | | count() / sum() / avg() | Aggregation functions |
Remember: Use the DSL style for simple queries and the DAO style when you need entity lifecycle management. Always use newSuspendedTransaction for coroutine support, and wrap database operations behind a repository interface for testability.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 15,106 | 9,490 | -37% | 1 | 1 | 0% | 2,938 | 7,746 | +164% | 0 | 0 | — |
case-02 | fail→pass | 14,995 | 10,625 | -29% | 1 | 1 | 0% | 2,994 | 7,955 | +166% | 0 | 0 | — |
case-03 | pass→pass | 16,295 | 13,299 | -18% | 1 | 1 | 0% | 3,167 | 8,451 | +167% | 0 | 0 | — |
case-04 | pass→pass | 11,574 | 7,300 | -37% | 1 | 1 | 0% | 2,235 | 7,139 | +219% | 0 | 0 | — |
case-20 | pass→pass | 11,541 | 8,959 | -22% | 1 | 1 | 0% | 2,265 | 7,551 | +233% | 0 | 0 | — |
case-21 | pass→pass | 9,983 | 6,874 | -31% | 1 | 1 | 0% | 1,768 | 7,002 | +296% | 0 | 0 | — |
case-05 | pass→pass | 10,966 | 9,759 | -11% | 1 | 1 | 0% | 2,010 | 7,669 | +282% | 0 | 0 | — |
case-06 | pass→pass | 10,686 | 4,881 | -54% | 1 | 1 | 0% | 1,958 | 6,626 | +238% | 0 | 0 | — |
case-07 | pass→pass | 17,738 | 9,053 | -49% | 1 | 1 | 0% | 2,971 | 7,337 | +147% | 0 | 0 | — |
case-08 | fail→pass | 12,709 | 9,133 | -28% | 1 | 1 | 0% | 2,173 | 7,447 | +243% | 0 | 0 | — |
case-09 | pass→pass | 11,333 | 4,545 | -60% | 1 | 1 | 0% | 2,016 | 6,558 | +225% | 0 | 0 | — |
case-19 | fail→pass | 11,302 | 5,580 | -51% | 1 | 1 | 0% | 2,091 | 6,780 | +224% | 0 | 0 | — |
case-10 | pass→pass | 14,142 | 11,147 | -21% | 1 | 1 | 0% | 2,764 | 8,048 | +191% | 0 | 0 | — |
case-11 | pass→pass | 7,545 | 6,881 | -9% | 1 | 1 | 0% | 1,334 | 7,021 | +426% | 0 | 0 | — |
case-12 | pass→pass | 12,009 | 6,631 | -45% | 1 | 1 | 0% | 2,269 | 6,990 | +208% | 0 | 0 | — |
case-13 | pass→pass | 15,624 | 6,010 | -62% | 1 | 1 | 0% | 2,716 | 6,728 | +148% | 0 | 0 | — |
case-14 | pass→pass | 4,644 | 3,125 | -33% | 1 | 1 | 0% | 747 | 6,212 | +732% | 0 | 0 | — |
case-15 | pass→pass | 11,161 | 6,203 | -44% | 1 | 1 | 0% | 2,023 | 6,928 | +242% | 0 | 0 | — |
case-16 | pass→pass | 15,631 | 10,001 | -36% | 1 | 1 | 0% | 2,832 | 7,724 | +173% | 0 | 0 | — |
case-17 | fail→pass | 9,459 | 4,662 | -51% | 1 | 1 | 0% | 1,758 | 6,600 | +275% | 0 | 0 | — |
case-18 | pass→pass | 10,164 | 4,543 | -55% | 1 | 1 | 0% | 1,654 | 6,578 | +298% | 0 | 0 | — |
case-22 | fail→pass | 10,354 | 8,063 | -22% | 1 | 1 | 0% | 1,890 | 7,198 | +281% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.