前置知识: Kotlin

Kotlin与Exposed

00:00
3 min Intermediate 2026/6/14

Kotlin SQL框架Exposed

概述

Exposed 是 JetBrains 开发的 Kotlin SQL 框架,提供两种风格来操作数据库:DSL(领域特定语言)和 DAO(数据访问对象)。DSL 风格让你用 Kotlin 代码写 SQL 查询,类型安全且直观;DAO 风格则提供了类似 ORM 的对象映射。与 Hibernate 等重量级 ORM 不同,Exposed 轻量、透明,让你对 SQL 有完全的控制。

如果你需要在 Kotlin 项目操作关系型数据库,Exposed 是一个 Kotlin 风的轻量选择

基础概念

  • Table表示数据表,用 Kotlin 对象定义结构
  • Column表示表中的列,支持类型安全操作
  • Transaction:所有数据库操作必须在事务执行
  • DSL:用 Kotlin 函数直接写查询,如 select { }insert { }
  • DAO 风:用 Entity 映射通过 Entity 操作数
  • SchemaUtils:用于自动创建和更新结构

快速上手

添加依赖

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:0.49.0")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.49.0")
    // 数据库驱动
    implementation("com.h2database:h2:2.2.224")      // H2 内存数据库
    implementation("org.postgresql:postgresql:42.7.1")  // PostgreSQL
    implementation("com.mysql:mysql-connector-j:8.3.0") // MySQL
    // 连接池(推荐)
    implementation("com.zaxxer:HikariCP:5.1.0")
}

简单的使用:

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

// 定义表结构
object Users : Table() {
    val id = integer("id").autoIncrement()   // 自增主键
    val name = varchar("name", 50)            // varchar(50)
    val age = integer("age")                  // integer
    override val primaryKey = PrimaryKey(id)  // 指定主键
}

fun main() {
    // 连接数据库
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

    // 在事务中操作
    transaction {
        // 创建表
        SchemaUtils.create(Users)

        // 插入数据
        Users.insert {
            it[name] = "Alice"
            it[age] = 25
        }
        Users.insert {
            it[name] = "Bob"
            it[age] = 30
        }

        // 查询所有数据
        Users.selectAll().forEach { row ->
            println("${row[Users.id]}: ${row[Users.name]}, ${row[Users.age]}")
        }
    }
}

详细用法

使用连接池

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database

fun initDatabase(): Database {
    val config = HikariConfig().apply {
        jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
        driverClassName = "org.postgresql.Driver"
        username = "postgres"
        password = "secret"
        maximumPoolSize = 10
        connectionTimeout = 30000
    }
    val dataSource = HikariDataSource(config)
    return Database.connect(dataSource)
}

查询操作

import org.jetbrains.exposed.sql.*

fun queryDemo() = transaction {
    // 查询所有
    val allUsers = Users.selectAll().toList()

    // 条件查询
    val adults = Users.select { Users.age greater 18 }
        .map { it[Users.name] }

    // 多条件查询
    val result = Users.select {
        (Users.age greater 20) and (Users.name like "A%")
    }

    // 排序
    val sorted = Users.selectAll()
        .orderBy(Users.age to SortOrder.DESC)
        .orderBy(Users.name to SortOrder.ASC)

    // 限制结果数量
    val top10 = Users.selectAll()
        .limit(10)
        .offset(20)  // 分页:跳过前20条

    // 聚合函数
    val avgAge = Users.age.avg()
    val count = Users.id.count()
    val maxAge = Users.age.max()
    Users.select(avgAge, count, maxAge).single().let { row ->
        println("平均年龄: ${row[avgAge]}")
        println("总人数: ${row[count]}")
        println("最大年龄: ${row[maxAge]}")
    }

    // 分组
    Users.select(Users.age, Users.id.count())
        .groupBy(Users.age)
        .forEach { row ->
            println("年龄 ${row[Users.age]}: ${row[Users.id.count()]} 人")
        }

    // 去重
    val distinctAges = Users.select(Users.age).withDistinct()
}

插入操作

import org.jetbrains.exposed.sql.*

fun insertDemo() = transaction {
    // 插入单条
    Users.insert {
        it[name] = "Alice"
        it[age] = 25
    }

    // 插入并获取自增ID
    val id = Users.insert {
        it[name] = "Bob"
        it[age] = 30
    } get Users.id

    // 批量插入
    Users.batchInsert(listOf("Charlie", "David", "Eve")) { userName ->
        this[Users.name] = userName
        this[Users.age] = 20
    }

    // 插入或忽略(如果主键冲突则跳过)
    Users.insertIgnore {
        it[Users.id] = 1
        it[name] = "Alice"
        it[age] = 25
    }

    // 插入或更新(如果主键冲突则更新)
    Users.insertOrUpdate {
        it[Users.id] = 1
        it[name] = "Alice"
        it[age] = 26  // 如果ID=1已存在,更新年龄
    }
}

更新和删除

import org.jetbrains.exposed.sql.*

fun updateDeleteDemo() = transaction {
    // 更新
    Users.update({ Users.name eq "Alice" }) {
        it[age] = 26
    }

    // 条件更新
    Users.update({ Users.age less 18 }) {
        it[age] = 18
    }

    // 删除
    Users.deleteWhere { Users.name eq "Bob" }

    // 删除所有
    Users.deleteAll()
}

多表关联

import org.jetbrains.exposed.sql.*

// 定义订单表
object Orders : Table() {
    val id = integer("id").autoIncrement()
    val userId = integer("user_id").references(Users.id)  // 外键
    val amount = decimal("amount", 10, 2)
    val createdAt = varchar("created_at", 30)
    override val primaryKey = PrimaryKey(id)
}

fun joinDemo() = transaction {
    // 内连接
    (Users innerJoin Orders)
        .select(Users.name, Orders.amount)
        .where { Users.age greater 20 }
        .forEach { row ->
            println("${row[Users.name]}: ${row[Orders.amount]}")
        }

    // 左连接
    (Users leftJoin Orders)
        .select(Users.name, Orders.amount)
        .forEach { row ->
            val amount = row.getOrNull(Orders.amount) ?: "无订单"
            println("${row[Users.name]}: $amount")
        }

    // 别名(自连接等场景)
    val userAlias = Users.alias("u")
    val orderAlias = Orders.alias("o")
}

DAO 风格

import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable

// 定义表
object Users : IntIdTable() {
    val name = varchar("name", 50)
    val age = integer("age")
}

// 定义 Entity
class User(id: EntityID<Int>) : IntEntity(id) {
    var name by Users.name
    var age by Users.age
}

// 定义 EntityClass
object UsersDao : IntEntityClass<User>(Users)

fun daoDemo() = transaction {
    SchemaUtils.create(Users)

    // 创建
    val user = User.new {
        name = "Alice"
        age = 25
    }

    // 读取
    val found = User[user.id]
    println("${found.name}, ${found.age}")

    // 更新
    found.age = 26

    // 删除
    found.delete()

    // 查询
    val adults = User.find { Users.age greater 18 }
    adults.forEach { println(it.name) }

    // 查询所有
    val all = User.all()
}

常见场景

与 Ktor 集成

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

fun Application.configureDatabase() {
    val db = initDatabase()

    routing {
        // 获取用户列表
        get("/users") {
            val users = transaction {
                Users.selectAll().map { row ->
                    mapOf(
                        "id" to row[Users.id],
                        "name" to row[Users.name],
                        "age" to row[Users.age]
                    )
                }
            }
            call.respond(users)
        }

        // 获取单个用户
        get("/users/{id}") {
            val id = call.parameters["id"]?.toInt() ?: return@get call.respondText("无效ID")
            val user = transaction {
                Users.select { Users.id eq id }.singleOrNull()?.let { row ->
                    mapOf("id" to row[Users.id], "name" to row[Users.name])
                }
            }
            user?.let { call.respond(it) } ?: call.respondText("用户不存在")
        }
    }
}

注意事项

  • 所有操作必须在事务:忘记 transaction { } 会导致运行时错误
  • Exposed 不是 ORM:它不会自动管理关联关系懒加载等,你需要自己写查询
  • 连接管理生产环境务必使用连接池(如 HikariCP),不要每次创建新连接
  • SQL 注入防护:Exposed 的参数化查询自动防止 SQL 注入,不要用字符串拼接
  • 事务隔离级别:默认使用数据库隔离级别,可以在 transaction 中指定

进阶用法

自定义列类型

import org.jetbrains.exposed.sql.ColumnType
import org.jetbrains.exposed.sql.Table

// 自定义枚举列类型
inline fun <reified T : Enum<T>> Table.enum(name: String): Column<T> =
    registerColumn(name, object : ColumnType<T>() {
        override fun sqlType(): String = "VARCHAR(50)"
        override fun valueFromDB(value: Any): T =
            java.lang.Enum.valueOf(T::class.java, value.toString())
        override fun valueToDB(value: T?): Any? = value?.name
    })

object StatusTable : Table() {
    val id = integer("id")
    val status = enum<Status>("status")
}

enum class Status { ACTIVE, INACTIVE, PENDING }

事务嵌套

import org.jetbrains.exposed.sql.transactions.transaction

fun nestedTransactionDemo() = transaction {
    // 外层事务
    Users.insert { it[name] = "Outer"; it[age] = 1 }

    // 嵌套事务(savepoint)
    transaction {
        Users.insert { it[name] = "Inner"; it[age] = 2 }
        // 如果这里抛出异常,只会回滚到 savepoint
    }
}

数据库迁移

import org.jetbrains.exposed.sql.SchemaUtils

fun migrateDemo() = transaction {
    // 自动创建不存在的表
    SchemaUtils.create(Users, Orders)

    // 检查表是否存在
    val exists = SchemaUtils.tableExists(Users)

    // 添加新列(需要手动执行 ALTER TABLE)
    exec("ALTER TABLE users ADD COLUMN email VARCHAR(100)")
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式