Kotlin Exposed Patterns

affaan-m/ECC/docs/zh-CN/skills/kotlin-exposed-patterns

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

JetBrains Exposed ORM 模式,包括 DSL 查询、DAO 模式、事务、HikariCP 连接池、Flyway 迁移和仓库模式。

Instructions onlySoftware Development
AI-generated overview

Reference patterns for using JetBrains Exposed ORM in Kotlin, covering DSL queries, DAO entities, transactions, pooling and migrations.

What it does
This skill provides reference patterns and code examples for database access with the JetBrains Exposed ORM in Kotlin. It covers DSL queries, DAO entity definitions, suspended transactions, HikariCP connection pooling, Flyway migrations, repository interfaces, JSONB columns and in-memory H2 testing. It produces guidance and code snippets rather than executable artifacts.
When to use it
Use it when setting up Exposed-based database access, writing DSL or DAO queries, configuring HikariCP pooling or Flyway migrations, or implementing a repository layer. It also helps when handling JSON columns, pagination, batch operations or Exposed tests with H2.
Requirements
No scripts are shipped; it is instructions and code examples only. Following the examples requires a Kotlin project with Exposed modules, a JDBC driver such as PostgreSQL, HikariCP, Flyway and, for tests, H2.

Kotlin Exposed 模式

使用 JetBrains Exposed ORM 进行数据库访问的全面模式,包括 DSL 查询、DAO、事务以及生产就绪的配置。

何时使用

  • 使用 Exposed 设置数据库访问
  • 使用 Exposed DSL 或 DAO 编写 SQL 查询
  • 使用 HikariCP 配置连接池
  • 使用 Flyway 创建数据库迁移
  • 使用 Exposed 实现仓储模式
  • 处理 JSON 列和复杂查询

工作原理

Exposed 提供两种查询风格:用于直接类似 SQL 表达式的 DSL 和用于实体生命周期管理的 DAO。HikariCP 通过 HikariConfig 配置来管理可重用的数据库连接池。Flyway 在启动时运行版本化的 SQL 迁移脚本以保持模式同步。所有数据库操作都在 newSuspendedTransaction 块内运行,以确保协程安全和原子性。仓储模式将 Exposed 查询包装在接口之后,使业务逻辑与数据层解耦,并且测试可以使用内存中的 H2 数据库。

示例

DSL 查询

kotlin
suspend fun findUserById(id: UUID): UserRow? =    newSuspendedTransaction {        UsersTable.selectAll()            .where { UsersTable.id eq id }            .map { it.toUser() }            .singleOrNull()    }

DAO 实体用法

kotlin
suspend fun createUser(request: CreateUserRequest): User =    newSuspendedTransaction {        UserEntity.new {            name = request.name            email = request.email            role = request.role        }.toModel()    }

HikariCP 配置

kotlin
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()}

数据库设置

HikariCP 连接池

kotlin
// DatabaseFactory.ktobject 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,)

Flyway 迁移

kotlin
// FlywayMigration.ktfun runMigrations(config: DatabaseConfig) {    Flyway.configure()        .dataSource(config.url, config.username, config.password)        .locations("classpath:db/migration")        .baselineOnMigrate(true)        .load()        .migrate()}
// Application startupfun 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.sqlCREATE 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);

表定义

DSL 风格表

kotlin
// tables/UsersTable.ktobject 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")}

复合表

kotlin
object 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)}

DSL 查询

基本 CRUD

kotlin
// Insertsuspend 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 IDsuspend fun findUserById(id: UUID): UserRow? =    newSuspendedTransaction {        UsersTable.selectAll()            .where { UsersTable.id eq id }            .map { it.toUser() }            .singleOrNull()    }
// Select with conditionssuspend fun findActiveAdmins(): List<UserRow> =    newSuspendedTransaction {        UsersTable.selectAll()            .where { (UsersTable.role eq Role.ADMIN) }            .orderBy(UsersTable.name)            .map { it.toUser() }    }
// Updatesuspend fun updateUserEmail(id: UUID, newEmail: String): Boolean =    newSuspendedTransaction {        UsersTable.update({ UsersTable.id eq id }) {            it[email] = newEmail            it[updatedAt] = CurrentTimestampWithTimeZone        } > 0    }
// Deletesuspend fun deleteUser(id: UUID): Boolean =    newSuspendedTransaction {        UsersTable.deleteWhere { UsersTable.id eq id } > 0    }
// Row mappingprivate 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 queriessuspend 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],                )            }    }
// Aggregationsuspend 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()]            }    }
// Subqueriessuspend 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 injectionprivate 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() }    }

分页

kotlin
data 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 insertsuspend 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        }    }}

DAO 模式

实体定义

kotlin
// entities/UserEntity.ktclass 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}

DAO 操作

kotlin
suspend 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 supportsuspend fun performDatabaseOperation(): Result<User> =    runCatching {        newSuspendedTransaction {            val user = UserEntity.new {                name = "Alice"                email = "[email protected]"            }            // All operations in this block are atomic            user.toModel()        }    }
// Good: Nested transactions with savepointssuspend 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    }}

事务隔离级别

kotlin
suspend 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    }}

仓储模式

接口定义

kotlin
interface 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}

Exposed 实现

kotlin
class 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],    )}

JSON 列

使用 kotlinx.serialization 的 JSONB

kotlin
// Custom column type for JSONBinline 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@Serializabledata 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()}

使用 Exposed 进行测试

用于测试的内存数据库

kotlin
class 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", "[email protected]"))
        user.name shouldBe "Alice"        user.email shouldBe "[email protected]"
        val found = repository.findById(user.id)        found shouldBe user    }
    test("findByEmail returns null for unknown email") {        val result = repository.findByEmail("[email protected]")        result.shouldBeNull()    }
    test("pagination works correctly") {        repeat(25) { i ->            repository.create(CreateUserRequest("User $i", "[email protected]"))        }
        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    }})

Gradle 依赖项

kotlin
// build.gradle.ktsdependencies {    // 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")}

快速参考:Exposed 模式

模式描述
object Table : UUIDTable("name")定义具有 UUID 主键的表
newSuspendedTransaction { }协程安全的事务块
Table.selectAll().where { }带条件的查询
Table.insertAndGetId { }插入并返回生成的 ID
Table.update({ condition }) { }更新匹配的行
Table.deleteWhere { }删除匹配的行
Table.batchInsert(items) { }高效的批量插入
innerJoin / leftJoin连接表
orderBy / limit / offset排序和分页
count() / sum() / avg()聚合函数

记住:对于简单查询使用 DSL 风格,当需要实体生命周期管理时使用 DAO 风格。始终使用 newSuspendedTransaction 以获得协程支持,并将数据库操作包装在仓储接口之后以提高可测试性。

Source and attribution

Source:affaan-m/ECCindocs/zh-CN/skills/kotlin-exposed-patternsat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal