[Kotlin] Komapperにおける関連ナビゲーション
はじめに
まずはKomapperについて簡単に紹介します。
KomapperはサーバーサイドKotlinのためのORMライブラリです。JDBCとR2DBCの2つの接続方式に対応しているのが大きな特徴ですが、その他にもコンパイル時にコード生成をしたりvalue classに対応していたりとユニークな機能もあります。
詳細はWebサイトをご覧ください。
この記事では、ORMとは切っても切り離せない「エンティティ間の関連(Association)ナビゲーション」についてKomapperならではの特徴を説明します。
前提
ER図
以下の3つのテーブルがあるとします。
データ
それぞれのテーブルに格納されているデータは次の通りです。
DEPARTMENT
DEPARTMENT_ID | DEPARTMENT_NAME |
---|---|
1 | ACCOUNTING |
2 | RESEARCH |
3 | SALES |
4 | OPERATIONS |
EMPLOYEE
EMPLOYEE_ID | EMPLOYEE_NAME | SALARY | DEPARTMENT_ID | ADDRESS_ID |
---|---|---|---|---|
1 | SMITH | 800.00 | 2 | 1 |
2 | ALLEN | 1600.00 | 3 | 2 |
3 | WARD | 1250.00 | 3 | 3 |
4 | JONES | 2975.00 | 2 | 4 |
5 | MARTIN | 1250.00 | 3 | 5 |
6 | BLAKE | 2850.00 | 3 | 6 |
7 | CLARK | 2450.00 | 1 | 7 |
8 | SCOTT | 3000.00 | 2 | 8 |
9 | KING | 5000.00 | 1 | 9 |
10 | TURNER | 1500.00 | 3 | 10 |
ADDRESS
ADDRESS_ID | STREET |
---|---|
1 | STREET 1 |
2 | STREET 2 |
3 | STREET 3 |
4 | STREET 4 |
5 | STREET 5 |
6 | STREET 6 |
7 | STREET 7 |
8 | STREET 8 |
9 | STREET 9 |
10 | STREET 10 |
Komapper用エンティティクラス
テーブルにマッピングするエンティティクラスです。通常のdata classにKomapperのアノテーションを付与しています。
@KomapperEntity
data class Address(
@KomapperId val addressId: Int,
val street: String,
)
@KomapperEntity
data class Employee(
@KomapperId val employeeId: Int,
val employeeName: String,
val salary: BigDecimal,
val addressId: Int,
val departmentId: Int,
)
@KomapperEntity
data class Department(
@KomapperId val departmentId: Int,
val departmentName: String,
)
バージョン情報
- Kotlin 1.7.0
- Java 11
- Komapper 1.1.2
一般的なORMにおける関連ナビゲーション
一般的なJavaやKotlinのORMではエンティティのプロパティにアクセスして関連を辿ることができます。例えば、JPAを使った場合次のように記述できるでしょう。
val departments: List<Department> = query.getResultList()
for (department in departments) {
println("departmentName: ${department.departmentName}")
for (employee in department.employees) {
val address = employee.address
println(" employeeName: ${employee.employeeName}, salary: ${employee.salary}, street: ${address?.street}")
}
}
このような機能は一見便利ですが、N+1問題を防ぐために多少煩雑な考慮(fetch戦略や関連のマッピング等)をせねばならず、見た目よりは厄介と言えます。
Komapperの関連ナビゲーション
Komapperでは別のアプローチを採ります。
Komapperはデータを1度で取得し、そのデータをストアと呼ばれるところに格納します。その後、1対1や1対多の関連を表すMapを求めに応じてストアから返します。そのため、N+1問題は起きません。
完全なコードではありませんが、上述の一般的なORMのサンプルコードに対応させる形で抜粋すると次のように記述できます(一部わかりやすさのために型を明示していますが必須ではありません)。
val store: EntityStore = ...
val departmentEmployees: Map<Department, Set<Employee>> = store.oneToMany(d, e)
val employeeAddress: Map<Employee, Address?> = store.oneToOne(e, a)
for ((department, employees) in departmentEmployees) {
println("departmentName: ${department.departmentName}")
for (employee in employees) {
val address = employeeAddress[employee]
println(" employeeName: ${employee.employeeName}, salary: ${employee.salary}, street: ${address?.street}")
}
}
ストアが保持するデータのイメージです。
ストアがこのようなデータを保持することを前提にして上述のコードを動かすと次のような出力になります。
departmentName: RESEARCH
employeeName: JONES, salary: 2975.00, street: STREET 4
employeeName: SCOTT, salary: 3000.00, street: STREET 8
departmentName: SALES
employeeName: BLAKE, salary: 2850.00, street: STREET 6
departmentName: ACCOUNTING
employeeName: CLARK, salary: 2450.00, street: STREET 7
employeeName: KING, salary: 5000.00, street: STREET 9
上述のコードはdepartmentエンティティからemployeeエンティティを経由してaddressエンティティにアクセスしていますが、departmentエンティティとaddressエンティティの関連のみを抽出することもできます。
val departmentAddresses: Map<Department, Set<Address>> = store.oneToMany(d, a)
for ((department, addresses) in departmentAddresses) {
println("departmentName: ${department.departmentName}")
for (address in addresses) {
println(" street: ${address.street}")
}
}
このコードを実施すると次のような出力が得られます。
departmentName: RESEARCH
street: STREET 4
street: STREET 8
departmentName: SALES
street: STREET 6
departmentName: ACCOUNTING
street: STREET 7
street: STREET 9
つまり、ER図上では直接つながっていなくてもデータを関連づけて扱うことが容易です。
ストアを取得するためのクエリ
先ほど、「Komapperは、データを1度で取得しストアと呼ばれるところに格納します」と述べました。ではストアはどのように入手するのでしょうか?
ストアは以下のようにクエリを実行することで入手できます。
val d = Meta.department
val e = Meta.employee
val a = Meta.address
val query = QueryDsl.from(e)
.innerJoin(d) { e.departmentId eq d.departmentId }
.innerJoin(a) { e.addressId eq a.addressId }
.where { e.salary greaterEq BigDecimal(2_000) }
.includeAll()
val store: EntityStore = db.runQuery(query)
ポイントはクエリ構築の最後でincludeAll()
を呼ぶことです。この呼び出しにより結合したテーブルすべてのカラムのデータを取得できます。includeAll()
を呼ばない場合、取得できるのはfrom()
に指定したテーブルのデータのみとなり関連を辿れません。
おわりに
エンティティの関連ナビゲーションについてKomapperのアプローチを紹介しました。
ご興味あればぜひKomapperをお試しください。まずはクイックスタートから始めるのがおすすめです。
Discussion