第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 属性。字段类型、长度、默认值、唯一索引,全在声明里搞定。

我的习惯:字段名用下划线风格(snake_case),和数据库保持一致。Kotlin 属性名用驼峰,Exposed 会自动映射。但如果你用 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 }
}
我曾经踩过的坑:updatedelete 如果不加条件,会操作全表。有一次我在测试环境忘了加 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()]} 个订单")
    }
}
核心要点:DSL 查询的每一步都是类型安全的。字段名写错了?编译报错。类型不匹配?编译报错。这比手写 SQL 的「运行时才报错」强太多了。

5. 知识体系总览

下面这张图,概括了 Exposed 框架的核心模块和它们之间的关系:

Exposed 框架核心模块 表定义 (Table) 字段、主键、外键、索引 CRUD 操作 insert / select / update / delete 事务管理 transaction / 隔离级别 / 回滚 DSL 查询 where / orderBy / limit / join / groupBy / slice ⚠️ 避坑指南 • 表定义时,字段名用 snake_case,属性名用 camelCase • CRUD 操作务必加 where 条件,防止误操作全表 • 事务范围尽量小,避免长事务导致锁竞争

6. 实战建议

Exposed 框架上手很快,但有几个点我建议你注意:

  • 连接池:别用 Exposed 自带的连接管理,配合 HikariCP 用。我在项目中用 Database.connect(hikariDataSource),稳定又高效。
  • 迁移工具:表结构变更别手动跑 SQL。用 Exposed 的 SchemaUtils 或者 Flyway 做版本管理。我吃过「测试环境和生产环境表结构不一致」的亏。
  • 批量操作:插入大量数据时,用 batchInsert 而不是循环 insert。性能差距能到 10 倍以上。
我的小技巧:写 DSL 查询时,先用 .prepareSQL(this) 打印出生成的 SQL,确认逻辑正确再跑。这能帮你快速定位问题。

好了,这一章的内容就到这里。Exposed 框架的核心就是表定义、CRUD、事务、DSL 查询这四块。你把它吃透了,Kotlin 后端的数据层基本就稳了。

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