在 Kotlin 后端开发中,Ktor 依靠协程与轻量级路由组织代码,PostgreSQL 则承担持久化与复杂查询。要把二者可靠地接在一起,核心不是写一条 JDBC URL,而是把数据源、连接池、事务和路由生命周期统一管理。本文围绕 HikariCP 与 Exposed 的组合给出完整做法。

一、引入依赖并读取数据库配置
如果项目还没有 Gradle 配置,先在 build.gradle.kts 中加入 Ktor 服务端、PostgreSQL 驱动、HikariCP 连接池以及 Exposed 相关依赖。不要直接在业务代码中使用 DriverManager.getConnection,那样每次请求都建立物理连接,延迟高且连接数很快耗尽。
Ktor 本身不限定数据库访问方式,但 Exposed 与 HikariCP 是 Kotlin 生态中配合较顺的组合。Exposed 提供的 DSL 可以在不写原生 SQL 的情况下完成建表与查询,HikariCP 则负责维护可复用的连接资源。
dependencies {
implementation("io.ktor:ktor-server-core:2.3.12")
implementation("io.ktor:ktor-server-netty:2.3.12")
implementation("org.jetbrains.exposed:exposed-core:0.53.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.53.0")
implementation("org.postgresql:postgresql:42.7.4")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("ch.qos.logback:logback-classic:1.5.6")
}
数据库连接参数建议放入 application.conf,避免硬编码。Ktor 的 ApplicationConfig 可以读取 HOCON 配置,再用 HikariConfig 构造连接池。下面示例使用环境变量读取敏感信息,本地开发可以写默认值。
database {
jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/ktor_demo"
jdbcUrl = ${?DATABASE_URL}
user = "postgres"
user = ${?DATABASE_USER}
password = "postgres"
password = ${?DATABASE_PASSWORD}
maximumPoolSize = 10
}
二、创建 DataSource 与 Exposed Database
在应用启动阶段初始化一次连接池,并把它交给 Exposed。HikariCP 的 DataSource 是线程安全的,Exposed 的 Database.connect 可以接收 DataSource 或连接池配置。不要在每次请求中重新创建,否则连接池失去复用意义。
新建一个 DatabaseFactory 对象,把初始化逻辑集中起来。密码、用户名和连接池参数从 Application 的 environment.config 读取。如果 PostgreSQL 部署在云上,还需要在 jdbcUrl 中追加 sslmode=require 等参数。
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.config.*
import org.jetbrains.exposed.sql.Database
object DatabaseFactory {
fun init(config: ApplicationConfig) {
val hikariConfig = HikariConfig().apply {
jdbcUrl = config.property("database.jdbcUrl").getString()
username = config.property("database.user").getString()
password = config.property("database.password").getString()
maximumPoolSize = config.property("database.maximumPoolSize").getString().toInt()
minimumIdle = 2
connectionTimeout = 30_000
validate()
}
val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource)
}
}
以上代码只做一次 Database.connect。Exposed 会保存全局默认数据库,后续 transaction 块直接使用。若应用要连接多个数据库,可以在 connect 时传入多个 Database 实例,并在事务块中显式指定。
为了验证表结构与数据,先定义 Exposed 的表对象。这里不执行原生 CREATE TABLE,而是通过 SchemaUtils.create 在初始化时建表。表字段与 PostgreSQL 类型会由 Exposed 自动映射。
import org.jetbrains.exposed.sql.Table
object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
val createdAt = long("created_at")
override val primaryKey = PrimaryKey(id)
}
这段定义中的 uniqueIndex 会在 PostgreSQL 中创建唯一索引。建模时尽量明确字段长度与约束,既能减少写入异常,也便于索引设计和查询优化。
三、在路由中执行查询与写入
Ktor 的协程环境与 Exposed 的阻塞 JDBC 事务需要做好线程切换。Exposed 内置 newSuspendedTransaction 函数,可以指定 Dispatchers.IO 执行事务,避免阻塞 Netty 的事件循环线程。查询结果需要及时转换成 DTO 返回给客户端。
下面的路由示例包含新增用户和按邮箱查询两个接口。写操作在事务内执行 insert,查询使用 select 配合条件过滤。注意事务块中的代码必须全部走完才不会提交,出现异常会自动回滚。
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
data class UserDto(val id: Int, val name: String, val email: String)
fun Route.userRoutes() {
post("/users") {
val body = call.receive<Map<String, String>>()
val name = body["name"] ?: return@post call.respondText("name is required", status = HttpStatusCode.BadRequest)
val email = body["email"] ?: return@post call.respondText("email is required", status = HttpStatusCode.BadRequest)
val createdId = newSuspendedTransaction(Dispatchers.IO) {
Users.insert {
it[Users.name] = name
it[Users.email] = email
it[createdAt] = System.currentTimeMillis()
} get Users.id
}
call.respondText("""{"id":$createdId}""", contentType = ContentType.Application.Json)
}
get("/users/{email}") {
val email = call.parameters["email"].orEmpty()
val user = newSuspendedTransaction(Dispatchers.IO) {
Users.selectAll()
.where { Users.email eq email }
.map { UserDto(it[Users.id], it[Users.name], it[Users.email]) }
.singleOrNull()
}
if (user == null) {
call.respondText("user not found", status = HttpStatusCode.NotFound)
} else {
call.respondText(
"""{"id":${user.id},"name":"${user.name}","email":"${user.email}"}""",
contentType = ContentType.Application.Json
)
}
}
}
最后在 Application.module 中初始化数据库并注册路由,Netty 启动后即可访问接口。SchemaUtils.create 会依据表对象自动建表,适合开发和测试环境,生产环境建议使用迁移工具管理表结构。
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import org.jetbrains.exposed.sql.SchemaUtils
fun main() {
embeddedServer(Netty, port = 8080) {
module()
}.start(wait = true)
}
fun Application.module() {
DatabaseFactory.init(environment.config)
SchemaUtils.create(Users)
routing {
userRoutes()
}
}
四、连接池调优与常见故障排查
HikariCP 的默认参数适合轻量应用,但 PostgreSQL 在高并发下需要调整。maximumPoolSize 不要一开始就设得很大,连接数过多会加重数据库端锁竞争。通常从 CPU 核数乘以 2 开始压测,再根据慢查询与等待时间逐步增加。minimumIdle 保持较小值即可,避免空闲连接占用过多 PG 连接配额。
另一个容易忽略的参数是 connectionTimeout。当连接池耗尽时,请求会在等待队列中排队,超过 connectionTimeout 会抛出超时异常。建议设置 30 秒,并配合监控日志定位是数据库响应慢还是池配置过小。HikariCP 还支持 leakDetectionThreshold,开启后能发现未及时归还的连接。
import com.zaxxer.hikari.HikariConfig
fun buildHikariConfig(): HikariConfig {
return HikariConfig().apply {
jdbcUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://127.0.0.1:5432/ktor_demo"
username = System.getenv("DATABASE_USER") ?: "postgres"
password = System.getenv("DATABASE_PASSWORD") ?: "postgres"
driverClassName = "org.postgresql.Driver"
maximumPoolSize = 10
minimumIdle = 2
connectionTimeout = 30_000
validationTimeout = 5_000
idleTimeout = 600_000
leakDetectionThreshold = 60_000
addDataSourceProperty("cachePrepStmts", "true")
addDataSourceProperty("prepStmtCacheSize", "250")
addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
}
}
连接不上 PostgreSQL 时,先确认驱动版本与 jdbcUrl 参数。云数据库常要求 SSL,可写 jdbc:postgresql://host:5432/db?sslmode=require。遇到身份验证失败,优先检查 pg_hba.conf 是否允许当前 IP 或网段,再检查用户名和数据库名是否精确匹配。
Exposed 默认会输出 SQL 日志,开发期可开启,排查慢查询时结合 PostgreSQL 的 pg_stat_statements 视图分析。不要在生产环境打印所有 SQL 明细,既有性能开销,也可能泄露用户数据。通过日志级别控制 Exposed SQL 输出即可。
Kotlin KtorPostgreSQL数据库连接修改时间:2026-09-19 21:40:53