Exposedを使用したデータベース永続化
コード例: tutorial-website-interactive-persistence
使用されているライブラリ: Exposed, h2database
この一連のチュートリアルでは、Ktorでシンプルなブログアプリケーションを作成する方法を紹介します。
- 最初のチュートリアルでは、画像やHTMLページなどの静的コンテンツをホストする方法を紹介しました。
- 2番目のチュートリアルでは、FreeMarkerテンプレートエンジンを使用してアプリケーションにインタラクティブ機能を追加しました。
- このチュートリアルでは、Exposedフレームワークを使用してウェブサイトに永続化機能を追加します。記事の保存にはH2ローカルデータベースを使用します。
- 次のチュートリアルでは、HikariCPライブラリとEhcacheライブラリをそれぞれ使用して、データベースの接続プーリングとキャッシングを実装する方法を見ていきます。
依存関係の追加
まず、ExposedとH2ライブラリの依存関係を追加する必要があります。gradle.propertiesファイルを開き、ライブラリのバージョンを指定します。
kotlinx_serialization_version = 1.8.0
kotlin_css_version = 1.0.0-pre.721次に、build.gradle.ktsを開き、以下の依存関係を追加します。
build.gradle.ktsファイルの右上隅にある Load Gradle Changes アイコンをクリックして、新しく追加された依存関係をインストールします。
モデルの更新
Exposedは、org.jetbrains.exposed.sql.Tableクラスをデータベーステーブルとして使用します。Articleモデルを更新するには、models/Article.ktファイルを開き、既存のコードを以下に置き換えます。
id、title、bodyカラムは、記事に関する情報を保存します。idカラムは主キー(primary key)として機能します。
Articlesオブジェクトのプロパティの型を確認すると、それらが必要な型引数を持つColumn型であることがわかります。idはColumn<Int>型であり、titleとbodyは両方ともColumn<String>型です。
データベースへの接続
データアクセスオブジェクト (DAO) は、特定のデータベースの詳細を公開せずにデータベースへのインターフェースを提供するパターンです。後で特定のデータベースリクエストを抽象化するために、DAOFacadeインターフェースを定義します。
Exposedを使用したすべてのデータベースアクセスは、データベースへの接続を取得することから始まります。そのためには、JDBC URLとドライバークラス名をDatabase.connect関数に渡します。com.exampleの中にdaoパッケージを作成し、新しいDatabaseSingleton.ktファイルを追加します。そして、以下のコードを挿入します。
ここでは
driverClassNameとjdbcURLがハードコードされていることに注意してください。Ktorでは、このような設定をカスタム設定グループに抽出することができます。
テーブルの作成
接続を取得した後、すべてのSQLステートメントはトランザクション内に配置する必要があります。
fun init() {
// ...
val database = Database.connect(jdbcURL, driverClassName)
transaction(database) {
// ここにステートメントを記述
}
}このコード例では、デフォルトのデータベースがtransaction関数に明示的に渡されています。データベースが1つしかない場合は、省略可能です。その場合、Exposedは自動的に最後に接続されたデータベースをトランザクションに使用します。
Database.connect関数は、トランザクションを呼び出すまで実際のデータベース接続を確立しません。将来の接続のための記述子(descriptor)を作成するだけです。
Articlesテーブルはすでに宣言されているため、init関数の最後にtransaction呼び出しでラップされたSchemaUtils.create(Articles)を呼び出すことで、テーブルがまだ存在しない場合に作成するようデータベースに指示できます。
fun init() {
// ...
val database = Database.connect(jdbcURL, driverClassName)
transaction(database) {
SchemaUtils.create(Articles)
}
}クエリの実行
利便性のために、DatabaseSingletonオブジェクト内にユーティリティ関数dbQueryを作成しましょう。これを今後のすべてのデータベースリクエストに使用します。ブロッキングな方法でアクセスするためにトランザクションを使用する代わりに、コルーチンを活用し、各クエリを独自のコルーチンで開始します。
結果として得られるDatabaseSingleton.ktファイルは以下のようになります。
起動時にデータベース設定をロードする
最後に、作成した設定をアプリケーションの起動時にロードする必要があります。Application.ktを開き、Application.moduleのボディからDatabaseSingleton.initを呼び出します。
永続化ロジックの実装
次に、記事を更新するために必要な操作を抽象化するインターフェースを作成しましょう。daoパッケージ内にDAOFacade.ktファイルを作成し、以下のコードを記述します。
すべての記事の一覧表示、IDによる記事の表示、新しい記事の追加、編集、削除を行う必要があります。これらの関数はすべて内部でデータベースクエリを実行するため、suspend関数として定義されています。
DAOFacadeインターフェースを実装するには、インターフェース名にキャレットを置き、インターフェースの横にある黄色の電球アイコンをクリックして Implement interface を選択します。表示されたダイアログでデフォルト設定のまま OK をクリックします。
Implement Members ダイアログで、すべての関数を選択して OK をクリックします。

IntelliJ IDEAがdaoパッケージ内にDAOFacadeImpl.ktファイルを作成します。Exposed DSLを使用してすべての関数を実装しましょう。
すべての記事を取得する
すべてのエントリを返す関数から始めましょう。リクエストはdbQuery呼び出しにラップされます。Table.selectAll拡張関数を呼び出して、データベースからすべてのデータを取得します。ArticlesオブジェクトはTableのサブクラスであるため、Exposed DSLメソッドを使用して操作します。
Table.selectAllはQueryのインスタンスを返すため、Articleインスタンスのリストを取得するには、各行のデータを手動で抽出し、データクラスに変換する必要があります。これは、ResultRowからArticleを構築するヘルパー関数resultRowToArticleを使用して行います。
ResultRowは、簡潔なget演算子を使用して特定のColumnに保存されたデータを取得する方法を提供し、配列やマップのようにブラケット構文([])を使用できるようにします。
Articles.idの型はColumn<Int>であり、これはExpressionインターフェースを実装しています。そのため、任意のカラムを式(expression)として渡すことができます。
記事を取得する
次に、1つの記事を返す関数を実装しましょう。
select関数は拡張ラムダを引数に取ります。このラムダ内の暗黙のレシーバーはSqlExpressionBuilder型です。この型を明示的に使用することはありませんが、クエリを構築するために使用するカラムに対する一連ের便利な操作が定義されています。比較(eq, less, greater)、算術演算(plus, times)、値が指定されたリストに含まれているかどうかの確認(inList, notInList)、値がnullかどうかの確認などが使用できます。
selectはQuery値のリストを返します。以前と同様に、それらを記事に変換します。このケースでは1つの記事であるはずなので、それを結果として返します。
新しい記事を追加する
テーブルに新しい記事を挿入するには、ラムダ引数を取るTable.insert関数を使用します。
このラムダ内で、どのカラムにどの値を設定するかを指定します。it引数はInsertStatement型であり、カラムと値を引数に取るset演算子を呼び出すことができます。
記事を編集する
既存の記事を更新するには、Table.updateを使用します。
記事を削除する
最後に、Table.deleteWhereを使用してデータベースから記事を削除します。
DAOFacadeの初期化
DAOFacadeのインスタンスを作成し、アプリケーションが開始される前にデータベースに挿入されるサンプル記事を追加しましょう。 DAOFacadeImpl.ktの最後に以下のコードを追加します。
ルートの更新
これで、実装したデータベース操作をルートハンドラー内で使用する準備が整いました。 plugins/Routing.ktファイルを開きます。 すべての記事を表示するには、getハンドラー内でdao.allArticlesを呼び出します。
新しい記事を投稿するには、post内でdao.addNewArticle関数を呼び出します。
表示および編集用の記事を取得するには、それぞれget("{id}")およびget("{id}/edit")内でdao.articleを使用します。
最後に、post("{id}")ハンドラーに移動し、dao.editArticleを使用して記事を更新し、dao.deleteArticleを使用して記事を削除します。
このチュートリアルの最終的なプロジェクトはこちらで確認できます: tutorial-website-interactive-persistence
アプリケーションの実行
ジャーナルアプリケーションが期待通りに動作するか見てみましょう。Application.kt内のfun main(...)の横にある Run ボタンを押すことでアプリケーションを実行できます。

IntelliJ IDEAがアプリケーションを起動し、数秒後にはアプリが実行中であることを示す確認メッセージが表示されます。
[main] INFO Application - Responding at http://0.0.0.0:8080ブラウザで http://localhost:8080/ を開き、記事の作成、編集、削除を試してみてください。記事は build/db.mv.db ファイルに保存されます。IntelliJ IDEAでは、Databaseツールウィンドウでこのファイルの内容を確認できます。

