Skip to content

Exposedを使用したデータベース永続化

コード例: tutorial-website-interactive-persistence

使用されているライブラリ: Exposed, h2database

この一連のチュートリアルでは、Ktorでシンプルなブログアプリケーションを作成する方法を紹介します。

  • 最初のチュートリアルでは、画像やHTMLページなどの静的コンテンツをホストする方法を紹介しました。
  • 2番目のチュートリアルでは、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カラムは主キー(primary key)として機能します。

Articlesオブジェクトのプロパティの型を確認すると、それらが必要な型引数を持つColumn型であることがわかります。idColumn<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関数に明示的に渡されています。データベースが1つしかない場合は、省略可能です。その場合、Exposedは自動的に最後に接続されたデータベースをトランザクションに使用します。

Database.connect関数は、トランザクションを呼び出すまで実際のデータベース接続を確立しません。将来の接続のための記述子(descriptor)を作成するだけです。

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による記事の表示、新しい記事の追加、編集、削除を行う必要があります。これらの関数はすべて内部でデータベースクエリを実行するため、suspend関数として定義されています。

DAOFacadeインターフェースを実装するには、インターフェース名にキャレットを置き、インターフェースの横にある黄色の電球アイコンをクリックして Implement interface を選択します。表示されたダイアログでデフォルト設定のまま OK をクリックします。

Implement Members ダイアログで、すべての関数を選択して OK をクリックします。

Implement Members

IntelliJ IDEAがdaoパッケージ内にDAOFacadeImpl.ktファイルを作成します。Exposed DSLを使用してすべての関数を実装しましょう。

すべての記事を取得する

すべてのエントリを返す関数から始めましょう。リクエストはdbQuery呼び出しにラップされます。Table.selectAll拡張関数を呼び出して、データベースからすべてのデータを取得します。ArticlesオブジェクトはTableのサブクラスであるため、Exposed DSLメソッドを使用して操作します。

kotlin

Table.selectAllQueryのインスタンスを返すため、Articleインスタンスのリストを取得するには、各行のデータを手動で抽出し、データクラスに変換する必要があります。これは、ResultRowからArticleを構築するヘルパー関数resultRowToArticleを使用して行います。

ResultRowは、簡潔なget演算子を使用して特定のColumnに保存されたデータを取得する方法を提供し、配列やマップのようにブラケット構文([])を使用できるようにします。

Articles.idの型はColumn<Int>であり、これはExpressionインターフェースを実装しています。そのため、任意のカラムを式(expression)として渡すことができます。

記事を取得する

次に、1つの記事を返す関数を実装しましょう。

kotlin

select関数は拡張ラムダを引数に取ります。このラムダ内の暗黙のレシーバーはSqlExpressionBuilder型です。この型を明示的に使用することはありませんが、クエリを構築するために使用するカラムに対する一連ের便利な操作が定義されています。比較(eq, less, greater)、算術演算(plus, times)、値が指定されたリストに含まれているかどうかの確認(inList, notInList)、値がnullかどうかの確認などが使用できます。

selectQuery値のリストを返します。以前と同様に、それらを記事に変換します。このケースでは1つの記事であるはずなので、それを結果として返します。

新しい記事を追加する

テーブルに新しい記事を挿入するには、ラムダ引数を取るTable.insert関数を使用します。

kotlin

このラムダ内で、どのカラムにどの値を設定するかを指定します。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.kt内のfun main(...)の横にある Run ボタンを押すことでアプリケーションを実行できます。

Run Server

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 tool window