Believe you can

If you can dream it, you can do it.

サーバサイドKotlinでKomapperを使ってみよう

Kotlin製ORマッパーのKomapperが5月に1.0のリリースされると聞いてSpringBoot上で試してみました。といってもサンプルソースも豊富なのでそれに毛が生えた程度です。。

CRUD

H2を利用してpersonテーブルを用意してCRUDを動かします。

create table person
(
    id    int GENERATED ALWAYS AS IDENTITY (START WITH 3) PRIMARY KEY,
    name  varchar(255) not null,
    email varchar(255) not null
);

エンティティ、マッピング定義

テーブルと対になるエンティティとPKなどの項目にアノテーションを定義して項目マッピングを行います。

data class Person(
    val id: Int? = null,
    val name: String,
    val email: String,
)

@KomapperEntityDef(Person::class)
data class PersonDef(
    @KomapperId
    @KomapperAutoIncrement
    val id: Nothing,
)

クエリ実行

QueryDSLを利用してSQLを実行します。メソッドチェーンでやりたいことをつなげていく感じなので直感的でわかりやすいですね。

@Service
class PersonService(
    private val database: JdbcDatabase
    ) {
    fun findAll(): List<Person> {
        return database.runQuery {
            val p = Meta.person
            QueryDsl.from(p).orderBy(p.id)
        }
    }

    fun findById(id: Int): Person {
        return database.runQuery {
            val p = Meta.person
            QueryDsl.from(p).where{ p.id eq id }.first()
        }
    }

    @Transactional
    fun insert(name: String, email: String): Person {
        return database.runQuery {
            val person = Person(name = name, email = email)
            val p = Meta.person
            QueryDsl.insert(p).single(person)
        }
    }

    @Transactional
    fun update(id: Int, name: String, email: String): Person {
        return database.runQuery {
            val person = findById(id)
            val p = Meta.person
            QueryDsl.update(p).single(person.copy(name = name, email = email))
        }
    }

    @Transactional
    fun delete(id: Int) {
        database.runQuery {
            val person = findById(id)
            val p = Meta.person
            QueryDsl.delete(p).single(person)
        }
    }
}

結合

テーブル結合をしたSQLも試してみます。チームテーブル、チームメンバーテーブルを用意して、personテーブルを含めた3テーブルの結合を行います。
DDLは次の通りです。

create table team
(
    id   int GENERATED ALWAYS AS IDENTITY (START WITH 3) PRIMARY KEY,
    name varchar(255) not null
);

create table team_member
(
    id        int GENERATED ALWAYS AS IDENTITY (START WITH 4) PRIMARY KEY,
    team_id  int not null,
    member_id int not null
);

エンティティとマッピング

data class Team(
    val id: Int? = null,
    val name: String,
)

@KomapperEntityDef(Team::class)
data class TeamDef(
    @KomapperId
    @KomapperAutoIncrement
    val id: Nothing,
)

data class TeamMember(
    val id: Int? = null,
    val teamId: Int,
    val memberId: Int,
)

@KomapperEntityDef(TeamMember::class)
data class TeamMemberDef(
    @KomapperId
    @KomapperAutoIncrement
    val id: Nothing,
)

left joinを行います。leftJoinメソッドで結合するテーブルを指定し、selectメソッドで取得するカラムを指定します。

@Service
class TeamService(
    private val database: JdbcDatabase
) {
    fun findById(id: Int): TeamDTO? {
        val g = Meta.team
        val m = Meta.teamMember
        val p = Meta.person
        val result = database.runQuery {
            QueryDsl.from(g)
                .leftJoin(m) { g.id eq m.teamId }
                .leftJoin(p) { m.memberId eq p.id }
                .where { g.id eq id }
                .orderBy(m.id)
                .select(g.id, g.name, p.id, p.name, p.email)
        }

        val members = result.map {
            Member(it[p.id]!!, it[p.name]!!, it[p.email]!!)
        }
        val member = result.first()
        val id = member[g.id]
        val name = member[g.name]

        return TeamDTO(id!!, name!!, members)
    }
}

完成したソースは↓です。

github.com

感想

SQLをメソッドチェーンで組み立てていくことができるので悩まずに実装できるのがいいですね。アノテーションプロセッサーを利用しているのでMetaで指定できるテーブルはコンパイルをしていないと怒られてしまうのは注意が必要です。(大した問題ではないが)

R2DBCに対応しているORマッパーは少ないので採用候補に上がるプロジェクトも多いんじゃないでしょうか。
基本機能を試した程度ですが、これから採用実績は増えていくこと間違いなしだと思います!!