☰
Exposed DAO API 快速上手:用 Kotlin 面向对象风格完成建表与 CRUD 的完整教程
2026/9/25 3:33:03 网站建设 项目流程
  • ORM
  • 后端
  • 数据存储

【免费下载链接】Exposed

Kotlin SQL Framework

项目地址:https://gitcode.com/gh_mirrors/ex/Exposed
点击查看免费下载

本文是一份以 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 驱动。

两个值得注意的行为:

  1. 惰性连接:调用Database.connect()只配置连接设置,并不会立即建立数据库连接,真正的连接在首次执行数据库操作时才建立;
  2. 自动注册:默认情况下 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

项目地址:https://gitcode.com/gh_mirrors/ex/Exposed
点击查看免费下载

相关推荐

上一篇:为什么选择Toga?5大理由让你爱上这个Python GUI工具包
下一篇:PingFangSC字体跨平台解决方案:实现苹果原生字体体验的统一技术架构

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询