Skip to content

使用 Exposed 实现数据库持久化

在本系列教程中,我们将向您展示如何在 Ktor 中创建一个简单的博客应用:

  • 在第一个教程中,我们展示了如何托管图片和 HTML 页面等静态内容。
  • 在第二个教程中,我们使用 FreeMarker 模板引擎为应用程序添加了交互性。
  • 本教程中,我们将使用 Exposed 框架为我们的网站添加持久化。我们将使用 H2 本地数据库来存储文章。
  • 下一个教程中,我们将了解如何分别使用 HikariCP 和 Ehcache 库实现数据库连接池与缓存。

添加依赖项

首先,您需要为 Exposed 和 H2 库添加依赖项。打开 gradle.properties 文件并指定库版本:

kotlin
kotlinx_serialization_version = 1.8.0
kotlin_css_version = 1.0.0-pre.721

然后,打开 build.gradle.kts 并添加以下依赖项:

kotlin

点击 build.gradle.kts 文件右上角的 Load Gradle Changes 图标以安装新添加的依赖项。

更新模型

Exposed 使用 org.jetbrains.exposed.sql.Table 类作为数据库表。要更新 Article 模型,请打开 models/Article.kt 文件并将现有代码替换为以下内容:

kotlin

idtitlebody 列将存储有关我们文章的信息。id 列将作为主键。

如果您检查 Articles 对象中属性的类型,您会看到它们具有带有必要类型实参的 Column 类型:id 的类型为 Column<Int>,而 titlebody 的类型均为 Column<String>

连接到数据库

数据访问对象 (DAO) 是一种模式,它提供数据库接口而不暴露特定数据库的细节。稍后我们将定义一个 DAOFacade 接口,以抽象化我们对数据库的特定请求。

每次使用 Exposed 进行数据库访问都是从获取数据库连接开始的。为此,您需要将 JDBC URL 和驱动类名传递给 Database.connect 函数。在 com.example 内部创建 dao 软件包,并添加一个新的 DatabaseSingleton.kt 文件。然后,插入这段代码:

kotlin

请注意,driverClassNamejdbcURL 在这里是硬编码的。Ktor 允许您将此类设置提取到自定义配置组

创建表

获取连接后,所有 SQL 语句都应放在事务中:

kotlin
fun init() {
    // ...
    val database = Database.connect(jdbcURL, driverClassName)
    transaction(database) {
        // 此处放置语句
    }
}

在此代码示例中,默认数据库被显式传递给 transaction 函数。如果您只有一个数据库,可以省略它。在这种情况下,Exposed 会自动将最后连接的数据库用于事务。

请注意,在调用事务之前,Database.connect 函数不会建立真正的数据库连接——它只是为未来的连接创建一个描述符。

鉴于 Articles 表已经声明,我们可以在 init 函数底部调用包装在 transaction 调用中的 SchemaUtils.create(Articles),以指示数据库在表不存在时创建该表:

kotlin
fun init() {
    // ...
    val database = Database.connect(jdbcURL, driverClassName)
    transaction(database) {
        SchemaUtils.create(Articles)
    }
}

执行查询

为了方便起见,让我们在 DatabaseSingleton 对象内部创建一个工具函数 dbQuery,我们将用它处理未来所有对数据库的请求。与其使用事务以阻塞方式访问它,不如利用协程并在各自的协程中启动每个查询:

kotlin

最终的 DatabaseSingleton.kt 文件应如下所示:

kotlin

在启动时加载数据库配置

最后,我们需要在应用程序启动时加载创建的配置。打开 Application.kt 并从 Application.module 主体中调用 DatabaseSingleton.init

kotlin

实现持久化逻辑

现在让我们创建一个接口,以抽象化更新文章所需的操作。在 dao 软件包内创建 DAOFacade.kt 文件,并填入以下代码:

kotlin

我们需要列出所有文章、按 ID 查看文章、添加新文章、编辑或删除文章。由于所有这些函数都在底层执行数据库查询,因此它们被定义为挂起函数。

要实现 DAOFacade 接口,请将文本光标置于其名称处,点击该接口旁边的黄色灯泡图标,然后选择 Implement interface。在调出的对话框中,保留默认设置并点击 OK

Implement Members 对话框中,选择所有函数并点击 OK

实现成员

IntelliJ IDEA 会在 dao 软件包内创建 DAOFacadeImpl.kt 文件。让我们使用 Exposed DSL 实现所有函数。

获取所有文章

让我们从返回所有条目的函数开始。我们的请求被包装在 dbQuery 调用中。我们调用 Table.selectAll 扩展函数来从数据库获取所有数据。Articles 对象是 Table 的子类,因此我们使用 Exposed DSL 方法来操作它。

kotlin

Table.selectAll 返回一个 Query 实例,因此要获取 Article 实例列表,我们需要手动提取每一行的数据并将其转换为我们的数据类。我们使用从 ResultRow 构建 Article 的辅助函数 resultRowToArticle 来完成此操作。

ResultRow 提供了一种通过使用简洁的 get 运算符来获取存储在指定 Column 中数据的方法,允许我们使用类似于数组或映射的方括号语法。

Articles.id 的类型是 Column<Int>,它实现了 Expression 接口。这就是为什么我们可以将任何列作为表达式传递的原因。

获取一篇文章

现在让我们实现一个返回一篇文章的函数:

kotlin

select 函数接受一个扩展 lambda 作为实参。此 lambda 内部的隐式接收者类型为 SqlExpressionBuilder。您不会显式使用此类型,但它定义了一系列用于列的有用操作,您可以利用这些操作来构建查询。您可以使用比较(eqlessgreater)、算术运算(plustimes)、检查值是否属于或不属于提供的列表(inListnotInList)、检查值是否为 null 或非 null 等等。

select 返回一个 Query 值的列表。和之前一样,我们将它们转换为文章。在我们的案例中,它应该是一篇文章,因此我们将其作为结果返回。

添加新文章

要在表中插入新文章,请使用 Table.insert 函数,它接受一个 lambda 实参:

kotlin

在这个 lambda 内部,我们指定应该为哪一列设置哪个值。it 实参的类型为 InsertStatement,我们可以在其上调用接受列和值作为实参的 set 运算符。

编辑文章

要更新现有文章,请使用 Table.update

kotlin

删除文章

最后,使用 Table.deleteWhere 从数据库中移除文章:

kotlin

初始化 DAOFacade

让我们创建一个 DAOFacade 实例,并在应用程序启动前添加一篇示例文章插入到数据库中。 在 DAOFacadeImpl.kt 的底部添加以下代码:

kotlin

更新路由

现在我们准备在路由处理程序中使用实现的数据库操作。 打开 plugins/Routing.kt 文件。 要显示所有文章,在 get 处理程序内部调用 dao.allArticles

kotlin

要发布新文章,在 post 内部调用 dao.addNewArticle 函数:

kotlin

要获取用于显示和编辑的文章,分别在 get("{id}")get("{id}/edit") 内部使用 dao.article

kotlin

最后,转到 post("{id}") 处理程序并使用 dao.editArticle 更新文章,使用 dao.deleteArticle 删除文章:

kotlin

您可以在此处找到本教程的最终项目:tutorial-website-interactive-persistence

运行应用程序

让我们看看我们的日志应用是否按预期运行。我们可以通过点击 Application.ktfun main(...) 旁边的 运行 按钮来运行我们的应用程序:

运行服务器

IntelliJ IDEA 将启动应用程序,几秒钟后,我们应该会看到应用正在运行的确认信息:

Bash
[main] INFO  Application - Responding at http://0.0.0.0:8080

在浏览器中打开 http://localhost:8080/ 并尝试创建、编辑和删除文章。文章将保存在 build/db.mv.db 文件中。在 IntelliJ IDEA 中,您可以在 Database 工具窗口中查看此文件的内容。

Database 工具窗口