- ORM
- 后端
- 数据存储
【免费下载链接】Exposed
Kotlin SQL Framework
本文是一份以 JetBrains Exposed(Kotlin SQL Framework)DAO API 为核心的上手指南,全程围绕一个可运行的简单控制台应用展开:你将学会配置内存数据库连接、用IntIdTable定义表、用IntEntity定义实体,并在事务中完成 Create、Read、Update、Delete 全流程操作。读完本文,你将掌握 Exposed DAO 的核心开发模式,并能在自己的 Kotlin 项目中直接迁移这套写法。文中所涉及的完整示例代码位于 documentation-website/Writerside/snippets/get-started-with-exposed-dao,可在仓库中对照阅读。
前置准备
开始之前,请确保你的机器上具备以下环境:
- Gradle:最新发行版(用于初始化与管理项目构建);
- JDK:版本 8 或更高;
- IDE:如 IntelliJ IDEA。官方推荐 IntelliJ IDEA Ultimate,其内置数据库工具并支持 Exposed 插件,可提供代码补全、检查,以及根据已有 schema 生成表与实体的能力。
创建新项目
在终端中创建一个新目录并进入:
mkdir exposed-dao-kotlin-app cd exposed-dao-kotlin-app运行gradle init初始化 Gradle 项目:
gradle init按提示选择以下选项:
- 项目类型选择
1: Application; - 实现语言选择
2: Kotlin; - 其余问题直接回车使用默认值即可。
初始化完成后,在 IDE 中打开项目(IntelliJ IDEA 可直接执行idea .)。
添加依赖
使用 Exposed 前需要为项目添加依赖。首先打开gradle/libs.versions.toml,定义 Exposed 与 H2 的版本和构件(当前仓库自身的版本声明可参考 gradle/libs.versions.toml):
[versions] //... exposed = "%exposed_version%" h2 = "%h2_db_version%" [libraries] //... exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } h2 = { module = "com.h2database:h2", version.ref = "h2" }三个 Exposed 构件各有分工:
- exposed-core:提供以类型安全方式操作数据库所需的基础组件与抽象,包含 DSL API;
- exposed-dao:提供 Data Access Object(DAO)API,即本文主角;
- exposed-jdbc:
exposed-core的扩展,增加 Java Database Connectivity(JDBC)支持。
然后打开app/build.gradle.kts,在dependencies块中加入这些模块:
dependencies { //... implementation(libs.exposed.core) implementation(libs.exposed.dao) implementation(libs.exposed.jdbc) implementation(libs.h2) //... }修改完构建脚本后,在 IntelliJ IDEA 中点击编辑器右侧的 Gradle 通知图标加载 Gradle 变更。
配置数据库连接
使用 Exposed 访问数据库时,第一步是获取连接并创建事务。连接配置通过Database.connect()完成。打开app/src/main/kotlin/org/example/App.kt,写入如下实现:
package org.example import org.jetbrains.exposed.v1.jdbc.Database fun main() { Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") // ... }Database.connect()创建了一个代表数据库的实例,这里传入了连接 URL 与驱动两个参数。逐段拆解 URLjdbc:h2:mem:test:
jdbc:表示这是一个 JDBC 连接;h2:表示目标数据库是 H2;mem:表示数据库运行在内存中,数据只存在于内存,应用停止即丢失;test:数据库名称。
org.h2.Driver指定用于建立连接的 H2 JDBC 驱动。
两个值得注意的行为:
- 惰性连接:调用
Database.connect()只配置连接设置,并不会立即建立数据库连接,真正的连接在首次执行数据库操作时才建立; - 自动注册:默认情况下 Exposed 会自动注册该数据库连接,如需改变这一行为,可在调用
Database.connect()时设置connectionAutoRegistration参数。
从源码结构看,Database.connect()在 Database.kt 中提供了多个重载,支持传入 URL、驱动、数据源(DataSource)等不同形式的连接配置,便于在真实项目中接入连接池。
至此,你已为 Kotlin 项目添加了 Exposed 并配置好数据库连接,接下来可以定义数据模型并使用 DAO API 与数据库交互。
定义表对象
Exposed 的 DAO API 提供了基类IdTable及其子类,用于定义以标准id列作为主键的表。在app/src/main/kotlin/org/example/目录下新建Task.kt文件,添加表定义:
package org.example import org.jetbrains.exposed.v1.core.dao.id.IntIdTable object Tasks : IntIdTable("tasks") { val title = varchar("name", 128) val description = varchar("description", 128) val isCompleted = bool("completed").default(false) }在IntIdTable构造函数中传入名称tasks,即为表配置了自定义名称。如果不提供名称,Exposed 会根据对象名推导表名,这可能因命名规范不同而产生意外结果。
Tasks对象定义了以下列:
title与description:String类型列,使用varchar()创建,每列最大长度 128 字符;isCompleted:Boolean类型列,使用bool()定义,并通过default(false)将默认值配置为false。
IntIdTable会自动为表添加一个自增整数id列作为主键。这一点在源码中有直接体现:见 IdTable.kt,IntIdTable的内部实现即为:
final override val id: Column<EntityID<Int>> = integer(columnName).autoIncrement(sequenceName).entityId() final override val primaryKey = PrimaryKey(id)也就是说,integer()定义 4 字节整型列、autoIncrement()声明自增(可传sequenceName指定序列名)、entityId()将其包装为EntityID<Int>,并最终声明为表的主键。到这里,你已经定义了一个带列的表,相当于为tasks表创建了蓝图。
定义实体
使用 DAO 方式时,每个基于IntIdTable定义的表都必须关联一个对应的实体类(实体类详细说明见 DAO-Entity-definition.topic)。实体类代表表中的单条记录,由主键唯一标识。继续完善Task.kt:
package org.example import org.jetbrains.exposed.v1.core.dao.id.EntityID import org.jetbrains.exposed.v1.core.dao.id.IntIdTable import org.jetbrains.exposed.v1.dao.IntEntity import org.jetbrains.exposed.v1.dao.IntEntityClass object Tasks : IntIdTable("tasks") { val title = varchar("name", 128) val description = varchar("description", 128) val isCompleted = bool("completed").default(false) } class Task(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<Task>(Tasks) var title by Tasks.title var description by Tasks.description var isCompleted by Tasks.isCompleted override fun toString(): String { return "Task(id=$id, title=$title, completed=$isCompleted)" } }逐项理解这段代码:
Task继承IntEntity,它是主键类型为Int的实体基类。源码定义见 IntEntity.kt(abstract class IntEntity(id: EntityID<Int>) : Entity<Int>(id));- 构造函数中的
EntityID<Int>参数代表该实体所映射的数据库行的主键; companion object继承IntEntityClass<Task>,将实体类与Tasks表绑定,同时承担查询入口的职责(源码见 IntEntity.kt);title、description、isCompleted三个属性通过 Kotlin 的by关键字委托给Tasks表中的对应列;toString()自定义了Task实例的字符串表示,便于调试与日志输出,打印结果将包含实体 ID、标题与完成状态。
创建和查询表
借助 DAO API,你可以用类型安全、面向对象的方式操作数据库,就像操作普通 Kotlin 类一样。所有数据库操作都必须运行在事务内:事务由Transaction类的实例表示,你可以在其 lambda 中定义和操作数据,Exposed 会在后台自动管理事务的打开与关闭(事务的完整说明见 Transactions.md)。
打开App.kt,加入事务函数:
package org.example import org.jetbrains.exposed.v1.core.StdOutSqlLogger import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.jetbrains.exposed.v1.jdbc.transactions.transaction fun main() { Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") transaction { addLogger(StdOutSqlLogger) SchemaUtils.create(Tasks) val task1 = Task.new { title = "Learn Exposed DAO" description = "Follow the DAO tutorial" } val task2 = Task.new { title = "Read The Hobbit" description = "Read chapter one" isCompleted = true } println("Created new tasks with ids ${task1.id} and ${task2.id}") val completed = Task.find { Tasks.isCompleted eq true }.toList() println("Completed tasks: ${completed.count()}") } }首先使用SchemaUtils.create()创建tasks表。SchemaUtils对象持有创建、修改和删除数据库对象的工具方法。
表创建完成后,使用IntEntityClass的扩展方法.new()添加两条新的Task记录:
val task1 = Task.new { title = "Learn Exposed DAO" description = "Follow the DAO tutorial" } val task2 = Task.new { title = "Read The Hobbit" description = "Read chapter one" isCompleted = true }从源码结构看,new()在 EntityClass.kt 中定义,接受一个以实体为接收者的初始化 lambda,Exposed 会在插入后把生成的主键回填到实体的id属性上。示例中task1、task2都是Task实体的实例,各自代表Tasks表中的新行;在new块内为各列赋值后,Exposed 会翻译为如下 SQL:
INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Learn Exposed DAO', 'Follow the DAO tutorial', FALSE) INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Read The Hobbit', 'Read chapter one', TRUE)随后用.find()执行带条件的查询,检索所有isCompleted为true的任务:
val completed = Task.find { Tasks.isCompleted eq true }.toList() println("Completed tasks: ${completed.count()}")find {}接收一个类型安全的条件 lambda(Tasks.isCompleted eq true),返回可迭代的结果集;toList()触发实际查询执行。注意 DAO 查询是惰性的——查询不会立即执行,直到你调用遍历结果的方法(如toList()、forEach())时才真正发起 SQL。
在测试代码之前,最好能看到 Exposed 发送给数据库的 SQL 语句,为此需要添加日志。
启用日志
在transaction块的开头加入如下代码以启用 SQL 查询日志:
transaction { addLogger(StdOutSqlLogger) SchemaUtils.create(Tasks) // ... }addLogger(StdOutSqlLogger)将 SQL 语句输出到标准输出(stdout),方便观察 Exposed 实际生成的 DDL 与 DML。
运行应用
在 IntelliJ IDEA 中点击 gutter 区域的运行按钮启动应用,应用将在底部Run工具窗口启动,你可以在那里看到 SQL 日志与打印结果:
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE' SQL: CREATE TABLE IF NOT EXISTS TASKS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL, DESCRIPTION VARCHAR(128) NOT NULL, COMPLETED BOOLEAN DEFAULT FALSE NOT NULL) SQL: INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Learn Exposed DAO', 'Follow the DAO tutorial', FALSE) SQL: INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Read The Hobbit', 'Read chapter one', TRUE) Created new tasks with ids 1 and 2 SQL: SELECT TASKS.ID, TASKS."name", TASKS.DESCRIPTION, TASKS.COMPLETED FROM TASKS WHERE TASKS.COMPLETED = TRUE Completed tasks: 1对照输出可以看出:SchemaUtils.create(Tasks)生成了CREATE TABLE IF NOT EXISTS;两次Task.new分别产生 INSERT;Task.find生成了带WHERE TASKS.COMPLETED = TRUE的 SELECT。这也验证了 DAO 实体到 SQL 的映射关系。
更新和删除任务
下面扩展应用功能:更新并删除任务。在同一个transaction()函数中追加代码(对应 snippets 中的 App.kt):
// Update task1.title = "Try Exposed DAO" task1.isCompleted = true println("Updated task1: $task1") // Delete task2.delete() println("Remaining tasks: ${Task.all().toList()}")更新实体属性就像修改普通 Kotlin 类的属性一样简单:直接对task1.title和task1.isCompleted赋值即可。删除则调用实体的.delete()方法,task2.delete()会将该行从数据库中移除。
关键机制:延迟刷新(lazy flush)。Exposed 在修改实体属性(如task1.title)时并不会立即发出UPDATE语句,而是将改动缓存在内存中,在下一次读操作之前或事务结束时统一刷新到数据库。因此你会在日志中看到这样的输出顺序:
SQL: UPDATE TASKS SET COMPLETED=TRUE, "name"='Try Exposed DAO' WHERE ID = 1重启应用(IntelliJ IDEA 中点击 rerun 按钮)后,应看到以下完整结果:
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE' SQL: CREATE TABLE IF NOT EXISTS TASKS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL, DESCRIPTION VARCHAR(128) NOT NULL, COMPLETED BOOLEAN DEFAULT FALSE NOT NULL) SQL: INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Learn Exposed DAO', 'Follow the DAO tutorial', FALSE) SQL: INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Read The Hobbit', 'Read chapter one', TRUE) Created new tasks with ids 1 and 2 SQL: SELECT TASKS.ID, TASKS."name", TASKS.DESCRIPTION, TASKS.COMPLETED FROM TASKS WHERE TASKS.COMPLETED = TRUE Completed tasks: 1 Updated task1: Task(id=1, title=Try Exposed DAO, completed=true) SQL: UPDATE TASKS SET COMPLETED=TRUE, "name"='Try Exposed DAO' WHERE ID = 1 SQL: DELETE FROM TASKS WHERE TASKS.ID = 2 SQL: SELECT TASKS.ID, TASKS."name", TASKS.DESCRIPTION, TASKS.COMPLETED FROM TASKS Remaining tasks: [Task(id=1, title=Try Exposed DAO, completed=true)]注意日志中Updated task1: Task(id=1, ...)打印在 UPDATE 之前:这正是因为toString()读取的是内存中尚未刷新的属性值,随后 Exposed 才在下次读取(Task.all())前把改动 flush 到数据库。最后Task.all()列出表中全部剩余任务,确认task2已被删除。
关于内存数据库的一个提醒
如果在此之后打开第二个事务,你可能会发现表和数据都消失了——即使应用还没有停止。这是 H2 内存数据库在管理连接与事务时的预期行为。若希望数据库保持打开,可在 URL 中追加DB_CLOSE_DELAY=-1:
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")下一步:继续深入 DAO API
你已经用 Exposed 的 DAO API 构建了一个简单的控制台应用,完成了内存数据库中的建表、插入、查询、更新与删除。接下来可以从两个方向继续深入:
- 学习完整的 CRUD 操作,掌握批量操作、复杂查询等进阶用法;
- 学习如何在实体间定义关系(一对一、一对多、多对多等)。
这些章节将帮助你用 Exposed 类型安全、面向对象的方式构建更复杂、更贴近真实业务的数据模型。
- ORM
- 后端
- 数据存储
【免费下载链接】Exposed
Kotlin SQL Framework
相关推荐
数据科学走进现实世界:Data-Science-For-Beginners 课程第 20 课“真实世界应用”全景解读
数据科学走进现实世界:Data Science For Beginners 课程第 20 课“真实世界应用”全景解读 本篇以 Data Science For
ORM后端数据存储如何快速上手WeUI:5分钟搭建微信风格界面的完整教程
如何快速上手WeUI:5分钟搭建微信风格界面的完整教程 WeUI是微信官方设计团队专门为微信网页开发量身打造的UI框架,能够帮助开发者快速构建与微信原生体验一致
UI组件前端如何快速上手TiDB:面向新手的完整使用教程
如何快速上手TiDB:面向新手的完整使用教程 TiDB是一个开源的分布式NewSQL数据库,它结合了传统关系型数据库的最佳特性和NoSQL系统的可扩展性。作为一
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考