6、数据库集成(Exposed):Exposed ORM介绍、表定义、CRUD操作、事务管理、连接池配置
好,咱们进入数据库这一块。说实话,后端开发里数据库操作是绕不开的硬骨头。Ktor 本身不绑死某个 ORM,但 JetBrains 自家出的 Exposed,我用下来觉得跟 Kotlin 的契合度最高。今天我就带你把它捋一遍。
核心知识点一览
Exposed ORM 本质是一个轻量级的 Kotlin SQL 框架,不是全量级 ORM(比如 Hibernate)。它分两种风格:DSL 方式和 DAO 方式。我个人更推荐 DSL,因为写起来更直观,调试也方便。
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 包裹,连接池参数根据实际并发量调优。做到这几点,数据库这块基本不会出大问题。