[Kotlin] KomapperのEmbedded Valueサポート
はじめに
Embedded Value(組込みバリュー)というデザインパターンをご存知でしょうか?昨日リリースしたKomapper 1.3.0でこの機能をサポートしたので紹介をします。
マッピング例
冒頭でリンクしたEmbedded Valueのページに載っている図の例をそのまま使ってクラスとテーブルをマッピングしてみます。
図の左側のEmployment
クラスは次のように表せます。Employment
から参照しているPerson
、DataRange
、Money
も含めて全てKotlinのデータクラスで表現します。
data class Employment(
val id: Int,
val person: Person,
val period: DataRange,
val salary: Money,
)
data class Person(
val personId: Int
)
data class DataRange(
val start: LocalDate,
val end: LocalDate
)
data class Money(
val amount: BigDecimal,
val currency: String
)
図の右側のEmployments
テーブルのDDLは次のとおりです。データベースにはH2 Database Engineを使いますが、endは予約語なのでクォートしてエスケープしています。
create table if not exists Employments (
id integer not null,
personId integer not null,
start date not null,
"end" date not null,
salaryAmount bigint not null,
salaryCurrency varchar(500) not null,
constraint pk_Employments primary key(id)
)
Employment
クラスとEmployments
テーブルとのマッピングは次のように記述できます。Embedded Valueとして扱うPerson
、DataRange
、Money
に対応するプロパティには@KomapperEmbedded
を注釈します。Embedded Valueが表現するカラムの内、カラムの名前を上書きしたり予約語のカラムをクォートしたりするために適宜@KomapperColumnOverride
を使います。
@KomapperEntityDef(Employment::class)
@KomapperTable("Employments")
data class EmploymentDef(
@KomapperId
val id: Nothing,
@KomapperEmbedded
val person: Nothing,
@KomapperEmbedded
@KomapperColumnOverride("end", KomapperColumn(alwaysQuote = true))
val period: Nothing,
@KomapperEmbedded
@KomapperColumnOverride("amount", KomapperColumn("salaryAmount"))
@KomapperColumnOverride("currency", KomapperColumn("salaryCurrency"))
val salary: Nothing,
)
データの追加と選択の実行例
上述のクラスやマッピング使って実際にデータベースアクセスを実行してみます。
fun main() {
val db = JdbcDatabase("jdbc:h2:mem:example;DB_CLOSE_DELAY=-1")
val e = Meta.employment
db.withTransaction {
// スキーマの作成
db.runQuery {
QueryDsl.create(e)
}
// インスタンスの作成
val employment1 = Employment(
id = 1,
person = Person(1),
period = DataRange(LocalDate.parse("2001-01-01"), LocalDate.parse("2020-12-31")),
salary = Money(BigDecimal(300_000), "JPY")
)
val employment2 = Employment(
id = 2,
person = Person(2),
period = DataRange(LocalDate.parse("2010-08-01"), LocalDate.parse("2022-07-31")),
salary = Money(BigDecimal(400_000), "JPY")
)
// 追加
db.runQuery {
// insert into Employments (id, personId, start, "end", salaryAmount, salaryCurrency) values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
QueryDsl.insert(e).multiple(employment1, employment2)
}
// 主キーによる検索
val single = db.runQuery {
// select t0_.id, t0_.personId, t0_.start, t0_."end", t0_.salaryAmount, t0_.salaryCurrency from Employments as t0_ where t0_.id = ?
QueryDsl.from(e).where { e.id eq 1 }.single()
}
println("主キーによる検索:\n$single")
// salaryによる検索
val list1 = db.runQuery {
// select t0_.id, t0_.personId, t0_.start, t0_."end", t0_.salaryAmount, t0_.salaryCurrency from Employments as t0_ where t0_.salaryAmount = ? and t0_.salaryCurrency = ?
QueryDsl.from(e).where { e.salary eq Money(BigDecimal(400_000), "JPY") }.orderBy(e.id)
}
println("salaryによる検索:\n${list1.joinToString("\n")}")
// salaryAmountによる検索
val list2 = db.runQuery {
// select t0_.id, t0_.personId, t0_.start, t0_."end", t0_.salaryAmount, t0_.salaryCurrency from Employments as t0_ where t0_.salaryAmount >= ?
QueryDsl.from(e).where { e.salary.amount greaterEq BigDecimal(300_000) }.orderBy(e.id)
}
println("salaryAmountによる検索:\n${list2.joinToString("\n")}")
}
}
出力結果は次のとおりです。
主キーによる検索:
Employment(id=1, person=Person(personId=1), period=DataRange(start=2001-01-01, end=2020-12-31), salary=Money(amount=300000, currency=JPY))
salaryによる検索:
Employment(id=2, person=Person(personId=2), period=DataRange(start=2010-08-01, end=2022-07-31), salary=Money(amount=400000, currency=JPY))
salaryAmountによる検索:
Employment(id=1, person=Person(personId=1), period=DataRange(start=2001-01-01, end=2020-12-31), salary=Money(amount=300000, currency=JPY))
Employment(id=2, person=Person(personId=2), period=DataRange(start=2010-08-01, end=2022-07-31), salary=Money(amount=400000, currency=JPY))
ポイントはEmbedded Valueを検索条件に指定できるところです。「salaryによる検索」のクエリをみてください。Money
オブジェクトを検索条件に指定しています。
QueryDsl.from(e).where { e.salary eq Money(BigDecimal(400_000), "JPY") }.orderBy(e.id)
一方で、Embedded Valueのプロパティを使った検索もできます。「salaryAmountによる検索」のクエリをみてください。ここではMoney
ではなくBigDecimal
オブジェクトを指定しています。
QueryDsl.from(e).where { e.salary.amount greaterEq BigDecimal(300_000) }.orderBy(e.id)
おまけ - アドバンストな使い方
標準ライブラリのデータクラスの利用
Embedded Valuesにはデータクラスであれば何でも指定可能です。例えば、今回のでサンプルコードのMoney
の代わりにKotlinの標準ライブラリに含まれるPair
を使うこともできます。そのとき、Employment
クラスやEmployementDef
クラスは次のように表現できます。
data class Employment(
val id: Int,
val person: Person,
val period: DataRange,
val salary: Pair<BigDecimal, String>,
)
@KomapperEntityDef(Employment::class)
@KomapperTable("Employments")
data class EmploymentDef(
@KomapperId
val id: Nothing,
@KomapperEmbedded
val person: Nothing,
@KomapperEmbedded
@KomapperColumnOverride("end", KomapperColumn(alwaysQuote = true))
val period: Nothing,
@KomapperEmbedded
@KomapperColumnOverride("first", KomapperColumn("salaryAmount"))
@KomapperColumnOverride("second", KomapperColumn("salaryCurrency"))
val salary: Nothing,
)
「salaryによる検索」のクエリは次のようになります。
QueryDsl.from(e).where { e.salary eq (BigDecimal(400_000) to "JPY") }.orderBy(e.id)
typealiasの利用
さらに言うと、Embedded Valuesにはデータクラスのtypealiasを指定することも可能になっています。そのとき、Employment
クラスは次のように表現できます。
typealias Salary = Pair<BigDecimal, String>
data class Employment(
val id: Int,
val person: Person,
val period: DataRange,
val salary: Salary,
)
EmployementDef
クラスや「salaryによる検索」のクエリの例は「標準ライブラリのデータクラスの利用」で示したものと同じになります。
おわりに
KomapperによるEmbedded Valueのサポートを紹介しました。
私はKomapperの作成者として利用者の声を聞きつつこんな感じだったら使いやすいのでは?と想像しながら実装をしていますが、役立つものを作れているのかなかなか確証を得られないでいます。ぜひフィードバックいただけたら嬉しいです。
Discussion