Komapperでクエリ結果を任意のクラスにマッピング
はじめに
KomapperはサーバサイドKotlinのためのORMです。
この記事ではKomapper v1.16.0で導入したクエリ結果を任意のクラスにマッピングする機能について紹介します。
この機能は、Hibernateで言えばselect new、Jooqで言えばPOJOsに記載されている機能に近いです。
"DTO projection"というキーワードで検索すると関連する情報が得られると思います。
何が嬉しいのか?
右から左へデータを移し替えるだけのような単純なコードを排除できるのが利点です。
後述するKotlinのDSLを使って構築するクエリにおいてはタイプセーフなマッピングもできます。
この記事で行うこと
AUTHORテーブルとBOOKテーブルを結合した結果を、BookWithAuthorName
というクラスにマッピングする例を示します。
事前準備
データベースにはH2 Database Engineを使います。
データベース
この記事で利用するデータベースのER図は次のとおりです。
DDLとサンプルデータ
-- author テーブルの作成
CREATE TABLE author (
author_id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
birth_year INT,
nationality VARCHAR(50)
);
-- book テーブルの作成
CREATE TABLE book (
book_id INT PRIMARY KEY,
title VARCHAR(200),
author_id INT,
publication_year INT,
genre VARCHAR(100),
isbn VARCHAR(20),
FOREIGN KEY (author_id) REFERENCES author(author_id)
);
-- author テーブルへのデータ挿入
INSERT INTO author (author_id, first_name, last_name, birth_year, nationality) VALUES
(1, 'Haruki', 'Murakami', 1949, 'Japanese'),
(2, 'J.K.', 'Rowling', 1965, 'British'),
(3, 'George', 'Orwell', 1903, 'British');
-- book テーブルへのデータ挿入
INSERT INTO book (book_id, title, author_id, publication_year, genre, isbn) VALUES
(1, 'Kafka on the Shore', 1, 2002, 'Fiction', '978-1400079278'),
(2, 'Norwegian Wood', 1, 1987, 'Fiction', '978-0375704024'),
(3, 'Harry Potter and the Sorcerer''s Stone', 2, 1997, 'Fantasy', '978-0590353427'),
(4, '1984', 3, 1949, 'Dystopian', '978-0451524935');
エンティティクラス
テーブルに対応するクラス(Author
とBook
)および結果として取得するマッピング対象クラス(BookWithAuthorName
)を定義します。
クラス定義
@KomapperEntity
data class Author(
@KomapperId val authorId: Int,
val firstName: String,
val lastName: String,
val birthYear: Int,
val nationality: String
)
@KomapperEntity
data class Book(
@KomapperId val bookId: Int,
val title: String,
val authorId: Int,
val publicationYear: Int,
val genre: String,
val isbn: String
)
@KomapperEntity
@KomapperProjection
data class BookWithAuthorName(
@KomapperId val bookId: Int,
val title: String,
val authorName: String,
)
なお、BookWithAuthorName
にアノテーションを付与したくない(できない)場合、次のように記述することも可能です。
data class BookWithAuthorName(
val bookId: Int,
val title: String,
val authorName: String,
)
@KomapperEntityDef(BookWithAuthorName::class)
@KomapperProjection
data class BookWithAuthorNameDef(
@KomapperId val bookId: Nothing,
)
上記のコードにおける大事なポイントは@KomapperProjection
というアノテーションが付与されていることです。このアノテーションはマッピングのための拡張関数をコンパイル時に生成します。
拡張関数のデフォルトの名前はselectAs + エンティティクラスの単純名
であり、この例ではselectAsBookWithAuthorName
となります。
多くのクラスにアノテーションを付与するのが面倒な人のために、アノテーションプロセッサのオプションもあります。
クエリの例
Komapperは以下の2種類のクエリについて、クエリの結果を任意のクラスにマッピングできます。
- KotlinのDSLを使って構築するクエリ
- SQLテンプレートを使って構築するクエリ
KotlinのDSLを使って構築するクエリの場合
クエリの最後で拡張関数であるselectAsBookWithAuthorName
を呼び出します。この関数の引数はBookWithAuthorName
クラスに対応しており、名前つき引数を使ってBookWithAuthorName
クラスでどの値を受け取るかを指定できます。
// メタモデルの取得
val a = Meta.author
val b = Meta.book
// クエリの構築
val query = QueryDsl.from(b)
.innerJoin(a) { b.authorId eq a.authorId }
.orderBy(b.bookId)
.selectAsBookWithAuthorName(
bookId = b.bookId,
title = b.title,
authorName = concat(concat(a.firstName, " "), a.lastName)
)
// クエリの実行
val list: List<BookWithAuthorName> = db.runQuery(query)
selectAsBookWithAuthorName
の呼び出しにおいて引数の数や型のチェックが行われるため、タイプセーフにマッピングを実現できていると言えます。
発行されるSQLは次のものになります。
select
t0_.book_id,
t0_.title,
(concat((concat(t1_.first_name, ' ')), t1_.last_name))
from
book as t0_
inner join
author as t1_
on (t0_.author_id = t1_.author_id)
order by
t0_.book_id asc
SELECT句にはselectAsBookWithAuthorName
の引数で指定したプロパティに対応するカラムのみが登場しており、SQL上でカラムの選択(射影)が行われていることがわかります。
SQLテンプレートを使って構築するクエリの場合
次にSQLテンプレートを利用する場合を見ていきましょう。
クエリの最後で拡張関数selectAsBookWithAuthorName
を呼び出せばマッピングが実現できます(SQLテンプレートを使う場合、Author
クラスやBook
クラスおよびそれらのメタモデルは不要です)。
// SQLテンプレートを記述
val sql = """
select
t0_.book_id,
t0_.title,
(concat((concat(t1_.first_name, ' ')), t1_.last_name))
from
book as t0_
inner join
author as t1_
on (t0_.author_id = t1_.author_id)
order by
t0_.book_id asc
""".trimIndent()
// クエリの構築
val query = QueryDsl.fromTemplate(sql).selectAsBookWithAuthorName()
// クエリの実行
val list: List<BookWithAuthorName> = db.runQuery(query)
上記の例では、SQLのSELECTリストのカラムの順序をBookWithAuthorName
クラスのコンストラクタのパラメーターの順序に合わせる必要がありますが、パラメータが多い場合、順序を保つのが難しいこともあるでしょう。
その場合、次のようにselectAsBookWithAuthorName
関数に ProjectionType.NAME
を渡すことでSELECTリストのカラム名とコンストラクタのパラメータ名をマッピングすることも可能です。
// SQLテンプレートを記述
val sql = """
select
t0_.title as title,
(concat((concat(t1_.first_name, ' ')), t1_.last_name)) as author_name,
t0_.book_id as book_id,
from
book as t0_
inner join
author as t1_
on (t0_.author_id = t1_.author_id)
order by
t0_.book_id asc
""".trimIndent()
// クエリの構築
val query = QueryDsl.fromTemplate(sql).selectAsBookWithAuthorName(ProjectionType.NAME)
// クエリの実行
val list: List<BookWithAuthorName> = db.runQuery(query)
上述のコード内のSQLテンプレートでは、SELECTリストの順番を入れ替えていますが、その代わりカラムに別名(BookWithAuthorName
クラスのプロパティに対応する名前)をつけています。
まとめ
Komapperのクエリ結果を任意のクラスにマッピングする機能を紹介しました。
質問や改善案などあれば、本記事のコメント欄やKotlinのkomapperチャネルなどでお気軽にコメントください。
Discussion