[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7BFE56CDZSPCS92M0RV64XE":3,"skill-same:01M7BFE56CDZSPCS92M0RV64XE":48},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":6,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M7BFE56CDZSPCS92M0RV64XE","kotlin-exposed-patterns",null,"JetBrains Exposed ORM 模式，包括 DSL 查询、DAO 模式、事务、HikariCP 连接池、Flyway 迁移和仓库模式。","YYC-Cube\u002FYYC3-Knowledge-Base","YYC-Cube","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F235006543?v=4","01M79J2PH4SWD2X0825X6PN748","yyc3","01M79J2PHVK3F6ZBYT8K2SCZCW",1,"MIT","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fkotlin-exposed-patterns\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: kotlin-exposed-patterns\ndescription: JetBrains Exposed ORM 模式，包括 DSL 查询、DAO 模式、事务、HikariCP 连接池、Flyway 迁移和仓库模式。\norigin: ECC\n---\n\n# Kotlin Exposed 模式\n\n使用 JetBrains Exposed ORM 进行数据库访问的全面模式，包括 DSL 查询、DAO、事务以及生产就绪的配置。\n\n## 何时使用\n\n* 使用 Exposed 设置数据库访问\n* 使用 Exposed DSL 或 DAO 编写 SQL 查询\n* 使用 HikariCP 配置连接池\n* 使用 Flyway 创建数据库迁移\n* 使用 Exposed 实现仓储模式\n* 处理 JSON 列和复杂查询\n\n## 工作原理\n\nExposed 提供两种查询风格：用于直接类似 SQL 表达式的 DSL 和用于实体生命周期管理的 DAO。HikariCP 通过 `HikariConfig` 配置来管理可重用的数据库连接池。Flyway 在启动时运行版本化的 SQL 迁移脚本以保持模式同步。所有数据库操作都在 `newSuspendedTransaction` 块内运行，以确保协程安全和原子性。仓储模式将 Exposed 查询包装在接口之后，使业务逻辑与数据层解耦，并且测试可以使用内存中的 H2 数据库。\n\n## 示例\n\n### DSL 查询\n\n```kotlin\nsuspend fun findUserById(id: UUID): UserRow? =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { UsersTable.id eq id }\n            .map { it.toUser() }\n            .singleOrNull()\n    }\n```\n\n### DAO 实体用法\n\n```kotlin\nsuspend fun createUser(request: CreateUserRequest): User =\n    newSuspendedTransaction {\n        UserEntity.new {\n            name = request.name\n            email = request.email\n            role = request.role\n        }.toModel()\n    }\n```\n\n### HikariCP 配置\n\n```kotlin\nval hikariConfig = HikariConfig().apply {\n    driverClassName = config.driver\n    jdbcUrl = config.url\n    username = config.username\n    password = config.password\n    maximumPoolSize = config.maxPoolSize\n    isAutoCommit = false\n    transactionIsolation = \"TRANSACTION_READ_COMMITTED\"\n    validate()\n}\n```\n\n## 数据库设置\n\n### HikariCP 连接池\n\n```kotlin\n\u002F\u002F DatabaseFactory.kt\nobject DatabaseFactory {\n    fun create(config: DatabaseConfig): Database {\n        val hikariConfig = HikariConfig().apply {\n            driverClassName = config.driver\n            jdbcUrl = config.url\n            username = config.username\n            password = config.password\n            maximumPoolSize = config.maxPoolSize\n            isAutoCommit = false\n            transactionIsolation = \"TRANSACTION_READ_COMMITTED\"\n            validate()\n        }\n\n        return Database.connect(HikariDataSource(hikariConfig))\n    }\n}\n\ndata class DatabaseConfig(\n    val url: String,\n    val driver: String = \"org.postgresql.Driver\",\n    val username: String = \"\",\n    val password: String = \"\",\n    val maxPoolSize: Int = 10,\n)\n```\n\n### Flyway 迁移\n\n```kotlin\n\u002F\u002F FlywayMigration.kt\nfun runMigrations(config: DatabaseConfig) {\n    Flyway.configure()\n        .dataSource(config.url, config.username, config.password)\n        .locations(\"classpath:db\u002Fmigration\")\n        .baselineOnMigrate(true)\n        .load()\n        .migrate()\n}\n\n\u002F\u002F Application startup\nfun Application.module() {\n    val config = DatabaseConfig(\n        url = environment.config.property(\"database.url\").getString(),\n        username = environment.config.property(\"database.username\").getString(),\n        password = environment.config.property(\"database.password\").getString(),\n    )\n    runMigrations(config)\n    val database = DatabaseFactory.create(config)\n    \u002F\u002F ...\n}\n```\n\n### 迁移文件\n\n```sql\n-- src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\u002FV1__create_users.sql\nCREATE TABLE users (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    name VARCHAR(100) NOT NULL,\n    email VARCHAR(255) NOT NULL UNIQUE,\n    role VARCHAR(20) NOT NULL DEFAULT 'USER',\n    metadata JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_users_email ON users(email);\nCREATE INDEX idx_users_role ON users(role);\n```\n\n## 表定义\n\n### DSL 风格表\n\n```kotlin\n\u002F\u002F tables\u002FUsersTable.kt\nobject UsersTable : UUIDTable(\"users\") {\n    val name = varchar(\"name\", 100)\n    val email = varchar(\"email\", 255).uniqueIndex()\n    val role = enumerationByName\u003CRole>(\"role\", 20)\n    val metadata = jsonb\u003CUserMetadata>(\"metadata\", Json.Default).nullable()\n    val createdAt = timestampWithTimeZone(\"created_at\").defaultExpression(CurrentTimestampWithTimeZone)\n    val updatedAt = timestampWithTimeZone(\"updated_at\").defaultExpression(CurrentTimestampWithTimeZone)\n}\n\nobject OrdersTable : UUIDTable(\"orders\") {\n    val userId = uuid(\"user_id\").references(UsersTable.id)\n    val status = enumerationByName\u003COrderStatus>(\"status\", 20)\n    val totalAmount = long(\"total_amount\")\n    val currency = varchar(\"currency\", 3)\n    val createdAt = timestampWithTimeZone(\"created_at\").defaultExpression(CurrentTimestampWithTimeZone)\n}\n\nobject OrderItemsTable : UUIDTable(\"order_items\") {\n    val orderId = uuid(\"order_id\").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE)\n    val productId = uuid(\"product_id\")\n    val quantity = integer(\"quantity\")\n    val unitPrice = long(\"unit_price\")\n}\n```\n\n### 复合表\n\n```kotlin\nobject UserRolesTable : Table(\"user_roles\") {\n    val userId = uuid(\"user_id\").references(UsersTable.id, onDelete = ReferenceOption.CASCADE)\n    val roleId = uuid(\"role_id\").references(RolesTable.id, onDelete = ReferenceOption.CASCADE)\n    override val primaryKey = PrimaryKey(userId, roleId)\n}\n```\n\n## DSL 查询\n\n### 基本 CRUD\n\n```kotlin\n\u002F\u002F Insert\nsuspend fun insertUser(name: String, email: String, role: Role): UUID =\n    newSuspendedTransaction {\n        UsersTable.insertAndGetId {\n            it[UsersTable.name] = name\n            it[UsersTable.email] = email\n            it[UsersTable.role] = role\n        }.value\n    }\n\n\u002F\u002F Select by ID\nsuspend fun findUserById(id: UUID): UserRow? =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { UsersTable.id eq id }\n            .map { it.toUser() }\n            .singleOrNull()\n    }\n\n\u002F\u002F Select with conditions\nsuspend fun findActiveAdmins(): List\u003CUserRow> =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { (UsersTable.role eq Role.ADMIN) }\n            .orderBy(UsersTable.name)\n            .map { it.toUser() }\n    }\n\n\u002F\u002F Update\nsuspend fun updateUserEmail(id: UUID, newEmail: String): Boolean =\n    newSuspendedTransaction {\n        UsersTable.update({ UsersTable.id eq id }) {\n            it[email] = newEmail\n            it[updatedAt] = CurrentTimestampWithTimeZone\n        } > 0\n    }\n\n\u002F\u002F Delete\nsuspend fun deleteUser(id: UUID): Boolean =\n    newSuspendedTransaction {\n        UsersTable.deleteWhere { UsersTable.id eq id } > 0\n    }\n\n\u002F\u002F Row mapping\nprivate fun ResultRow.toUser() = UserRow(\n    id = this[UsersTable.id].value,\n    name = this[UsersTable.name],\n    email = this[UsersTable.email],\n    role = this[UsersTable.role],\n    metadata = this[UsersTable.metadata],\n    createdAt = this[UsersTable.createdAt],\n    updatedAt = this[UsersTable.updatedAt],\n)\n```\n\n### 高级查询\n\n```kotlin\n\u002F\u002F Join queries\nsuspend fun findOrdersWithUser(userId: UUID): List\u003COrderWithUser> =\n    newSuspendedTransaction {\n        (OrdersTable innerJoin UsersTable)\n            .selectAll()\n            .where { OrdersTable.userId eq userId }\n            .orderBy(OrdersTable.createdAt, SortOrder.DESC)\n            .map { row ->\n                OrderWithUser(\n                    orderId = row[OrdersTable.id].value,\n                    status = row[OrdersTable.status],\n                    totalAmount = row[OrdersTable.totalAmount],\n                    userName = row[UsersTable.name],\n                )\n            }\n    }\n\n\u002F\u002F Aggregation\nsuspend fun countUsersByRole(): Map\u003CRole, Long> =\n    newSuspendedTransaction {\n        UsersTable\n            .select(UsersTable.role, UsersTable.id.count())\n            .groupBy(UsersTable.role)\n            .associate { row ->\n                row[UsersTable.role] to row[UsersTable.id.count()]\n            }\n    }\n\n\u002F\u002F Subqueries\nsuspend fun findUsersWithOrders(): List\u003CUserRow> =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where {\n                UsersTable.id inSubQuery\n                    OrdersTable.select(OrdersTable.userId).withDistinct()\n            }\n            .map { it.toUser() }\n    }\n\n\u002F\u002F LIKE and pattern matching — always escape user input to prevent wildcard injection\nprivate fun escapeLikePattern(input: String): String =\n    input.replace(\"\\\\\", \"\\\\\\\\\").replace(\"%\", \"\\\\%\").replace(\"_\", \"\\\\_\")\n\nsuspend fun searchUsers(query: String): List\u003CUserRow> =\n    newSuspendedTransaction {\n        val sanitized = escapeLikePattern(query.lowercase())\n        UsersTable.selectAll()\n            .where {\n                (UsersTable.name.lowerCase() like \"%${sanitized}%\") or\n                    (UsersTable.email.lowerCase() like \"%${sanitized}%\")\n            }\n            .map { it.toUser() }\n    }\n```\n\n### 分页\n\n```kotlin\ndata class Page\u003CT>(\n    val data: List\u003CT>,\n    val total: Long,\n    val page: Int,\n    val limit: Int,\n) {\n    val totalPages: Int get() = ((total + limit - 1) \u002F limit).toInt()\n    val hasNext: Boolean get() = page \u003C totalPages\n    val hasPrevious: Boolean get() = page > 1\n}\n\nsuspend fun findUsersPaginated(page: Int, limit: Int): Page\u003CUserRow> =\n    newSuspendedTransaction {\n        val total = UsersTable.selectAll().count()\n        val data = UsersTable.selectAll()\n            .orderBy(UsersTable.createdAt, SortOrder.DESC)\n            .limit(limit)\n            .offset(((page - 1) * limit).toLong())\n            .map { it.toUser() }\n\n        Page(data = data, total = total, page = page, limit = limit)\n    }\n```\n\n### 批量操作\n\n```kotlin\n\u002F\u002F Batch insert\nsuspend fun insertUsers(users: List\u003CCreateUserRequest>): List\u003CUUID> =\n    newSuspendedTransaction {\n        UsersTable.batchInsert(users) { user ->\n            this[UsersTable.name] = user.name\n            this[UsersTable.email] = user.email\n            this[UsersTable.role] = user.role\n        }.map { it[UsersTable.id].value }\n    }\n\n\u002F\u002F Upsert (insert or update on conflict)\nsuspend fun upsertUser(id: UUID, name: String, email: String) {\n    newSuspendedTransaction {\n        UsersTable.upsert(UsersTable.email) {\n            it[UsersTable.id] = EntityID(id, UsersTable)\n            it[UsersTable.name] = name\n            it[UsersTable.email] = email\n            it[updatedAt] = CurrentTimestampWithTimeZone\n        }\n    }\n}\n```\n\n## DAO 模式\n\n### 实体定义\n\n```kotlin\n\u002F\u002F entities\u002FUserEntity.kt\nclass UserEntity(id: EntityID\u003CUUID>) : UUIDEntity(id) {\n    companion object : UUIDEntityClass\u003CUserEntity>(UsersTable)\n\n    var name by UsersTable.name\n    var email by UsersTable.email\n    var role by UsersTable.role\n    var metadata by UsersTable.metadata\n    var createdAt by UsersTable.createdAt\n    var updatedAt by UsersTable.updatedAt\n\n    val orders by OrderEntity referrersOn OrdersTable.userId\n\n    fun toModel(): User = User(\n        id = id.value,\n        name = name,\n        email = email,\n        role = role,\n        metadata = metadata,\n        createdAt = createdAt,\n        updatedAt = updatedAt,\n    )\n}\n\nclass OrderEntity(id: EntityID\u003CUUID>) : UUIDEntity(id) {\n    companion object : UUIDEntityClass\u003COrderEntity>(OrdersTable)\n\n    var user by UserEntity referencedOn OrdersTable.userId\n    var status by OrdersTable.status\n    var totalAmount by OrdersTable.totalAmount\n    var currency by OrdersTable.currency\n    var createdAt by OrdersTable.createdAt\n\n    val items by OrderItemEntity referrersOn OrderItemsTable.orderId\n}\n```\n\n### DAO 操作\n\n```kotlin\nsuspend fun findUserByEmail(email: String): User? =\n    newSuspendedTransaction {\n        UserEntity.find { UsersTable.email eq email }\n            .firstOrNull()\n            ?.toModel()\n    }\n\nsuspend fun createUser(request: CreateUserRequest): User =\n    newSuspendedTransaction {\n        UserEntity.new {\n            name = request.name\n            email = request.email\n            role = request.role\n        }.toModel()\n    }\n\nsuspend fun updateUser(id: UUID, request: UpdateUserRequest): User? =\n    newSuspendedTransaction {\n        UserEntity.findById(id)?.apply {\n            request.name?.let { name = it }\n            request.email?.let { email = it }\n            updatedAt = OffsetDateTime.now(ZoneOffset.UTC)\n        }?.toModel()\n    }\n```\n\n## 事务\n\n### 挂起事务支持\n\n```kotlin\n\u002F\u002F Good: Use newSuspendedTransaction for coroutine support\nsuspend fun performDatabaseOperation(): Result\u003CUser> =\n    runCatching {\n        newSuspendedTransaction {\n            val user = UserEntity.new {\n                name = \"Alice\"\n                email = \"alice@example.com\"\n            }\n            \u002F\u002F All operations in this block are atomic\n            user.toModel()\n        }\n    }\n\n\u002F\u002F Good: Nested transactions with savepoints\nsuspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) {\n    newSuspendedTransaction {\n        val from = UserEntity.findById(fromId) ?: throw NotFoundException(\"User $fromId not found\")\n        val to = UserEntity.findById(toId) ?: throw NotFoundException(\"User $toId not found\")\n\n        \u002F\u002F Debit\n        from.balance -= amount\n        \u002F\u002F Credit\n        to.balance += amount\n\n        \u002F\u002F Both succeed or both fail\n    }\n}\n```\n\n### 事务隔离级别\n\n```kotlin\nsuspend fun readCommittedQuery(): List\u003CUser> =\n    newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {\n        UserEntity.all().map { it.toModel() }\n    }\n\nsuspend fun serializableOperation() {\n    newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) {\n        \u002F\u002F Strictest isolation level for critical operations\n    }\n}\n```\n\n## 仓储模式\n\n### 接口定义\n\n```kotlin\ninterface UserRepository {\n    suspend fun findById(id: UUID): User?\n    suspend fun findByEmail(email: String): User?\n    suspend fun findAll(page: Int, limit: Int): Page\u003CUser>\n    suspend fun search(query: String): List\u003CUser>\n    suspend fun create(request: CreateUserRequest): User\n    suspend fun update(id: UUID, request: UpdateUserRequest): User?\n    suspend fun delete(id: UUID): Boolean\n    suspend fun count(): Long\n}\n```\n\n### Exposed 实现\n\n```kotlin\nclass ExposedUserRepository(\n    private val database: Database,\n) : UserRepository {\n\n    override suspend fun findById(id: UUID): User? =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll()\n                .where { UsersTable.id eq id }\n                .map { it.toUser() }\n                .singleOrNull()\n        }\n\n    override suspend fun findByEmail(email: String): User? =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll()\n                .where { UsersTable.email eq email }\n                .map { it.toUser() }\n                .singleOrNull()\n        }\n\n    override suspend fun findAll(page: Int, limit: Int): Page\u003CUser> =\n        newSuspendedTransaction(db = database) {\n            val total = UsersTable.selectAll().count()\n            val data = UsersTable.selectAll()\n                .orderBy(UsersTable.createdAt, SortOrder.DESC)\n                .limit(limit)\n                .offset(((page - 1) * limit).toLong())\n                .map { it.toUser() }\n            Page(data = data, total = total, page = page, limit = limit)\n        }\n\n    override suspend fun search(query: String): List\u003CUser> =\n        newSuspendedTransaction(db = database) {\n            val sanitized = escapeLikePattern(query.lowercase())\n            UsersTable.selectAll()\n                .where {\n                    (UsersTable.name.lowerCase() like \"%${sanitized}%\") or\n                        (UsersTable.email.lowerCase() like \"%${sanitized}%\")\n                }\n                .orderBy(UsersTable.name)\n                .map { it.toUser() }\n        }\n\n    override suspend fun create(request: CreateUserRequest): User =\n        newSuspendedTransaction(db = database) {\n            UsersTable.insert {\n                it[name] = request.name\n                it[email] = request.email\n                it[role] = request.role\n            }.resultedValues!!.first().toUser()\n        }\n\n    override suspend fun update(id: UUID, request: UpdateUserRequest): User? =\n        newSuspendedTransaction(db = database) {\n            val updated = UsersTable.update({ UsersTable.id eq id }) {\n                request.name?.let { name -> it[UsersTable.name] = name }\n                request.email?.let { email -> it[UsersTable.email] = email }\n                it[updatedAt] = CurrentTimestampWithTimeZone\n            }\n            if (updated > 0) findById(id) else null\n        }\n\n    override suspend fun delete(id: UUID): Boolean =\n        newSuspendedTransaction(db = database) {\n            UsersTable.deleteWhere { UsersTable.id eq id } > 0\n        }\n\n    override suspend fun count(): Long =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll().count()\n        }\n\n    private fun ResultRow.toUser() = User(\n        id = this[UsersTable.id].value,\n        name = this[UsersTable.name],\n        email = this[UsersTable.email],\n        role = this[UsersTable.role],\n        metadata = this[UsersTable.metadata],\n        createdAt = this[UsersTable.createdAt],\n        updatedAt = this[UsersTable.updatedAt],\n    )\n}\n```\n\n## JSON 列\n\n### 使用 kotlinx.serialization 的 JSONB\n\n```kotlin\n\u002F\u002F Custom column type for JSONB\ninline fun \u003Creified T : Any> Table.jsonb(\n    name: String,\n    json: Json,\n): Column\u003CT> = registerColumn(name, object : ColumnType\u003CT>() {\n    override fun sqlType() = \"JSONB\"\n\n    override fun valueFromDB(value: Any): T = when (value) {\n        is String -> json.decodeFromString(value)\n        is PGobject -> {\n            val jsonString = value.value\n                ?: throw IllegalArgumentException(\"PGobject value is null for column '$name'\")\n            json.decodeFromString(jsonString)\n        }\n        else -> throw IllegalArgumentException(\"Unexpected value: $value\")\n    }\n\n    override fun notNullValueToDB(value: T): Any =\n        PGobject().apply {\n            type = \"jsonb\"\n            this.value = json.encodeToString(value)\n        }\n})\n\n\u002F\u002F Usage in table\n@Serializable\ndata class UserMetadata(\n    val preferences: Map\u003CString, String> = emptyMap(),\n    val tags: List\u003CString> = emptyList(),\n)\n\nobject UsersTable : UUIDTable(\"users\") {\n    val metadata = jsonb\u003CUserMetadata>(\"metadata\", Json.Default).nullable()\n}\n```\n\n## 使用 Exposed 进行测试\n\n### 用于测试的内存数据库\n\n```kotlin\nclass UserRepositoryTest : FunSpec({\n    lateinit var database: Database\n    lateinit var repository: UserRepository\n\n    beforeSpec {\n        database = Database.connect(\n            url = \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL\",\n            driver = \"org.h2.Driver\",\n        )\n        transaction(database) {\n            SchemaUtils.create(UsersTable)\n        }\n        repository = ExposedUserRepository(database)\n    }\n\n    beforeTest {\n        transaction(database) {\n            UsersTable.deleteAll()\n        }\n    }\n\n    test(\"create and find user\") {\n        val user = repository.create(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n\n        user.name shouldBe \"Alice\"\n        user.email shouldBe \"alice@example.com\"\n\n        val found = repository.findById(user.id)\n        found shouldBe user\n    }\n\n    test(\"findByEmail returns null for unknown email\") {\n        val result = repository.findByEmail(\"unknown@example.com\")\n        result.shouldBeNull()\n    }\n\n    test(\"pagination works correctly\") {\n        repeat(25) { i ->\n            repository.create(CreateUserRequest(\"User $i\", \"user$i@example.com\"))\n        }\n\n        val page1 = repository.findAll(page = 1, limit = 10)\n        page1.data shouldHaveSize 10\n        page1.total shouldBe 25\n        page1.hasNext shouldBe true\n\n        val page3 = repository.findAll(page = 3, limit = 10)\n        page3.data shouldHaveSize 5\n        page3.hasNext shouldBe false\n    }\n})\n```\n\n## Gradle 依赖项\n\n```kotlin\n\u002F\u002F build.gradle.kts\ndependencies {\n    \u002F\u002F Exposed\n    implementation(\"org.jetbrains.exposed:exposed-core:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-dao:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-jdbc:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-json:1.0.0\")\n\n    \u002F\u002F Database driver\n    implementation(\"org.postgresql:postgresql:42.7.5\")\n\n    \u002F\u002F Connection pooling\n    implementation(\"com.zaxxer:HikariCP:6.2.1\")\n\n    \u002F\u002F Migrations\n    implementation(\"org.flywaydb:flyway-core:10.22.0\")\n    implementation(\"org.flywaydb:flyway-database-postgresql:10.22.0\")\n\n    \u002F\u002F Testing\n    testImplementation(\"com.h2database:h2:2.3.232\")\n}\n```\n\n## 快速参考：Exposed 模式\n\n| 模式 | 描述 |\n|---------|-------------|\n| `object Table : UUIDTable(\"name\")` | 定义具有 UUID 主键的表 |\n| `newSuspendedTransaction { }` | 协程安全的事务块 |\n| `Table.selectAll().where { }` | 带条件的查询 |\n| `Table.insertAndGetId { }` | 插入并返回生成的 ID |\n| `Table.update({ condition }) { }` | 更新匹配的行 |\n| `Table.deleteWhere { }` | 删除匹配的行 |\n| `Table.batchInsert(items) { }` | 高效的批量插入 |\n| `innerJoin` \u002F `leftJoin` | 连接表 |\n| `orderBy` \u002F `limit` \u002F `offset` | 排序和分页 |\n| `count()` \u002F `sum()` \u002F `avg()` | 聚合函数 |\n\n**记住**：对于简单查询使用 DSL 风格，当需要实体生命周期管理时使用 DAO 风格。始终使用 `newSuspendedTransaction` 以获得协程支持，并将数据库操作包装在仓储接口之后以提高可测试性。\n","# Kotlin Exposed 模式\n\n使用 JetBrains Exposed ORM 进行数据库访问的全面模式，包括 DSL 查询、DAO、事务以及生产就绪的配置。\n\n## 何时使用\n\n* 使用 Exposed 设置数据库访问\n* 使用 Exposed DSL 或 DAO 编写 SQL 查询\n* 使用 HikariCP 配置连接池\n* 使用 Flyway 创建数据库迁移\n* 使用 Exposed 实现仓储模式\n* 处理 JSON 列和复杂查询\n\n## 工作原理\n\nExposed 提供两种查询风格：用于直接类似 SQL 表达式的 DSL 和用于实体生命周期管理的 DAO。HikariCP 通过 `HikariConfig` 配置来管理可重用的数据库连接池。Flyway 在启动时运行版本化的 SQL 迁移脚本以保持模式同步。所有数据库操作都在 `newSuspendedTransaction` 块内运行，以确保协程安全和原子性。仓储模式将 Exposed 查询包装在接口之后，使业务逻辑与数据层解耦，并且测试可以使用内存中的 H2 数据库。\n\n## 示例\n\n### DSL 查询\n\n```kotlin\nsuspend fun findUserById(id: UUID): UserRow? =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { UsersTable.id eq id }\n            .map { it.toUser() }\n            .singleOrNull()\n    }\n```\n\n### DAO 实体用法\n\n```kotlin\nsuspend fun createUser(request: CreateUserRequest): User =\n    newSuspendedTransaction {\n        UserEntity.new {\n            name = request.name\n            email = request.email\n            role = request.role\n        }.toModel()\n    }\n```\n\n### HikariCP 配置\n\n```kotlin\nval hikariConfig = HikariConfig().apply {\n    driverClassName = config.driver\n    jdbcUrl = config.url\n    username = config.username\n    password = config.password\n    maximumPoolSize = config.maxPoolSize\n    isAutoCommit = false\n    transactionIsolation = \"TRANSACTION_READ_COMMITTED\"\n    validate()\n}\n```\n\n## 数据库设置\n\n### HikariCP 连接池\n\n```kotlin\n\u002F\u002F DatabaseFactory.kt\nobject DatabaseFactory {\n    fun create(config: DatabaseConfig): Database {\n        val hikariConfig = HikariConfig().apply {\n            driverClassName = config.driver\n            jdbcUrl = config.url\n            username = config.username\n            password = config.password\n            maximumPoolSize = config.maxPoolSize\n            isAutoCommit = false\n            transactionIsolation = \"TRANSACTION_READ_COMMITTED\"\n            validate()\n        }\n\n        return Database.connect(HikariDataSource(hikariConfig))\n    }\n}\n\ndata class DatabaseConfig(\n    val url: String,\n    val driver: String = \"org.postgresql.Driver\",\n    val username: String = \"\",\n    val password: String = \"\",\n    val maxPoolSize: Int = 10,\n)\n```\n\n### Flyway 迁移\n\n```kotlin\n\u002F\u002F FlywayMigration.kt\nfun runMigrations(config: DatabaseConfig) {\n    Flyway.configure()\n        .dataSource(config.url, config.username, config.password)\n        .locations(\"classpath:db\u002Fmigration\")\n        .baselineOnMigrate(true)\n        .load()\n        .migrate()\n}\n\n\u002F\u002F Application startup\nfun Application.module() {\n    val config = DatabaseConfig(\n        url = environment.config.property(\"database.url\").getString(),\n        username = environment.config.property(\"database.username\").getString(),\n        password = environment.config.property(\"database.password\").getString(),\n    )\n    runMigrations(config)\n    val database = DatabaseFactory.create(config)\n    \u002F\u002F ...\n}\n```\n\n### 迁移文件\n\n```sql\n-- src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\u002FV1__create_users.sql\nCREATE TABLE users (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    name VARCHAR(100) NOT NULL,\n    email VARCHAR(255) NOT NULL UNIQUE,\n    role VARCHAR(20) NOT NULL DEFAULT 'USER',\n    metadata JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_users_email ON users(email);\nCREATE INDEX idx_users_role ON users(role);\n```\n\n## 表定义\n\n### DSL 风格表\n\n```kotlin\n\u002F\u002F tables\u002FUsersTable.kt\nobject UsersTable : UUIDTable(\"users\") {\n    val name = varchar(\"name\", 100)\n    val email = varchar(\"email\", 255).uniqueIndex()\n    val role = enumerationByName\u003CRole>(\"role\", 20)\n    val metadata = jsonb\u003CUserMetadata>(\"metadata\", Json.Default).nullable()\n    val createdAt = timestampWithTimeZone(\"created_at\").defaultExpression(CurrentTimestampWithTimeZone)\n    val updatedAt = timestampWithTimeZone(\"updated_at\").defaultExpression(CurrentTimestampWithTimeZone)\n}\n\nobject OrdersTable : UUIDTable(\"orders\") {\n    val userId = uuid(\"user_id\").references(UsersTable.id)\n    val status = enumerationByName\u003COrderStatus>(\"status\", 20)\n    val totalAmount = long(\"total_amount\")\n    val currency = varchar(\"currency\", 3)\n    val createdAt = timestampWithTimeZone(\"created_at\").defaultExpression(CurrentTimestampWithTimeZone)\n}\n\nobject OrderItemsTable : UUIDTable(\"order_items\") {\n    val orderId = uuid(\"order_id\").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE)\n    val productId = uuid(\"product_id\")\n    val quantity = integer(\"quantity\")\n    val unitPrice = long(\"unit_price\")\n}\n```\n\n### 复合表\n\n```kotlin\nobject UserRolesTable : Table(\"user_roles\") {\n    val userId = uuid(\"user_id\").references(UsersTable.id, onDelete = ReferenceOption.CASCADE)\n    val roleId = uuid(\"role_id\").references(RolesTable.id, onDelete = ReferenceOption.CASCADE)\n    override val primaryKey = PrimaryKey(userId, roleId)\n}\n```\n\n## DSL 查询\n\n### 基本 CRUD\n\n```kotlin\n\u002F\u002F Insert\nsuspend fun insertUser(name: String, email: String, role: Role): UUID =\n    newSuspendedTransaction {\n        UsersTable.insertAndGetId {\n            it[UsersTable.name] = name\n            it[UsersTable.email] = email\n            it[UsersTable.role] = role\n        }.value\n    }\n\n\u002F\u002F Select by ID\nsuspend fun findUserById(id: UUID): UserRow? =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { UsersTable.id eq id }\n            .map { it.toUser() }\n            .singleOrNull()\n    }\n\n\u002F\u002F Select with conditions\nsuspend fun findActiveAdmins(): List\u003CUserRow> =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where { (UsersTable.role eq Role.ADMIN) }\n            .orderBy(UsersTable.name)\n            .map { it.toUser() }\n    }\n\n\u002F\u002F Update\nsuspend fun updateUserEmail(id: UUID, newEmail: String): Boolean =\n    newSuspendedTransaction {\n        UsersTable.update({ UsersTable.id eq id }) {\n            it[email] = newEmail\n            it[updatedAt] = CurrentTimestampWithTimeZone\n        } > 0\n    }\n\n\u002F\u002F Delete\nsuspend fun deleteUser(id: UUID): Boolean =\n    newSuspendedTransaction {\n        UsersTable.deleteWhere { UsersTable.id eq id } > 0\n    }\n\n\u002F\u002F Row mapping\nprivate fun ResultRow.toUser() = UserRow(\n    id = this[UsersTable.id].value,\n    name = this[UsersTable.name],\n    email = this[UsersTable.email],\n    role = this[UsersTable.role],\n    metadata = this[UsersTable.metadata],\n    createdAt = this[UsersTable.createdAt],\n    updatedAt = this[UsersTable.updatedAt],\n)\n```\n\n### 高级查询\n\n```kotlin\n\u002F\u002F Join queries\nsuspend fun findOrdersWithUser(userId: UUID): List\u003COrderWithUser> =\n    newSuspendedTransaction {\n        (OrdersTable innerJoin UsersTable)\n            .selectAll()\n            .where { OrdersTable.userId eq userId }\n            .orderBy(OrdersTable.createdAt, SortOrder.DESC)\n            .map { row ->\n                OrderWithUser(\n                    orderId = row[OrdersTable.id].value,\n                    status = row[OrdersTable.status],\n                    totalAmount = row[OrdersTable.totalAmount],\n                    userName = row[UsersTable.name],\n                )\n            }\n    }\n\n\u002F\u002F Aggregation\nsuspend fun countUsersByRole(): Map\u003CRole, Long> =\n    newSuspendedTransaction {\n        UsersTable\n            .select(UsersTable.role, UsersTable.id.count())\n            .groupBy(UsersTable.role)\n            .associate { row ->\n                row[UsersTable.role] to row[UsersTable.id.count()]\n            }\n    }\n\n\u002F\u002F Subqueries\nsuspend fun findUsersWithOrders(): List\u003CUserRow> =\n    newSuspendedTransaction {\n        UsersTable.selectAll()\n            .where {\n                UsersTable.id inSubQuery\n                    OrdersTable.select(OrdersTable.userId).withDistinct()\n            }\n            .map { it.toUser() }\n    }\n\n\u002F\u002F LIKE and pattern matching — always escape user input to prevent wildcard injection\nprivate fun escapeLikePattern(input: String): String =\n    input.replace(\"\\\\\", \"\\\\\\\\\").replace(\"%\", \"\\\\%\").replace(\"_\", \"\\\\_\")\n\nsuspend fun searchUsers(query: String): List\u003CUserRow> =\n    newSuspendedTransaction {\n        val sanitized = escapeLikePattern(query.lowercase())\n        UsersTable.selectAll()\n            .where {\n                (UsersTable.name.lowerCase() like \"%${sanitized}%\") or\n                    (UsersTable.email.lowerCase() like \"%${sanitized}%\")\n            }\n            .map { it.toUser() }\n    }\n```\n\n### 分页\n\n```kotlin\ndata class Page\u003CT>(\n    val data: List\u003CT>,\n    val total: Long,\n    val page: Int,\n    val limit: Int,\n) {\n    val totalPages: Int get() = ((total + limit - 1) \u002F limit).toInt()\n    val hasNext: Boolean get() = page \u003C totalPages\n    val hasPrevious: Boolean get() = page > 1\n}\n\nsuspend fun findUsersPaginated(page: Int, limit: Int): Page\u003CUserRow> =\n    newSuspendedTransaction {\n        val total = UsersTable.selectAll().count()\n        val data = UsersTable.selectAll()\n            .orderBy(UsersTable.createdAt, SortOrder.DESC)\n            .limit(limit)\n            .offset(((page - 1) * limit).toLong())\n            .map { it.toUser() }\n\n        Page(data = data, total = total, page = page, limit = limit)\n    }\n```\n\n### 批量操作\n\n```kotlin\n\u002F\u002F Batch insert\nsuspend fun insertUsers(users: List\u003CCreateUserRequest>): List\u003CUUID> =\n    newSuspendedTransaction {\n        UsersTable.batchInsert(users) { user ->\n            this[UsersTable.name] = user.name\n            this[UsersTable.email] = user.email\n            this[UsersTable.role] = user.role\n        }.map { it[UsersTable.id].value }\n    }\n\n\u002F\u002F Upsert (insert or update on conflict)\nsuspend fun upsertUser(id: UUID, name: String, email: String) {\n    newSuspendedTransaction {\n        UsersTable.upsert(UsersTable.email) {\n            it[UsersTable.id] = EntityID(id, UsersTable)\n            it[UsersTable.name] = name\n            it[UsersTable.email] = email\n            it[updatedAt] = CurrentTimestampWithTimeZone\n        }\n    }\n}\n```\n\n## DAO 模式\n\n### 实体定义\n\n```kotlin\n\u002F\u002F entities\u002FUserEntity.kt\nclass UserEntity(id: EntityID\u003CUUID>) : UUIDEntity(id) {\n    companion object : UUIDEntityClass\u003CUserEntity>(UsersTable)\n\n    var name by UsersTable.name\n    var email by UsersTable.email\n    var role by UsersTable.role\n    var metadata by UsersTable.metadata\n    var createdAt by UsersTable.createdAt\n    var updatedAt by UsersTable.updatedAt\n\n    val orders by OrderEntity referrersOn OrdersTable.userId\n\n    fun toModel(): User = User(\n        id = id.value,\n        name = name,\n        email = email,\n        role = role,\n        metadata = metadata,\n        createdAt = createdAt,\n        updatedAt = updatedAt,\n    )\n}\n\nclass OrderEntity(id: EntityID\u003CUUID>) : UUIDEntity(id) {\n    companion object : UUIDEntityClass\u003COrderEntity>(OrdersTable)\n\n    var user by UserEntity referencedOn OrdersTable.userId\n    var status by OrdersTable.status\n    var totalAmount by OrdersTable.totalAmount\n    var currency by OrdersTable.currency\n    var createdAt by OrdersTable.createdAt\n\n    val items by OrderItemEntity referrersOn OrderItemsTable.orderId\n}\n```\n\n### DAO 操作\n\n```kotlin\nsuspend fun findUserByEmail(email: String): User? =\n    newSuspendedTransaction {\n        UserEntity.find { UsersTable.email eq email }\n            .firstOrNull()\n            ?.toModel()\n    }\n\nsuspend fun createUser(request: CreateUserRequest): User =\n    newSuspendedTransaction {\n        UserEntity.new {\n            name = request.name\n            email = request.email\n            role = request.role\n        }.toModel()\n    }\n\nsuspend fun updateUser(id: UUID, request: UpdateUserRequest): User? =\n    newSuspendedTransaction {\n        UserEntity.findById(id)?.apply {\n            request.name?.let { name = it }\n            request.email?.let { email = it }\n            updatedAt = OffsetDateTime.now(ZoneOffset.UTC)\n        }?.toModel()\n    }\n```\n\n## 事务\n\n### 挂起事务支持\n\n```kotlin\n\u002F\u002F Good: Use newSuspendedTransaction for coroutine support\nsuspend fun performDatabaseOperation(): Result\u003CUser> =\n    runCatching {\n        newSuspendedTransaction {\n            val user = UserEntity.new {\n                name = \"Alice\"\n                email = \"alice@example.com\"\n            }\n            \u002F\u002F All operations in this block are atomic\n            user.toModel()\n        }\n    }\n\n\u002F\u002F Good: Nested transactions with savepoints\nsuspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) {\n    newSuspendedTransaction {\n        val from = UserEntity.findById(fromId) ?: throw NotFoundException(\"User $fromId not found\")\n        val to = UserEntity.findById(toId) ?: throw NotFoundException(\"User $toId not found\")\n\n        \u002F\u002F Debit\n        from.balance -= amount\n        \u002F\u002F Credit\n        to.balance += amount\n\n        \u002F\u002F Both succeed or both fail\n    }\n}\n```\n\n### 事务隔离级别\n\n```kotlin\nsuspend fun readCommittedQuery(): List\u003CUser> =\n    newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {\n        UserEntity.all().map { it.toModel() }\n    }\n\nsuspend fun serializableOperation() {\n    newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) {\n        \u002F\u002F Strictest isolation level for critical operations\n    }\n}\n```\n\n## 仓储模式\n\n### 接口定义\n\n```kotlin\ninterface UserRepository {\n    suspend fun findById(id: UUID): User?\n    suspend fun findByEmail(email: String): User?\n    suspend fun findAll(page: Int, limit: Int): Page\u003CUser>\n    suspend fun search(query: String): List\u003CUser>\n    suspend fun create(request: CreateUserRequest): User\n    suspend fun update(id: UUID, request: UpdateUserRequest): User?\n    suspend fun delete(id: UUID): Boolean\n    suspend fun count(): Long\n}\n```\n\n### Exposed 实现\n\n```kotlin\nclass ExposedUserRepository(\n    private val database: Database,\n) : UserRepository {\n\n    override suspend fun findById(id: UUID): User? =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll()\n                .where { UsersTable.id eq id }\n                .map { it.toUser() }\n                .singleOrNull()\n        }\n\n    override suspend fun findByEmail(email: String): User? =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll()\n                .where { UsersTable.email eq email }\n                .map { it.toUser() }\n                .singleOrNull()\n        }\n\n    override suspend fun findAll(page: Int, limit: Int): Page\u003CUser> =\n        newSuspendedTransaction(db = database) {\n            val total = UsersTable.selectAll().count()\n            val data = UsersTable.selectAll()\n                .orderBy(UsersTable.createdAt, SortOrder.DESC)\n                .limit(limit)\n                .offset(((page - 1) * limit).toLong())\n                .map { it.toUser() }\n            Page(data = data, total = total, page = page, limit = limit)\n        }\n\n    override suspend fun search(query: String): List\u003CUser> =\n        newSuspendedTransaction(db = database) {\n            val sanitized = escapeLikePattern(query.lowercase())\n            UsersTable.selectAll()\n                .where {\n                    (UsersTable.name.lowerCase() like \"%${sanitized}%\") or\n                        (UsersTable.email.lowerCase() like \"%${sanitized}%\")\n                }\n                .orderBy(UsersTable.name)\n                .map { it.toUser() }\n        }\n\n    override suspend fun create(request: CreateUserRequest): User =\n        newSuspendedTransaction(db = database) {\n            UsersTable.insert {\n                it[name] = request.name\n                it[email] = request.email\n                it[role] = request.role\n            }.resultedValues!!.first().toUser()\n        }\n\n    override suspend fun update(id: UUID, request: UpdateUserRequest): User? =\n        newSuspendedTransaction(db = database) {\n            val updated = UsersTable.update({ UsersTable.id eq id }) {\n                request.name?.let { name -> it[UsersTable.name] = name }\n                request.email?.let { email -> it[UsersTable.email] = email }\n                it[updatedAt] = CurrentTimestampWithTimeZone\n            }\n            if (updated > 0) findById(id) else null\n        }\n\n    override suspend fun delete(id: UUID): Boolean =\n        newSuspendedTransaction(db = database) {\n            UsersTable.deleteWhere { UsersTable.id eq id } > 0\n        }\n\n    override suspend fun count(): Long =\n        newSuspendedTransaction(db = database) {\n            UsersTable.selectAll().count()\n        }\n\n    private fun ResultRow.toUser() = User(\n        id = this[UsersTable.id].value,\n        name = this[UsersTable.name],\n        email = this[UsersTable.email],\n        role = this[UsersTable.role],\n        metadata = this[UsersTable.metadata],\n        createdAt = this[UsersTable.createdAt],\n        updatedAt = this[UsersTable.updatedAt],\n    )\n}\n```\n\n## JSON 列\n\n### 使用 kotlinx.serialization 的 JSONB\n\n```kotlin\n\u002F\u002F Custom column type for JSONB\ninline fun \u003Creified T : Any> Table.jsonb(\n    name: String,\n    json: Json,\n): Column\u003CT> = registerColumn(name, object : ColumnType\u003CT>() {\n    override fun sqlType() = \"JSONB\"\n\n    override fun valueFromDB(value: Any): T = when (value) {\n        is String -> json.decodeFromString(value)\n        is PGobject -> {\n            val jsonString = value.value\n                ?: throw IllegalArgumentException(\"PGobject value is null for column '$name'\")\n            json.decodeFromString(jsonString)\n        }\n        else -> throw IllegalArgumentException(\"Unexpected value: $value\")\n    }\n\n    override fun notNullValueToDB(value: T): Any =\n        PGobject().apply {\n            type = \"jsonb\"\n            this.value = json.encodeToString(value)\n        }\n})\n\n\u002F\u002F Usage in table\n@Serializable\ndata class UserMetadata(\n    val preferences: Map\u003CString, String> = emptyMap(),\n    val tags: List\u003CString> = emptyList(),\n)\n\nobject UsersTable : UUIDTable(\"users\") {\n    val metadata = jsonb\u003CUserMetadata>(\"metadata\", Json.Default).nullable()\n}\n```\n\n## 使用 Exposed 进行测试\n\n### 用于测试的内存数据库\n\n```kotlin\nclass UserRepositoryTest : FunSpec({\n    lateinit var database: Database\n    lateinit var repository: UserRepository\n\n    beforeSpec {\n        database = Database.connect(\n            url = \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL\",\n            driver = \"org.h2.Driver\",\n        )\n        transaction(database) {\n            SchemaUtils.create(UsersTable)\n        }\n        repository = ExposedUserRepository(database)\n    }\n\n    beforeTest {\n        transaction(database) {\n            UsersTable.deleteAll()\n        }\n    }\n\n    test(\"create and find user\") {\n        val user = repository.create(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n\n        user.name shouldBe \"Alice\"\n        user.email shouldBe \"alice@example.com\"\n\n        val found = repository.findById(user.id)\n        found shouldBe user\n    }\n\n    test(\"findByEmail returns null for unknown email\") {\n        val result = repository.findByEmail(\"unknown@example.com\")\n        result.shouldBeNull()\n    }\n\n    test(\"pagination works correctly\") {\n        repeat(25) { i ->\n            repository.create(CreateUserRequest(\"User $i\", \"user$i@example.com\"))\n        }\n\n        val page1 = repository.findAll(page = 1, limit = 10)\n        page1.data shouldHaveSize 10\n        page1.total shouldBe 25\n        page1.hasNext shouldBe true\n\n        val page3 = repository.findAll(page = 3, limit = 10)\n        page3.data shouldHaveSize 5\n        page3.hasNext shouldBe false\n    }\n})\n```\n\n## Gradle 依赖项\n\n```kotlin\n\u002F\u002F build.gradle.kts\ndependencies {\n    \u002F\u002F Exposed\n    implementation(\"org.jetbrains.exposed:exposed-core:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-dao:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-jdbc:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0\")\n    implementation(\"org.jetbrains.exposed:exposed-json:1.0.0\")\n\n    \u002F\u002F Database driver\n    implementation(\"org.postgresql:postgresql:42.7.5\")\n\n    \u002F\u002F Connection pooling\n    implementation(\"com.zaxxer:HikariCP:6.2.1\")\n\n    \u002F\u002F Migrations\n    implementation(\"org.flywaydb:flyway-core:10.22.0\")\n    implementation(\"org.flywaydb:flyway-database-postgresql:10.22.0\")\n\n    \u002F\u002F Testing\n    testImplementation(\"com.h2database:h2:2.3.232\")\n}\n```\n\n## 快速参考：Exposed 模式\n\n| 模式 | 描述 |\n|---------|-------------|\n| `object Table : UUIDTable(\"name\")` | 定义具有 UUID 主键的表 |\n| `newSuspendedTransaction { }` | 协程安全的事务块 |\n| `Table.selectAll().where { }` | 带条件的查询 |\n| `Table.insertAndGetId { }` | 插入并返回生成的 ID |\n| `Table.update({ condition }) { }` | 更新匹配的行 |\n| `Table.deleteWhere { }` | 删除匹配的行 |\n| `Table.batchInsert(items) { }` | 高效的批量插入 |\n| `innerJoin` \u002F `leftJoin` | 连接表 |\n| `orderBy` \u002F `limit` \u002F `offset` | 排序和分页 |\n| `count()` \u002F `sum()` \u002F `avg()` | 聚合函数 |\n\n**记住**：对于简单查询使用 DSL 风格，当需要实体生命周期管理时使用 DAO 风格。始终使用 `newSuspendedTransaction` 以获得协程支持，并将数据库操作包装在仓储接口之后以提高可测试性。\n",{"name":5,"origin":24,"description":7},"ECC","main","87afe6ce96220accf50a345dea46784c8c9f210e","YYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fkotlin-exposed-patterns",0,"https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base","https:\u002F\u002Fkb.yyc3.top\u002F","https:\u002F\u002Fgithub.com\u002FYYC-Cube","万象归元于云枢 | 深栈智启新纪元","2026-11-10 10:37:32",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill kotlin-exposed-patterns",{"tool":40,"command":41},"bunx","bunx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill kotlin-exposed-patterns",{"tool":43,"command":44},"pnpm","pnpm dlx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill kotlin-exposed-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fzh-CN\u002Fskills\u002Fkotlin-exposed-patterns\u002FSKILL.md",true,[],[49,56,63,70,77,84],{"skillNo":50,"name":51,"nameZh":6,"description":52,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":53,"categories":54,"keywords":55,"publishedAt":19},"01M7CTDWHERCXPQCP0K03A86AS","vercel-sandbox","Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include \"Vercel Sandbox browser\", \"microVM Chrome\", \"agent-browser in sandbox\", \"browser automation on Vercel\", or any task requiring Chrome in a Vercel Sandbox.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fvercel-sandbox\u002FSKILL.md",[],[],{"skillNo":57,"name":58,"nameZh":6,"description":59,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":60,"categories":61,"keywords":62,"publishedAt":19},"01M7CTD8D2BNJY54S65M47HEJA","slack","Interact with Slack workspaces using browser automation. Use when the user needs to check unread channels, navigate Slack, send messages, extract data, find information, search conversations, or automate any Slack task. Triggers include \"check my Slack\", \"what channels have unreads\", \"send a message to\", \"search Slack for\", \"extract from Slack\", \"find who said\", or any task requiring programmatic Slack interaction.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fslack\u002FSKILL.md",[],[],{"skillNo":64,"name":65,"nameZh":6,"description":66,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":67,"categories":68,"keywords":69,"publishedAt":19},"01M7CTCSSB8ZP3Q58C3YZDZK1W","electron","Automate Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) using agent-browser via Chrome DevTools Protocol. Use when the user needs to interact with an Electron app, automate a desktop app, connect to a running app, control a native app, or test an Electron application. Triggers include \"automate Slack app\", \"control VS Code\", \"interact with Discord app\", \"test this Electron app\", \"connect to desktop app\", or any task requiring automation of a native Electron application.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Felectron\u002FSKILL.md",[],[],{"skillNo":71,"name":72,"nameZh":6,"description":73,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":74,"categories":75,"keywords":76,"publishedAt":19},"01M7CTCBJY76688V4DCZGWFHGQ","dogfood","Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to \"dogfood\", \"QA\", \"exploratory test\", \"find issues\", \"bug hunt\", \"test this app\u002Fsite\u002Fplatform\", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fdogfood\u002FSKILL.md",[],[],{"skillNo":78,"name":79,"nameZh":6,"description":80,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":81,"categories":82,"keywords":83,"publishedAt":19},"01M7CTC4H0FFGVEAQR9PA4GA1A","agent-browser","Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to \"open a website\", \"fill out a form\", \"click a button\", \"take a screenshot\", \"scrape data from a page\", \"test this web app\", \"login to a site\", \"automate browser actions\", or any task requiring programmatic web interaction.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fagent-browser\u002FSKILL.md",[],[],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":19},"01M7CTBKVFYBG78FDSABSDJTTQ","shadcn","Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. Provides project context, component docs, and usage examples. Applies when working with shadcn\u002Fui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for \"shadcn init\", \"create an app with --preset\", \"switch to --preset\", and for building a design system or applying a DESIGN.md or brand spec to shadcn\u002Fui.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-UI-界面组件\u002FYYC3-UI-界面组件-ui\u002Fskills\u002Fshadcn\u002FSKILL.md",[],[]]