エンティティクラス

概要

Komapperでは、データベースのテーブルに対応するKotlinクラスをエンティティクラスと呼びます。

エンティティクラスをテーブルにマッピングさせるには別途アノテーションを用いたマッピング定義が必要です。

マッピング定義はコンパイル時に解析されその結果がメタモデルとなります。 メタモデルはクエリの構築や実行で利用されます。

エンティティクラスの定義

エンティティクラスは次の要件を満たさなければいけません。

  • Data Classである
  • 可視性がprivateでない
  • 型パラメータを持っていない

例えば、次のようなテーブル定義があるとします。

create table if not exists ADDRESS (
  ADDRESS_ID integer not null auto_increment,
  STREET varchar(500) not null,
  VERSION integer not null,
  CREATED_AT timestamp,
  UPDATED_AT timestamp,
  constraint pk_ADDRESS primary key(ADDRESS_ID)
);

上記のテーブル定義に対応するエンティティクラス定義は次のようになります。

data class Address(
  val id: Int = 0,
  val street: String,
  val version: Int = 0,
  val createdAt: LocalDateTime? = null,
  val updatedAt: LocalDateTime? = null,
)

プロパティとカラムの間における型の対応関係については データ型 を参照ください。

マッピング定義

マッピング定義の作成方法は2種類あります。

  • エンティティクラス自身がマッピング定義を持つ方法(セルフマッピング)
  • エンティティクラスとは別にエンティティ定義クラスを作成する方法(分離マッピング)

同一のエンティティクラスに対して1つの方法のみ適用できます。

セルフマッピング

このときエンティティクラスは前のセクションで説明した要件に加えて次の条件を満たさなければいけません。

  • @KomapperEntityで注釈される

例えば、前のセクションで示したAddressクラスにこの方法を適用すると次のように変更できます。

@KomapperEntity
data class Address(
  @KomapperId
  @KomapperAutoIncrement
  @KomapperColumn(name = "ADDRESS_ID")
  val id: Int = 0,
  val street: String,
  @KomapperVersion
  val version: Int = 0,
  @KomapperCreatedAt
  val createdAt: LocalDateTime? = null,
  @KomapperUpdatedAt
  val updatedAt: LocalDateTime? = null,
)

分離マッピング

エンティティ定義クラスは次の要件を満たさなければいけません。

  • Data Classである
  • 可視性がprivateでない
  • 型パラメータを持っていない
  • @KomapperEntityDefで注釈され引数でエンティティクラスを受け取る
  • エンティティクラスに定義されたプロパティと異なる名前のプロパティを持たない

例えば、前のセクションで示したAddressクラスに対するエンティティ定義クラスは次のように記述できます。

@KomapperEntityDef(Address::class)
data class AddressDef(
  @KomapperId
  @KomapperAutoIncrement
  @KomapperColumn(name = "ADDRESS_ID")
  val id: Nothing,
  @KomapperVersion
  val version: Nothing,
  @KomapperCreatedAt
  val createdAt: Nothing,
  @KomapperUpdatedAt
  val updatedAt: Nothing,
)

エンティティ定義クラスは、参照するエンティティクラスに定義された同名のプロパティに対し様々な設定ができます。 定義されないプロパティに対してはデフォルトのマッピング定義が適用されます。 上記の例ではエンティティクラスに登場するstreetプロパティがエンティティ定義クラスには登場しませんが、 streetプロパティにはテーブル上のSTREETカラムにマッピングされます。

エンティティ定義クラスのプロパティの型に制約はありません。上記の例ではNothingを使っています。

メタモデル

マッピング定義からはorg.komapper.core.dsl.metamodel.EntityMetamodelのインターフェースを実装する形でメタモデルが生成されます。

生成されたメタモデルはorg.komapper.core.dsl.Metaオブジェクトの拡張プロパティとなります。 アプリケーションではこの拡張プロパティを使ってクエリを組み立てられます。

// get a generated metamodel
val a = Meta.address

// define a query
val query = QueryDsl.from(e).where { a.street eq "STREET 101" }.orderBy(a.id)

aliases

上述の例では拡張プロパティの名前はaddressですが、これは@KomapperEntity@KomapperEntityDefaliasesプロパティで変更できます。

@KomapperEntity(aliases = ["addr"])
data class Address(
  ...
)

aliasesプロパティには複数の名前を指定できます。 その際、名前ごとに異なるインスタンスとして公開されます。 複数の異なるインスタンスが必要となる主なユースケースは自己結合やサブクエリです。

@KomapperEntity(aliases = ["employee", "manager"])
data class Employee(
  ...
)

例えば、マネージャーの一覧を取得するには上記のように複数の名前をつけた上で以下のようなクエリを作ります。

val e = Meta.employee
val m = Meta.manager
val query: Query<List<Employee>> = QueryDsl.from(m)
  .distinct()
  .innerJoin(e) {
    m.employeeId eq e.managerId
  }

なお、事前に名前を持ったメタモデルを定義しない場合でも、clone関数を使えば同じことが実現可能です。

val e = Meta.employee
val m = e.clone()
val query: Query<List<Employee>> = QueryDsl.from(m)
  .distinct()
  .innerJoin(e) {
    m.employeeId eq e.managerId
  }

clone

clone関数を使って既存のメタモデルを基に別のメタモデルを生成できます。 主なユースケースは、データ構造が同じで名前だけが異なるテーブルにデータをコピーする場合です。

val a = Meta.address
val archive = a.clone(table = "ADDRESS_ARCHIVE")
val query = QueryDsl.insert(archive).select {
  QueryDsl.from(a).where { a.id between 1..5 }
}

cloneしたメタモデルを他のメタモデルと同様に公開したい場合は、 オブジェクトでインスタンスを保持した上でMetaオブジェクトの拡張プロパティを定義してください。

object MetamodelHolder {
  private val _addressArchive = Meta.address.clone(table = "ADDRESS_ARCHIVE")
  val Meta.addressArchive get() = _addressArchive
}

define

define関数を使ってメタモデルに対しデフォルトのWHERE句を定義できます。 あるメタモデルを使う際に必ず同じ検索条件を用いたいケースで便利です。

object MetamodelHolder {
  private val _bostonOnly = Meta.department.define { d ->
    where {
      d.location eq "BOSTON"
    }
  }
  val Meta.bostonOnly get() = _bostonOnly
}

上記のbostonOnlyメタモデルを利用すると、 クエリで検索条件を指定しないにも関わらずWHERE句をもったSQLが生成されます。

val d = Meta.bostonOnly
val query = QueryDsl.from(d)
/*
select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT as t0_ where t0_.LOCATION = ?
*/

WHERE句を持つクエリを組み立てた場合は検索条件がAND演算子で連結されます。

val d = Meta.bostonOnly
val query = QueryDsl.from(d).where { d.departmentNo greaterEq 0 }
/*
select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT as t0_ where t0_.LOCATION = ? and t0_.DEPARTMENT_NO >= ?
*/

defineしたメタモデルを結合先としてクエリに含めた場合もこの機能は有効です。

val e = Meta.employee
val d = Meta.bostonOnly
val query = QueryDsl.from(e).innerJoin(d) {
  e.departmentId eq d.departmentId
}
/*
select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE, t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION from EMPLOYEE as t0_ inner join DEPARTMENT as t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID) where t1_.LOCATION = ?
*/

SELECT文だけでなくUPDATE文やDELETE文でも有効です。

val d = Meta.bostonOnly
val query = QueryDsl.delete(d).all()
/*
delete from DEPARTMENT as t0_ where t0_.LOCATION = ?
*/

デフォルトのWHERE句にパラメータを渡したい場合は、拡張関数として定義することもできます。 ただし、メタモデルが毎回異なるインスタンスとなることは注意してください。

object MetamodelHolder {
    fun Meta.locationSpecificDepartment(value: String) = Meta.department.define { d ->
        where {
            d.location eq value
        }
    }
}

上記の拡張関数を呼び出す例です。

val d = Meta.locationSpecificDepartment("NEW YORK")
val query = QueryDsl.from(d)
val list = db.runQuery { query }

クラスに付与するアノテーション一覧

ここで説明するアノテーションは全てorg.komapper.annotationパッケージに属します。

@KomapperEntity

エンティティクラスがマッピング定義を持つことを表します。 aliasesプロパティを持ちます。

@KomapperEntity(aliases = ["addr"])
data class Address(
  ...
)

aliasesについては aliases を参照ください。

@KomapperEntityDef

エンティティ定義クラスであることを表します。 entityプロパティやaliasesプロパティを指定できます。

@KomapperEntityDef(entity = Address::class, aliases = ["addr"])
data class AddressDef(
  ...
)

aliasesについては aliases を参照ください。

@KomapperTable

エンティティクラスとマッピングするテーブルの名前を明示的に指定します。

@KomapperEntityDef(Address::class)
@KomapperTable("ADDRESS", schema = "ACCOUNT", alwaysQuote = true)
data class AddressDef(
  ...
)

catalogプロパティやschemaプロパティにはテーブルが属するカタログやスキーマの名前を指定できます。

alwaysQuoteプロパティにtrueを設定すると生成されるSQLの識別子が引用符で囲まれます。

このアノテーションでテーブルの名前を指定しない場合、アノテーション処理のkomapper.namingStrategyオプションに従って名前が解決されます。

アノテーションプロセッシングのオプションも参照ください。

プロパティに付与するアノテーション一覧

ここで説明するアノテーションは全てorg.komapper.annotationパッケージに属します。

@KomapperId

プライマリーキーであることを表します。 複合プライマリキーを表すために、 1つのエンティティクラス内に複数指定することもできます。

@KomapperSequence

プライマリキーがデータベースのシーケンスで生成されることを表します。 必ず@KomapperIdと一緒に付与する必要があります。

このアノテーションを付与するプロパティの型は次のいずれかでなければいけません。

  • Int
  • Long
  • UInt
  • 上述の型をプロパティとして持つValue Class
@KomapperId
@KomapperSequence(name = "ADDRESS_SEQ", startWith = 1, incrementBy = 100)
val id: Int

nameプロパティにはシーケンスの名前を指定しなければいけません。カタログやスキーマの指定もできます。

startWithプロパティとincrementByプロパティの値はシーケンス定義に合わせなければいけません。

alwaysQuoteプロパティにtrueを設定すると生成されるSQLの識別子が引用符で囲まれます。

@KomapperAutoIncrement

プライマリーキーがデータベースの自動インクリメント機能で生成されることを表します。 必ず@KomapperIdと一緒に付与する必要があります。

このアノテーションを付与するプロパティの型は次のいずれかでなければいけません。

  • Int
  • Long
  • UInt
  • 上述の型をプロパティとして持つValue Class

@KomapperVersion

楽観的排他制御に使われるバージョン番号であることを表します。

このアノテーションを付与すると、 UpdateクエリDeleteクエリで楽観的排他制御が行われます。

このアノテーションを付与するプロパティの型は次のいずれかでなければいけません。

  • Int
  • Long
  • UInt
  • 上述の型をプロパティとして持つValue Class

@KomapperCreatedAt

生成時のタイムスタンプであることを表します。

このアノテーションを付与すると、 Insertクエリにてタイムスタンプがプロパティに設定されます。

このアノテーションを付与するプロパティの型は次のいずれかでなければいけません。

  • java.time.LocalDateTime
  • java.time.OffsetDateTime
  • 上述の型をプロパティとして持つValue Class

@KomapperUpdatedAt

更新時のタイムスタンプであることを表します。

このアノテーションを付与すると、 InsertクエリUpdateクエリにてタイムスタンプがプロパティに設定されます。

このアノテーションを付与するプロパティの型は次のいずれかでなければいけません。

  • java.time.LocalDateTime
  • java.time.OffsetDateTime
  • 上述の型をプロパティとして持つValue Class

@KomapperEnum

Enum型のプロパティに対し、プロパティとカラムのマッピング方法を明示的に指定します。

@KomapperEnum(EnumType.ORDINAL)
val color: Nothing // このcolorプロパティがColorというEnum型に対応すると想定してください

@KomapperEnumtypeプロパティには次のいずれかを指定できます。

EnumType.NAME
Enumクラスのnameプロパティを文字列型のカラムにマッピングする。
EnumType.ORDINAL
Enumクラスのordinalプロパティを整数型のカラムにマッピングする。

Enum型のプロパティに対して@KomapperEnumを指定しない場合、 アノテーション処理のkomapper.enumStrategyオプションに従ってマッピング方法が解決されます。

アノテーションプロセッシングのオプションも参照ください。

@KomapperColumn

プロパティとマッピングするカラムの名前を明示的に指定します。

@KomapperColumn(name = "ADDRESS_ID", alwaysQuote = true, masking = true)
val id: Nothing

alwaysQuoteプロパティにtrueを設定すると生成されるSQLの識別子が引用符で囲まれます。

maskingプロパティにtrueを設定すると、 ログの中で対応するデータがマスキングされます。

このアノテーションでカラムの名前を指定しない場合、アノテーション処理のkomapper.namingStrategyオプションに従って名前が解決されます。

アノテーションプロセッシングのオプションも参照ください。

@KomapperIgnore

マッピングの対象外であることを表します。

@KomapperEmbeddedId

複合プライマリキーの組込バリューを表します。

data class EmoloyeeId(val id1: Int, val id2: String)

@KomapperEntity
data Employee(@KomapperEmbeddedId val id: EmoloyeeId, val: name: String)

@KomapperEmbedded

組込バリューを表します。

data class Money(val amount: BigDecimal, val currency: String)

@KomapperEntity
data Employee(@KomapperId val id: Int, @KomapperEmbedded val: salary: Money)

@KomapperEnumOverride

組込バリュー内のEnum型のプロパティに対し、 @KomapperEnumを適用します。

enum class Currency { JPY, USD }

data class Money(val amount: BigDecimal, val currency: Currency)

@KomapperEntity
data Employee( 
  @KomapperId
  val id: Int,
  @KomapperEmbedded
  @KomapperEnumOverrde("currency", KomapperEnum(EnumType.ORDINAL))
  val: salary: Money
)

@KomapperColumnOverride

組込バリュー内のプロパティに対し、 @KomapperColumnを適用します。

data class Money(val amount: BigDecimal, val currency: String)

@KomapperEntity
data Employee( 
  @KomapperId
  val id: Int,
  @KomapperEmbedded
  @KomapperColumnOverrde("amount", KomapperColumn("SALARY_AMOUNT"))
  @KomapperColumnOverrde("currency", KomapperColumn("SALARY_CURRENCY"))
  val: salary: Money
)
最終更新 August 10, 2022: Support for Embedded Value pattern (eb08782)