Kotlin Exposed是JetBrains开源的SQL DSL框架,它提供了一套类型安全的API,让我们用Kotlin代码就能表达复杂的SQL查询逻辑。相比手写SQL字符串,Exposed在编译期就能发现字段名拼写错误、类型不匹配等问题,配合PostgreSQL强大的功能特性,可以构建出既灵活又可靠的数据访问层。本文将从环境搭建开始,逐步深入讲解Exposed DSL的各种查询技巧。

一、环境搭建与数据库连接
使用Exposed操作PostgreSQL,首先需要在build.gradle中引入相关依赖。Exposed采用了模块化设计,核心模块只包含DSL基础能力,JDBC驱动需要单独引入:
dependencies {
implementation("org.jetbrains.exposed:exposed-core:0.44.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.44.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.44.1")
implementation("org.postgresql:postgresql:42.6.0")
}
依赖引入后,通过Database.connect建立与PostgreSQL的连接。连接字符串遵循标准JDBC格式,建议将敏感信息放在配置文件或环境变量中,避免硬编码在代码里:
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction
fun initDatabase() {
Database.connect(
url = "jdbc:postgresql://127.0.0.1:5432/orderdb",
driver = "org.postgresql.Driver",
user = "postgres",
password = System.getenv("DB_PASSWORD") ?: "postgres"
)
}
需要注意的是,Exposed的所有数据库操作都必须在transaction块内执行,否则会抛出异常。这是框架的设计约束,目的是保证事务边界的清晰性。生产环境推荐配合HikariCP连接池使用,只需把connect的dataSource参数换成连接池实例即可,能显著提升高并发场景下的性能表现。
二、表定义与基础DSL查询
DSL查询的前提是先定义表结构。Exposed通过继承Table类来声明表,每个属性对应一个列,代码如下:
import org.jetbrains.exposed.sql.Table
object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 64)
val email = varchar("email", 128)
val createdAt = timestamp("created_at")
override val primaryKey = PrimaryKey(id)
}
object Orders : Table("orders") {
val id = integer("id").autoIncrement()
val userId = integer("user_id") references Users.id
val amount = decimal("amount", 12, 2)
val status = varchar("status", 16)
override val primaryKey = PrimaryKey(id)
}
定义好表之后,就可以使用select进行查询。最简单的形式是查询全部记录:
transaction {
Users.selectAll().forEach { row ->
println("${row[Users.name]} - ${row[Users.email]}")
}
}
带条件过滤时使用select { ... },条件表达式完全类型安全,写错字段类型会直接编译报错。常见的条件操作符包括eq、neq、greater、less、like、inList等,可以通过and、or自由组合:
transaction {
// 查询名字包含"张"且邮箱以@qq.com结尾的用户
Users.select {
(Users.name like "%张%") and (Users.email like "%@qq.com")
}.forEach { row ->
println(row[Users.name])
}
// 使用inList查询多个状态
Orders.select { Orders.status inList listOf("PAID", "SHIPPED") }
}
查询结果的每一行是一个ResultRow,通过列引用取值时会自动做类型转换。如果只需要查询单个值,可以用slice限定查询列,减少数据传输量,这在查询大表时是一个不可忽视的优化细节。
三、多表关联与聚合查询
实际业务中多表关联查询非常常见。Exposed支持innerJoin、leftJoin、rightJoin等多种连接方式,写法相当直观:
transaction {
(Users innerJoin Orders)
.select { Orders.status eq "PAID" }
.forEach { row ->
println("用户${row[Users.name]} 订单金额${row[Orders.amount]}")
}
}
由于表定义中已经声明了userId references Users.id的外键关系,innerJoin时框架会自动推断关联条件,不需要手写on子句。如果关联条件比较特殊,也可以显式指定:Users.join(Orders, JoinType.INNER, additionalConstraint = { Users.id eq Orders.userId })。
聚合查询方面,Exposed提供了count、sum、avg、max、min等函数,配合groupBy和having可以实现分组统计。下面是一个统计每个用户已支付订单总金额的例子:
transaction {
val totalAmount = Orders.amount.sum()
(Users innerJoin Orders)
.select { Orders.status eq "PAID" }
.groupBy(Users.id, Users.name)
.having { totalAmount greater BigDecimal("100") }
.forEach { row ->
println("${row[Users.name]} 累计消费: ${row[totalAmount]}")
}
}
这里有个容易踩的坑:PostgreSQL对groupBy的要求很严格,select中出现的非聚合列必须全部出现在groupBy里,否则会报错。Exposed不会替你自动补全分组列,因此写聚合查询时要格外留意列的完整性。分页查询则通过limit和offset实现,排序用orderBy:
transaction {
Orders.selectAll()
.orderBy(Orders.id to SortOrder.DESC)
.limit(20, offset = 40) // 第3页,每页20条
.toList()
}
四、进阶用法与方案选型建议
除了常规查询,Exposed还支持子查询。可以把一个查询结果作为表达式嵌入另一个查询中,例如查询下单金额高于全局平均值的订单:
transaction {
val avgAmount = Orders.amount.avg()
val avgExpr = Orders.slice(avgAmount).selectAll().single()[avgAmount]
Orders.select { Orders.amount greater avgExpr }.toList()
}
当遇到特别复杂的SQL时,也不必强行用DSL表达,Exposed允许通过exec或with执行原生SQL,保留了这个逃生通道。另外要说明的是,Exposed提供了两套API:本文讲的DSL是面向SQL的低层API,而TableDAO和Entity(实体类)则是更高层的封装。DSL更接近SQL思维,控制力强、性能开销小;DAO适合 CRUD为主的简单业务,开发效率更高。团队技术选型时,如果成员对SQL比较熟悉、查询场景复杂多变,推荐DSL优先;如果追求快速开发且模型简单,DAO会更省心。
最后提醒几个实践要点:一是PostgreSQL的JSONB字段可以借助jsonb列类型和自定义表达式来操作,处理半结构化数据非常方便;二是大数据量查询时善用slice只取需要的列;三是记得为高频查询条件建立索引,DSL再优雅也弥补不了缺失索引带来的性能损失。掌握这些内容后,用Exposed操作PostgreSQL基本就游刃有余了。
PostgreSQLKotlin ExposedDSL查询修改时间:2026-09-16 07:10:36