🦘

[Kotlin] KomapperのEmbedded Valueサポート

2022/08/11に公開

はじめに

Embedded Value組込みバリュー)というデザインパターンをご存知でしょうか?昨日リリースしたKomapper 1.3.0でこの機能をサポートしたので紹介をします。

マッピング例

冒頭でリンクしたEmbedded Valueのページに載っている図の例をそのまま使ってクラスとテーブルをマッピングしてみます。

aggregateMappingSketch

図の左側のEmploymentクラスは次のように表せます。Employmentから参照しているPersonDataRangeMoneyも含めて全て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として扱うPersonDataRangeMoneyに対応するプロパティには@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