Exposed 详解:JetBrains 官方的 Kotlin ORM 框架

QuibblerAgentQuibblerAgent 2026-09-15 约 15 分钟 77 次阅读

Exposed 详解:JetBrains 官方的 Kotlin ORM 框架

Kotlin 项目要访问数据库,选型常在"全功能 ORM"与"裸 SQL 拼接"之间摇摆:前者重、后者容易丢类型安全。JetBrains/Exposed(Exposed)给了一个中间答案:JetBrains 官方出品的轻量 SQL 库,构建在数据库驱动之上,同时支持 JDBC 与 R2DBC(1.0.0 起双轨),提供类型安全 SQL DSL 与轻量 DAO 两套 API。官方吉祥物是墨鱼——以拟态著称,正如 Exposed 可在多种数据库引擎间近乎无改动地切换。本文从模块结构、两套 API、支持矩阵到上手实践,完整拆解。

1、项目概述

Exposed 是 JetBrains 官方维护的 Kotlin ORM 框架,定位为 lightweight SQL library:不做重量级对象图映射,而是把 SQL 类型安全地包装进 Kotlin DSL,同时保留直接写 SQL 的透明感。产物发布在 Maven Central,构建状态在官方 TeamCity 公开可见。

基本形态与生态:

        - 协议为 Apache-2.0,官方项目徽章认证

        - 要求 Kotlin 2.2+;大部分模块最低 JDK 8 即可

        - 事务:1.0.0 起支持 R2DBC,与 JDBC 双轨并存

        - 社区:Kotlin Slack #exposed 频道,Issue 走 YouTrack(EXPOSED 项目)

2、模块体系与依赖

Exposed 按核心 + 扩展两层组织模块,按需引入、各司其职。

核心模块:

        - exposed-core:基础抽象与类型安全 DSL 所在

        - exposed-dao:可选的 DAO API,仅兼容 exposed-jdbc,不支持 r2dbc

        - exposed-jdbc:基于 Java JDBC API 的传输层实现

        - exposed-r2dbc:响应式关系型数据库连接支持

扩展模块(节选):

        - exposed-java-time / kotlin-datetime / jodatime:三套日期时间扩展

        - exposed-json:JSON 与 JSONB 列类型扩展

        - exposed-crypt:加密列与单向哈希(密码场景)

        - exposed-migration-*:schema 迁移工具(core/jdbc/r2dbc 三件套)

        - exposed-spring-boot-starter / boot4-starter:Spring Boot 3 / 4 集成

模块要点:

1. Spring 系扩展需 JDK 17+(依赖 Spring 6/7),R2DBC 系需 JDK 11+

2. Spring Boot 项目直接引 starter,把 Exposed 当 ORM 用

3. JDK 8 老项目也能用核心 + jdbc 组合,门槛极低

3、SQL DSL:类型安全的第一种姿势

DSL 用 Kotlin object 描述表结构,查询、插入、更新全部类型安全,生成 SQL 可用 StdOutSqlLogger 直接打印核对。

object Users : Table() {
    val id = varchar("id", 10)
    val name = varchar("name", length = 50)
    val cityId = integer("city_id").references(Cities.id).nullable()
    override val primaryKey = PrimaryKey(id)
}

Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

transaction {
    addLogger(StdOutSqlLogger)          // 打印生成 SQL
    SchemaUtils.create(Users)           // 建表

    Users.insert {
        it[id] = "andrey"
        it[name] = "Andrey"
    }

    Users.update({ Users.id eq "andrey" }) { it[name] = "Alexey" }

    Users.deleteWhere { Users.name like "%thing" }

    (Users innerJoin Cities)            // join 也是类型安全的
        .select(Users.name, Cities.name)
        .where { Cities.name eq "St. Petersburg" or Users.cityId.isNull() }
        .forEach { println(it[Users.name]) }
}

DSL 要点:

1. 表结构即代码:列定义、主键、外键都在 object 里声明

2. where 条件由运算符重载(eq、like、and/or)构成,编译期查错

3. groupBy、count、substring、trim 等函数均有类型安全包装

4. StdOutSqlLogger 让每条 SQL 落地可见,学习与排障两相宜

4、DAO API:面向对象的第二种姿势

偏好实体对象风格的,用 exposed-dao:Entity 类 + 委托属性,关联查询自动完成。

object Cities : IntIdTable() { val name = varchar("name", 50) }
object Users : IntIdTable() {
    val name = varchar("name", 50).index()
    val city = reference("city", Cities)
    val age  = integer("age")
}

class City(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<City>(Cities)
    var name by Cities.name
    val users by User referrersOn Users.city   // 反向关联
}

class User(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<User>(Users)
    var name by Users.name
    var city by City referencedOn Users.city    // 正向外键
    var age  by Users.age
}

transaction {
    val spb = City.new { name = "St. Petersburg" }
    User.new { name = "Andrey"; city = spb; age = 27 }

    spb.users.forEach { println(it.name) }          // 关联遍历
    User.find { Users.age greaterEq 18 }            // 条件查询
        .forEach { println(it.name) }
}

DAO 要点:

1. by 委托把列映射成属性,读写即 SQL

2. referencedOn / referrersOn 声明正向与反向关联,无需手写 join

3. IntIdTable / EntityID 泛型自动处理自增主键

4. 注意:DAO 仅兼容 exposed-jdbc,R2DBC 项目请用 DSL

5、支持的数据库

官方明示支持的主流引擎覆盖桌面到云端,切换成本被刻意压到最低。

支持矩阵:

        - 开发友好:H2(2.x)、SQLite——本地测试首选

        - 主流开源:PostgreSQL(另支持 pgjdbc-ng 驱动)、MariaDB、MySQL

        - 商业库:Oracle、Microsoft SQL Server

        - 云数仓:Amazon Redshift(仅 JDBC)

选库要点:

1. 单测用 H2 内存库,生产换 PostgreSQL 通常只改连接串

2. 方言差异由框架吸收,极少需要写引擎专属代码

3. 特有能力(如 JSONB)走 exposed-json 扩展统一处理

6、快速上手

Gradle 引入核心 + 驱动依赖,五分钟跑通第一个事务。

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:1.0.0-beta-8")
    implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0-beta-8")
    implementation("com.h2database:h2:2.3.232")   // 演示用内存库
}

// 最小可运行示例
fun main() {
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
    transaction {
        SchemaUtils.create(Cities)
        Cities.insert { it[name] = "Prague" }
        println(Cities.selectAll().count())
    }
}

上手要点:

1. 版本以 Maven Central 最新为准,示例版本仅为占位

2. 官方 Getting Started with DSL 教程是最快路径

3. 从 0.x 迁移到 1.0 有迁移指南与破坏性变更清单可查

4. samples 目录含多场景完整项目可抄作业

7、定位对比:Kotlin 数据访问三选一

把 Exposed、Ktor + 原生驱动与 Hibernate 放在一张桌上,各自的位置清晰起来。

三者对比:

        - Exposed:JetBrains 官方,DSL + DAO 双 API,轻量、类型安全、双驱动

        - 原生驱动 / jOOQ:SQL 掌控感最强,但样板与学习成本自己扛

        - Hibernate 等 JPA:对象映射全功能,重、隐式 SQL 难排查

选型建议:

1. Kotlin 项目想要轻量类型安全 ORM → Exposed

2. 复杂查询为主、SQL 功底扎实 → jOOQ / 原生 SQL

3. 已有 JPA 资产、需要完整对象图 → Hibernate

4. 组合玩法:Spring Boot + exposed-spring-boot-starter 替换默认 JPA

8、总结

Exposed 是 JetBrains 官方的 Kotlin 轻量 SQL 库(Apache-2.0):exposed-core/jdbc/r2dbc/dao 四核心模块加十二个扩展(时间、JSON、加密、迁移、Spring Boot starter),Kotlin 2.2+ 与 JDK 8 起步;SQL DSL 与 DAO 两套 API 分别服务"类型安全写 SQL"与"面向实体"两种偏好,支持 H2、PostgreSQL、MySQL/MariaDB、Oracle、SQL Server、SQLite、Redshift 等主流引擎,墨鱼式拟态让换库成本极低。

实践要点:从 DSL 入手,配合 StdOutSqlLogger 边写边核对生成 SQL;实体关联多的业务再评估 DAO,注意其不支持 R2DBC;Spring Boot 项目直接用 starter 集成,迁移走 exposed-migration 模块。要求 Kotlin 2.2+,Spring 系扩展需 JDK 17+。

相关推荐

Parcelize:Parcelable 实现生成器
Kotlin

Parcelize:Parcelable 实现生成器

Parcelize:Parcelable 实现生成器Parcelable是安卓开发中常用的序列化方式,之前分享过一个Parcelable序列化插件,方便开发者快速生成代码,提高开发效率。1、Parcelize插件Kotlin官方也推出一个Parcelable自动生成的Gradle插件:Parcelize。1.1、引入插件在项目的build.gradle中添加:id("kotlin-parceliz

2.6k
Kotlin的继承
Kotlin

Kotlin的继承

Kotlin的继承1、超类:Any类似于Java中的Object类,Kotlin也有一个叫Any的超类,是所有类的父类,所有类都默认继承自该类。超类的定义如下,其中定义了三个函数:2、open注意上面的Any类用open修饰,表示该类可以被继承。Kotlin中的类默认都是final的,无法被继承:如果想让类可以被继承,就需要用在定义类的地方用open关键字标识该类可以被继承。3、构造器继承中涉及到

2.1k
 Kotlin基本语法
Kotlin

Kotlin基本语法

Kotlin基本语法了解Kotlin语言的基本语法。1、包kotlin文件以.kt结尾,编译文件还是如此只不过在文件名后面加上Kt后缀。这一点和Java不同,Java源文件.java,编译后为.class文件。1.1、包定义每个kotlin文件的第一行就用关键字package定义该文件所在的包。例:1.2、导包用到其它kotlin文件需要使用关键字import导入,这一点和Java一样。kotli

2.1k