第78章:Exposed框架:表定义、CRUD操作、事务管理、DSL查询
说实话,Kotlin 做后端开发,绕不开数据库操作。JPA 太重,JDBC 太原始。我个人这几年用得最顺手的,就是 JetBrains 自家的 Exposed 框架。它不像 ORM 那样黑魔法,也不像手写 SQL 那么累。说白了,它就是 Kotlin 风格的 SQL 包装器。
这一章,我带你从零搭建 Exposed 的数据库操作层。表怎么定义、数据怎么增删改查、事务怎么管、DSL 怎么写——一条龙讲清楚。
1. 表定义:用 Kotlin 代码描述数据库结构
Exposed 的表定义,本质上是 Kotlin 对象。你不需要写 SQL DDL,直接用代码描述字段和约束。
// 定义用户表
object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 100).uniqueIndex()
val age = integer("age").default(0)
val createdAt = datetime("created_at")
override val primaryKey = PrimaryKey(id)
}
嗯,这里要注意:Table() 是基类,每个字段对应一个 Kotlin 属性。字段类型、长度、默认值、唯一索引,全在声明里搞定。
varchar("userName", 50) 这种混搭,后期维护会疯。
如果你需要关联表,比如用户和订单,可以这样定义外键:
object Orders : Table() {
val id = integer("id").autoIncrement()
val userId = integer("user_id").references(Users.id)
val amount = decimal("amount", 10, 2)
val status = varchar("status", 20)
}
我在项目中遇到过一个问题:references 默认不会在数据库层面创建外键约束,只是逻辑关联。如果你需要物理外键,得手动加 ForeignKeyConstraint。不过我个人建议,微服务场景下尽量别用物理外键,维护成本太高。
2. CRUD 操作:增删改查的 Kotlin 写法
Exposed 的 CRUD 非常直观。先看插入:
transaction {
Users.insert {
it[name] = "张三"
it[email] = "zhangsan@example.com"
it[age] = 28
it[createdAt] = LocalDateTime.now()
}
}
查询呢?用 selectAll 或者带条件的 select:
transaction {
val user = Users.selectAll().where { Users.email eq "zhangsan@example.com" }.single()
println(user[Users.name])
}
更新和删除也很直白:
transaction {
Users.update({ Users.id eq 1 }) {
it[age] = 29
}
Users.deleteWhere { Users.id eq 2 }
}
update 和 delete 如果不加条件,会操作全表。有一次我在测试环境忘了加 where 子句,直接把整个用户表清空了……还好有备份。所以,生产环境务必先 select 确认条件。
3. 事务管理:别让数据不一致
Exposed 的事务管理,核心就是 transaction 块。它自动开启、提交、回滚。你不需要手动管理连接。
transaction {
// 多个操作在同一个事务里
Users.insert { ... }
Orders.insert { ... }
// 如果这里抛出异常,前面所有操作自动回滚
throw RuntimeException("模拟异常")
}
为什么这样设计?因为数据库操作最怕半截失败。你想想看,用户注册时,插入了用户表,但订单表插入失败——用户数据就脏了。事务就是用来保证「要么全做,要么全不做」。
如果你需要嵌套事务,Exposed 也支持:
transaction {
// 外层事务
Users.insert { ... }
transaction(TransactionIsolation.REPEATABLE_READ) {
// 内层事务,独立隔离级别
Orders.insert { ... }
}
}
我个人习惯:一个请求一个事务。别在循环里开事务,也别把事务范围拉得太大。事务越短,锁竞争越少,并发越高。
4. DSL 查询:比 SQL 更 Kotlin
Exposed 的 DSL 查询,是我最喜欢它的地方。它不像 JPA 那样用字符串拼 JPQL,也不像 MyBatis 那样写 XML。它就是纯 Kotlin 代码,类型安全,编译期就能发现错误。
来个复杂点的例子:查询年龄大于 25 的用户,按创建时间倒序,只取前 10 条。
transaction {
val result = Users
.selectAll()
.where { Users.age greater 25 }
.orderBy(Users.createdAt, SortOrder.DESC)
.limit(10)
.toList()
result.forEach { row ->
println("${row[Users.name]} - ${row[Users.age]}")
}
}
连表查询呢?用 join 或者 innerJoin:
transaction {
val result = (Users innerJoin Orders)
.select(Users.name, Orders.amount)
.where { Orders.status eq "PAID" }
.toList()
result.forEach { row ->
println("${row[Users.name]} 支付了 ${row[Orders.amount]}")
}
}
你可能会问:聚合查询怎么写?比如统计每个用户的订单总数:
transaction {
val result = Orders
.slice(Orders.userId, Orders.id.count())
.selectAll()
.groupBy(Orders.userId)
.toList()
result.forEach { row ->
println("用户 ${row[Orders.userId]} 有 ${row[Orders.id.count()]} 个订单")
}
}
5. 知识体系总览
下面这张图,概括了 Exposed 框架的核心模块和它们之间的关系:
6. 实战建议
Exposed 框架上手很快,但有几个点我建议你注意:
- 连接池:别用 Exposed 自带的连接管理,配合 HikariCP 用。我在项目中用
Database.connect(hikariDataSource),稳定又高效。 - 迁移工具:表结构变更别手动跑 SQL。用 Exposed 的
SchemaUtils或者 Flyway 做版本管理。我吃过「测试环境和生产环境表结构不一致」的亏。 - 批量操作:插入大量数据时,用
batchInsert而不是循环insert。性能差距能到 10 倍以上。
.prepareSQL(this) 打印出生成的 SQL,确认逻辑正确再跑。这能帮你快速定位问题。
好了,这一章的内容就到这里。Exposed 框架的核心就是表定义、CRUD、事务、DSL 查询这四块。你把它吃透了,Kotlin 后端的数据层基本就稳了。
公众号:蓝海资料掘金营,微信deep3321