6、数据库集成(Exposed):Exposed ORM介绍、表定义、CRUD操作、事务管理、连接池配置

好,咱们进入数据库这一块。说实话,后端开发里数据库操作是绕不开的硬骨头。Ktor 本身不绑死某个 ORM,但 JetBrains 自家出的 Exposed,我用下来觉得跟 Kotlin 的契合度最高。今天我就带你把它捋一遍。

核心知识点一览

Exposed ORM 本质是一个轻量级的 Kotlin SQL 框架,不是全量级 ORM(比如 Hibernate)。它分两种风格:DSL 方式和 DAO 方式。我个人更推荐 DSL,因为写起来更直观,调试也方便。

Exposed ORM 表定义 Table 对象 + 列类型 CRUD 操作 insert / select / update / delete 事务管理 transaction { ... } 连接池配置(HikariCP) Database.connect() + HikariCP 参数 Exposed 数据库集成核心模块

6.1 Exposed ORM 是什么?

Exposed 是 JetBrains 官方出品的 Kotlin SQL 框架。它不像 Hibernate 那样有完整的 JPA 实现,也不像 MyBatis 那样需要手写 XML。说白了,它就是在 Kotlin 的语法糖上直接映射 SQL 操作。

我记得第一次用 Exposed 的时候,最让我舒服的一点是——它不需要额外的代码生成。你定义个 Table 对象,然后就能直接写查询了。对于 Ktor 这种轻量级框架来说,简直是绝配。

我的建议:如果你团队里都是 Kotlin 开发者,优先选 Exposed。如果团队里有 Java 背景的人,可能会觉得 DAO 方式更亲切。我个人一直用 DSL 方式,因为调试 SQL 时能看到完整的语句。

6.2 表定义

先看一个最简单的表定义。假设我们要做一个用户表:

// 定义表结构
object UsersTable : Table("users") {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val email = varchar("email", 100).uniqueIndex()
    val age = integer("age").nullable()
    val createdAt = datetime("created_at")
    
    override val primaryKey = PrimaryKey(id)
}

这里有几个点要注意:

  • Table("users") —— 指定数据库里的表名,不写的话默认用类名
  • .autoIncrement() —— 自增主键,我习惯每个表都加一个
  • .uniqueIndex() —— 唯一索引,email 这种字段必须加
  • .nullable() —— 允许为空,默认是非空

嗯,这里要特别说一下。我在项目中遇到过有人把表名写成小写,但数据库里是大写,结果查不到数据。Exposed 默认会做大小写转换,但不同数据库行为不一样。建议统一用小写。

6.3 CRUD 操作

CRUD 是基本功。咱们直接上代码,边看边讲。

插入数据

// 插入单条
UsersTable.insert {
    it[name] = "张三"
    it[email] = "zhangsan@example.com"
    it[age] = 28
    it[createdAt] = Instant.now()
}

// 批量插入
UsersTable.batchInsert(listOf(
    rowOf(name to "李四", email to "lisi@example.com"),
    rowOf(name to "王五", email to "wangwu@example.com")
))

你想想看,用 Exposed 插入数据是不是比写原生 SQL 舒服多了?类型安全,IDE 还能自动补全。

查询数据

// 查询所有用户
UsersTable.selectAll().forEach { row ->
    println("${row[UsersTable.name]} - ${row[UsersTable.email]}")
}

// 条件查询
UsersTable.select { UsersTable.age greaterEq 18 }
    .orderBy(UsersTable.createdAt to SortOrder.DESC)
    .limit(10)
    .forEach { row ->
        // 处理结果
    }

// 查询单条
val user = UsersTable.select { UsersTable.id eq 1 }.singleOrNull()

这里有个坑。我曾经用 .single() 去查,结果数据库里有多条匹配,直接抛异常了。后来我改成 .singleOrNull(),安全多了。

更新和删除

// 更新
UsersTable.update({ UsersTable.id eq 1 }) {
    it[name] = "张三(已修改)"
    it[age] = 29
}

// 删除
UsersTable.deleteWhere { UsersTable.id eq 1 }

注意:deleteWhere 如果不加条件,会清空整张表!我刚开始用的时候手滑过一次,还好是测试环境。生产环境一定要加条件检查。

6.4 事务管理

事务是数据库操作的基石。Exposed 的事务管理非常简洁:

transaction {
    // 事务内的所有操作
    UsersTable.insert { ... }
    OrdersTable.insert { ... }
    
    // 如果中间抛出异常,自动回滚
    // 如果正常执行完,自动提交
}

为什么说这个设计好?因为 Kotlin 的 lambda 天然支持作用域控制。你不需要手动 commit 或 rollback,异常时自动回滚。

我记得有一次线上问题,用户下单时同时更新了用户余额和订单表。如果不用事务,余额扣了但订单没生成,那就出大事了。用 transaction 包裹后,要么全部成功,要么全部失败。

进阶用法:事务可以嵌套,但默认是共用同一个连接。如果你需要独立事务,可以用 Transaction(connection).apply { ... } 手动控制。

6.5 连接池配置

连接池这块,Exposed 官方推荐用 HikariCP。说实话,HikariCP 也是目前 Java/Kotlin 生态里性能最好的连接池,没有之一。

配置方式如下:

// 在 Application.kt 或模块初始化时
Database.connect(
    url = "jdbc:postgresql://localhost:5432/mydb",
    driver = "org.postgresql.Driver",
    user = "dbuser",
    password = "dbpass",
    databaseConfig = DatabaseConfig {
        // 连接池配置
        maxPoolSize = 10
        minPoolSize = 2
        connectionTimeout = 30_000  // 30秒
        idleTimeout = 600_000       // 10分钟
        maxLifetime = 1_800_000     // 30分钟
    }
)

参数说明:

参数 说明 推荐值
maxPoolSize 最大连接数 10~20(根据并发量调整)
minPoolSize 最小空闲连接 2~5
connectionTimeout 获取连接的超时时间 30秒
idleTimeout 连接空闲多久后回收 10分钟
maxLifetime 连接最大存活时间 30分钟

我曾经在生产环境踩过一个坑:连接池的 maxLifetime 设得太长(2小时),结果数据库那边主动断开后,连接池里的连接变成「死连接」,应用层完全不知道。后来我把 maxLifetime 设为 30 分钟,并且加上了连接验证:

// 连接验证
connectionTestQuery = "SELECT 1"
validationTimeout = 5_000  // 5秒

这样每次从连接池拿连接时,都会先验证一下是否可用。虽然有一点点性能开销,但比起连接失效导致的报错,这点代价完全值得。

总结一下:Exposed + HikariCP 是 Ktor 后端最稳的数据库组合。表定义用 DSL 方式,CRUD 操作注意 null 安全和唯一约束,事务用 transaction 包裹,连接池参数根据实际并发量调优。做到这几点,数据库这块基本不会出大问题。


公众号:蓝海资料掘金营,微信 deep3321