Kotlin公開フレームワークのガイド

1前書き

このチュートリアルでは、https://github.com/JetBrains/Exposed[Exposed]を使用してリレーショナルデータベースをクエリする方法について説明します。

公開されているのは、JetBrainsによって開発されたオープンソースライブラリ(Apacheライセンス)です。これは、いくつかのリレーショナルデータベースの実装に慣用的なKotlin APIを提供しながら、データベースベンダー間の違いを取り除きます。

  • Exposedは、SQL上のハイレベルDSLと軽量のORM(Object-Relational Mapping)の両方として使用することができます** したがって、このチュートリアルでは両方の使用法について説明します。

2公開されたフレームワーク設定

Maven Centralではまだ公開されていません。専用のリポジトリを使用する必要があります。

<repositories>
    <repository>
        <id>exposed</id>
        <name>exposed</name>
        <url>https://dl.bintray.com/kotlin/exposed</url>
    </repository>
</repositories>

その後、ライブラリを含めることができます。

<dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed</artifactId>
    <version>0.10.4</version>
</dependency>

また、次のセクションでは、メモリ内のH2データベースを使用した例を示します。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
</dependency>

Exposed の最新版とBintrayの最新版https://search.maven.org/search ?q = g:com.h2データベース%20a:Maven Central上のh2[H2]。

3データベースへの接続

データベース接続を Database クラスで定義します。

Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

名前付きパラメータとして user password を指定することもできます。

Database.connect(
  "jdbc:h2:mem:test", driver = "org.h2.Driver",
  user = "myself", password = "secret")
  • connect を起動してもDBへの接続がすぐに確立されるわけではありません。

3.1. 追加パラメータ

他の接続パラメータを提供する必要がある場合は、データベース接続の取得を完全に制御できる connect メソッドの別のオーバーロードを使用します。

Database.connect({ DriverManager.getConnection("jdbc:h2:mem:test;MODE=MySQL") })

このバージョンの connect にはクロージャパラメータが必要です。データベースへの新しい接続が必要になるたびに、Exposedはクロージャを呼び出します

3.2. DataSource を使用する

代わりに、エンタープライズアプリケーションの場合のように DataSource を使用してデータベースに接続する場合(たとえば、接続プーリングの恩恵を受けるため)、適切な connect オーバーロードを使用できます。

Database.connect(datasource)

4トランザクションを開く

Exposedのすべてのデータベース操作にはアクティブなトランザクションが必要です。

transaction メソッドはクロージャを取り、アクティブなトランザクションでそれを呼び出します。

transaction {
   //Do cool stuff
}

__transaction __は、クロージャが返すものは何でも返します。その後、Exposedはブロックの実行が終了すると自動的にトランザクションを閉じます。

4.1. コミットとロールバック

  • __transaction __blockが正常に終了すると、Exposedはトランザクションをコミットします。代わりに、クロージャが例外をスローして終了すると、フレームワークはトランザクションをロールバックします。

手動でトランザクションをコミットまたはロールバックすることもできます。 Kotlinの魔法のおかげで、 transaction に提供したクロージャーは実際には Transaction クラスのインスタンスです。

したがって、 commit rollback メソッドを利用できます。

transaction {
   //Do some stuff
    commit()
   //Do other stuff
}

4.2. ロギングステートメント

フレームワークを学んだりデバッグしたりする際には、Exposedがデータベースに送るSQLステートメントやクエリを調べるのが役に立つかもしれません。

アクティブなトランザクションにこのようなロガーを簡単に追加できます。

transaction {
    addLogger(StdOutSqlLogger)
   //Do stuff
}

5テーブルの定義

通常、Exposedでは、生のSQL文字列と名前を扱うことはしません。

代わりに、高レベルのDSLを使用して、テーブル、列、キー、関係などを定義します。

各テーブルを Table クラスのインスタンスで表します。

object StarWarsFilms : Table()

Exposedは自動的にクラス名からテーブルの名前を計算しますが、明示的な名前を指定することもできます。

object StarWarsFilms : Table("STAR__WARS__FILMS")

5.1. 列

テーブルは列なしでは意味がありません。テーブルクラスのプロパティとして列を定義します。

object StarWarsFilms : Table() {
    val id = integer("id").autoIncrement().primaryKey()
    val sequelId = integer("sequel__id").uniqueIndex()
    val name = varchar("name", 50)
    val director = varchar("director", 50)
}

Kotlinが私たちのためにそれらを推論することができるので、我々は簡潔のためにタイプを省略しました。

とにかく、各列は __Column <T> __型であり、名前、型、そしておそらく型パラメータを持ちます。

5.2. 主キー

前のセクションの例からわかるように、** 流暢なAPIを使用して簡単にインデックスと主キーを定義できます。

ただし、整数の主キーを持つテーブルの一般的なケースでは、Exposedはキーを定義するクラス IntIdTable および LongIdTable を提供します。

object StarWarsFilms : IntIdTable() {
    val sequelId = integer("sequel__id").uniqueIndex()
    val name = varchar("name", 50)
    val director = varchar("director", 50)
}

UUIDTableもあります。さらに、 IdTableをサブクラス化することで、独自のバリアントを定義できます。

5.3. 外部キー

外部キーは簡単に導入できます。また、コンパイル時に既知のプロパティを常に参照しているので、静的型付けからも恩恵を受けます。

各映画で出演している俳優の名前を追跡したいとします。

object Players : Table() {
    val sequelId = integer("sequel__id")
      .uniqueIndex()
      .references(StarWarsFilms.sequelId)
    val name = varchar("name", 50)
}

参照されている列から派生することができるときに列の型(この場合は integer )をつづらなくても済むようにするために、 reference メソッドを省略形として使用できます。

val sequelId = reference("sequel__id", StarWarsFilms.sequelId).uniqueIndex()

参照が主キーの場合は、列の名前を省略できます。

val filmId = reference("film__id", StarWarsFilms)

5.4. テーブルを作成する

プログラムで上記のようにテーブルを作成することができます。

transaction {
    SchemaUtils.create(StarWarsFilms, Players)
   //Do stuff
}

テーブルは、存在しない場合にのみ作成されます。ただし、データベースの移行はサポートされていません。

6. クエリ

前のセクションで示したようにテーブルクラスを定義したら、フレームワークが提供する拡張機能を使用してデータベースにクエリを発行できます。

** 6.1. すべて選択

**

データベースからデータを抽出するために、テーブルクラスから構築された Query オブジェクトを使用します。最も簡単なクエリは、与えられたテーブルのすべての行を返すものです。

val query = StarWarsFilms.selectAll()

クエリは Iterable、 なので forEachをサポートします:

query.forEach {
    assertTrue { it[StarWarsFilms.sequelId]>= 7 }
}

上記の例では暗黙的に it と呼ばれるクロージャパラメータは、 ResultRow クラスのインスタンスです。列をキーとした地図として見ることができます。

** 6.2. 列のサブセットを選択する

**

slice メソッドを使用して、テーブルの列のサブセットを選択すること、つまり投影を実行することもできます。

StarWarsFilms.slice(StarWarsFilms.name, StarWarsFilms.director).selectAll()
  .forEach {
      assertTrue { it[StarWarsFilms.name].startsWith("The") }
  }

列に関数を適用するためにも slice を使用します。

StarWarsFilms.slice(StarWarsFilms.name.countDistinct())

count や__avgなどの集計関数を使用する場合は、多くの場合、クエリにgroup by句が必要です。このグループについては、6.5項で説明します。

** 6.3. where式を使ったフィルタリング+

**

  • Exposedには、クエリやその他の種類のステートメントをフィルタ処理するために使用される where 式** のための専用DSLが含まれています。これは、以前に経験した列のプロパティと一連のブール演算子に基づくミニ言語です。

これはwhere式です。

{ (StarWarsFilms.director like "J.J.%") and (StarWarsFilms.sequelId eq 7) }

その型は複雑です。これは、 like、eq、 などの演算子を定義する SqlExpressionBuilder のサブクラスです。ご覧のとおり、これは and および or 演算子を組み合わせた一連の比較です。

そのような式を select メソッドに渡すと、再びクエリが返されます。

val select = StarWarsFilms.select { ... }
assertEquals(1, select.count())

型推論のおかげで、上の例のようにwhere式の複雑な型を select メソッドに直接渡すときにスペルアウトする必要はありません。

  • 式はKotlinオブジェクトなので、クエリパラメータに特別な規定はありません** 変数を使うだけです。

val sequelNo = 7
StarWarsFilms.select { StarWarsFilms.sequelId >= sequelNo }

6.4. 高度なフィルタリング

select とそのバリアントから返される Query オブジェクトには、クエリを絞り込むために使用できるメソッドがいくつかあります。

たとえば、重複行を除外したい場合があります。

query.withDistinct(true).forEach { ... }

あるいは、たとえばUIの結果にページ番号を付ける場合など、行のサブセットのみを返したい場合があります。

query.limit(20, offset = 40).forEach { ... }
  • これらのメソッドは新しい Query を返すので、簡単にチェーンすることができます。**

6.5. 並べ替え順とグループ化順

__Query.orderBy methodは、ソートを昇順にするか降順にするかを示す SortOrder__値にマップされた列のリストを受け入れます。

query.orderBy(StarWarsFilms.name to SortOrder.ASC)

1つまたは複数の列によるグループ化は、特に集約関数を使用する場合(6.2節を参照)に役立ちますが、 groupBy メソッドを使用して実現されます。

StarWarsFilms
  .slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director)
  .selectAll()
  .groupBy(StarWarsFilms.director)

6.6. 参加する

結合は、リレーショナルデータベースのセールスポイントの1つです。最も単純なケースでは、外部キーがあり、結合条件がない場合、組み込み結合演算子の1つを使用できます。

(StarWarsFilms innerJoin Players).selectAll()

ここでは innerJoin を示しましたが、同じ原則で左、右、およびクロスジョインも利用できます。

それから、where式を使って結合条件を追加できます。たとえば、外部キーがなく、明示的に結合を実行する必要がある場合です。

(StarWarsFilms innerJoin Players)
  .select { StarWarsFilms.sequelId eq Players.sequelId }

一般的な場合、結合の完全形式は次のとおりです。

val complexJoin = Join(
  StarWarsFilms, Players,
  onColumn = StarWarsFilms.sequelId, otherColumn = Players.sequelId,
  joinType = JoinType.INNER,
  additionalConstraint = { StarWarsFilms.sequelId eq 8 })
complexJoin.selectAll()

6.7. エイリアシング

  • カラム名からプロパティへのマッピングのおかげで、たとえカラムが同じ名前を持っていたとしても、我々は典型的な結合でエイリアスを作る必要がありません。

(StarWarsFilms innerJoin Players)
  .selectAll()
  .forEach {
      assertEquals(it[StarWarsFilms.sequelId], it[Players.sequelId])
  }

実際、上記の例では、 StarWarsFilms.sequelId Players.sequelId は別の列です。

しかし、同じテーブルがクエリ内に複数回現れる場合は、エイリアスを付けたいと思うかもしれません。それには alias 関数を使います。

val sequel = StarWarsFilms.alias("sequel")

テーブルのようにエイリアスを使うことができます。

Join(StarWarsFilms, sequel,
  additionalConstraint = {
      sequel[StarWarsFilms.sequelId]eq StarWarsFilms.sequelId + 1
  }).selectAll().forEach {
      assertEquals(
        it[sequel[StarWarsFilms.sequelId]], it[StarWarsFilms.sequelId]+ 1)
  }

上記の例では、 sequel エイリアスが結合に参加しているテーブルであることがわかります。その列の1つにアクセスするときは、エイリアステーブルの列をキーとして使用します。

sequel[StarWarsFilms.sequelId]----

===  **  7. ステートメント**

データベースへの問い合わせ方法を見たので、DMLステートメントの実行方法を見てみましょう。

====  **  7.1. データを挿入する**

** データを挿入するために、__挿入__関数のバリアントの1つを呼び出します。

[source,java,gutter:,true]

StarWarsFilms.insert { it[name]= "The Last Jedi" it[sequelId]= 8 it[director]= "Rian Johnson" }

上記のクロージャーには2つの注目すべきオブジェクトが含まれています。

**  __this__(クロージャ自体)は__StarWarsFilms__のインスタンスです。

クラス;これが、プロパティである列にアクセスできる理由です。
未修飾の名前
**  __it__(クロージャーパラメーター)は____InsertStatementです。それは

挿入する各列にスロットを持つマップのような構造

[[auto-increment]]

====  **  7.2. 自動インクリメント列の値の抽出**

自動生成された列(通常は自動インクリメントまたはシーケンス)を含むinsertステートメントがある場合、生成された値を取得したいと思うかもしれません。

通常の場合、生成された値は1つだけで、__insertAndGetIdを呼び出します。

[source,java,gutter:,true]

val id = StarWarsFilms.insertAndGetId { it[name]= "The Last Jedi" it[sequelId]= 8 it[director]= "Rian Johnson" } assertEquals(1, id.value)

複数の生成値がある場合は、それらを名前で読み取ることができます。

[source,java,gutter:,true]

val insert = StarWarsFilms.insert { it[name]= "The Force Awakens" it[sequelId]= 7 it[director]= "J.J. Abrams" } assertEquals(2, insert[StarWarsFilms.id]?.value)

====  **  7.3. データを更新する**

データベース内の既存のデータを更新するために、クエリと挿入について学んだことを使用できます。確かに、単純な更新はselectとinsertの組み合わせのように見えます。

[source,java,gutter:,true]

StarWarsFilms.update ({ StarWarsFilms.sequelId eq 8 }) { it[name]= "Episode VIII – The Last Jedi" }

__UpdateStatement__クロージャーと組み合わせたwhere式の使用方法を見ることができます。実際、__UpdateStatement__と__InsertStatement__は、共通のスーパークラス__UpdateBuilderを介してほとんどのAPIとロジックを共有しています。

** 古い値から新しい値を計算して列を更新する必要があるときは、__SqlExpressionBuilderを利用します。

[source,java,gutter:,true]

StarWarsFilms.update ({ StarWarsFilms.sequelId eq 8 }) { with(SqlExpressionBuilder) { it.update(StarWarsFilms.sequelId, StarWarsFilms.sequelId + 1) } }

これは、更新命令を構築するために使用できる中置演算子(__plus__、__minus__など)を提供するオブジェクトです。

====  **  7.4. データを削除する**

最後に、__deleteWhere__メソッドを使ってデータを削除できます。

[source,java,gutter:,true]

StarWarsFilms.deleteWhere ({ StarWarsFilms.sequelId eq 8 })

[[dao-api]]

===  **  8 DAO API、軽量ORM

**

これまでは、Exposedを使用して、Kotlinオブジェクトの操作からSQLクエリーおよびステートメントに直接マッピングしていました。 __insert、update、select__などの各メソッド呼び出しにより、SQLストリングがただちにデータベースに送信されます。

ただし、Exposedには、単純なORMを構成する高レベルのDAO APIもあります。それではそれでは深めましょう。

====  **  8.1. エンティティ**

前のセクションでは、クラスを使用してデータベーステーブルを表現し、静的メソッドを使用してデータベーステーブルに対する操作を表現しました。

さらに一歩進めて、エンティティの各インスタンスがデータベース行を表すテーブルクラスに基づいてエンティティを定義できます。

[source,java,gutter:,true]

class StarWarsFilm(id: EntityID<Int>) : Entity<Int>(id) { companion object : EntityClass<Int, StarWarsFilm>(StarWarsFilms)

    var sequelId by StarWarsFilms.sequelId
    var name     by StarWarsFilms.name
    var director by StarWarsFilms.director
}
それでは、上記の定義を少しずつ分析しましょう。

1行目では、エンティティが__Entity__を拡張するクラスであることがわかります。それは特定のタイプ(この場合は__Int__)のIDを持ちます。

[source,java,gutter:,true]

class StarWarsFilm(id: EntityID<Int>) : Entity<Int>(id) {

それから、コンパニオンオブジェクト定義に遭遇します。コンパニオンオブジェクトは、エンティティクラス、つまりエンティティを定義する静的メタデータと、それに対して実行できる操作を表します。

さらに、コンパニオンオブジェクトの宣言では、エンティティ__StarWarsFilm ____が単一行__ __ __を表す複数の行のコレクションを表しているため、__StarWarsFilms __複数のエンティティに接続します。

[source,java,gutter:,true]

companion object : EntityClass<Int, StarWarsFilm>(StarWarsFilms)

最後に、対応するテーブル列へのプロパティデリゲートとして実装されたプロパティがあります。

[source,java,gutter:,true]

var sequelId by StarWarsFilms.sequelId var name by StarWarsFilms.name var director by StarWarsFilms.director

** カラムは不変のメタデータであるため、以前はカラムを__val__で宣言していました。さて、代わりに、エンティティプロパティを__var、__で宣言しています。これらは、データベース行の可変スロットだからです。

====  **  8.2. データを挿入する**

テーブルに行を挿入するには、トランザクションで静的ファクトリメソッド__new__を使用してエンティティクラスの新しいインスタンスを作成するだけです。

[source,java,gutter:,true]

val theLastJedi = StarWarsFilm.new { name = "The Last Jedi" sequelId = 8 director = "Rian Johnson" }

** データベースに対する操作は遅延して実行されることに注意してください。 ** 比較のために、Hibernateはウォームキャッシュを__session__と呼びます。

これは必要に応じて自動的に行われます。例えば、最初に生成された識別子を読んだとき、Exposedは黙ってinsert文を実行します。

[source,java,gutter:,true]

assertEquals(1, theLastJedi.id.value)//Reading the ID causes a flush

この振る舞いを、データベースに対して即時にステートメントを発行するセクション7.1の__挿入__メソッドと比較してください。ここでは、より高度な抽象化を進めています。

====  **  8.3. オブジェクトの更新と削除

**

行を更新するには、単純にそのプロパティに代入します。

[source,java,gutter:,true]

theLastJedi.name = "Episode VIII – The Last Jedi"

オブジェクトを削除している間に__delete__を呼び出します。

[source,java,gutter:,true]

theLastJedi.delete()

__new__と同様に、更新と操作は遅延して実行されます。

** 更新と削除は以前にロードされたオブジェクトに対してのみ実行できます** 大量の更新と削除のためのAPIはありません。代わりに、セクション7で見た低レベルのAPIを使用する必要があります。それでも、2つのAPIは同じトランザクションで一緒に使用できます。

====  **  8.4. 問い合わせ中**

DAO APIでは、3種類のクエリを実行できます。

条件なしですべてのオブジェクトをロードするには、静的メソッド__allを使用します。

[source,java,gutter:,true]

val movies = StarWarsFilm.all()

IDで単一のオブジェクトを読み込むには、__findByIdを呼び出します。

[source,java,gutter:,true]

val theLastJedi = StarWarsFilm.findById(1)

そのIDを持つオブジェクトがない場合、__findById__は__nullを返します。

** 最後に、一般的なケースでは、where式を使って__find__を使います。

[source,java,gutter:,true]

val movies = StarWarsFilm.find { StarWarsFilms.sequelId eq 8 }

====  **  8.5. 多対一アソシエーション**

結合がリレーショナルデータベースの重要な機能であるのと同じように、結合から参照へのマッピングはORMの重要な側面です。

ユーザーによる各映画の評価を追跡したいとします。まず、2つの追加テーブルを定義します。

[source,java,gutter:,true]

object Users: IntIdTable() { val name = varchar("name", 50) }

object UserRatings: IntIdTable() { val value = long("value") val film = reference("film", StarWarsFilms) val user = reference("user", Users) }

次に、対応するエンティティを書きます。些細な__User__エンティティを省略し、__UserRating__クラスに直接移動します。

[source,actionscript3,gutter:,true]

class UserRating(id: EntityID<Int>): IntEntity(id) { companion object : IntEntityClass<UserRating>(UserRatings)

    var value by UserRatings.value
    var film  by StarWarsFilm referencedOn UserRatings.film
    var user  by User         referencedOn UserRatings.user
}
** 特に、関連を表すプロパティに対する__referencedOn__ infixメソッドの呼び出しに注意してください** パターンは次のとおりです。

この方法で宣言されたプロパティは通常のプロパティと同じように動作しますが、その値は関連オブジェクトです。

[source,java,gutter:,true]

val someUser = User.new { name = "Some User" } val rating = UserRating.new { value = 9 user = someUser film = theLastJedi } assertEquals(theLastJedi, rating.film)

====  **  8.6. オプションの関連付け+

**

前のセクションで見た関連付けは必須です。つまり、常に値を指定する必要があります。

オプションの関連付けが必要な場合は、まずテーブル内でその列をNULL可能として宣言する必要があります。

[source,java,gutter:,true]

val user = reference("user", Users).nullable()

次に、エンティティで__referencedOn__の代わりに__optionalReferencedOn__を使用します。

[source,java,gutter:,true]

var user by User optionalReferencedOn UserRatings.user

そのようにして、__user__プロパティーはNULL可能になります。

====  **  8.7. 一対多関連+

**

私達はまた協会の反対側を写像したいと思うかもしれません。評価は映画に関するものです。これは、データベースで外部キーを使用してモデル化したものです。したがって、映画には多くの評価があります。

映画のレーティングをマッピングするには、協会の「一方」の側、つまり、この例では映画の実体にプロパティを追加します。

[source,java,gutter:,true]

class StarWarsFilm(id: EntityID<Int>) : Entity<Int>(id) { //Other properties elided val ratings by UserRating referrersOn UserRatings.film }

このパターンは多対1の関係のパターンと似ていますが、__referrersOnを使用します。__このように定義されたプロパティは__Iterableです。

[source,java,gutter:,true]

theLastJedi.ratings.forEach { …​ }

通常のプロパティとは異なり、__ratingsを__valで定義したことに注意してください。** 実際、プロパティは不変であり、読み取ることしかできません。**

このプロパティの値には、突然変異用のAPIもありません。そのため、新しい評価を追加するには、映画を参照してそれを作成する必要があります。

[source,java,gutter:,true]

UserRating.new { value = 8 user = someUser film = theLastJedi }

その後、映画の__ratings__リストに新しく追加された評価が含まれます。

====  **  8.8. 多対多関連

**

場合によっては、多対多関連が必要になることがあります。 __Actors__テーブルへの参照を__StarWarsFilm__クラスに追加したいとしましょう。

[source,java,gutter:,true]

object Actors: IntIdTable() { val firstname = varchar("firstname", 50) val lastname = varchar("lastname", 50) }

class Actor(id: EntityID<Int>): IntEntity(id) { companion object : IntEntityClass<Actor>(Actors)

    var firstname by Actors.firstname
    var lastname by Actors.lastname
}
テーブルとエンティティを定義したので、関連を表すために別のテーブルが必要です。

[source,java,gutter:,true]

object StarWarsFilmActors : Table() { val starWarsFilm = reference("starWarsFilm", StarWarsFilms).primaryKey(0) val actor = reference("actor", Actors).primaryKey(1) }

テーブルには2つの列があり、両方とも外部キーであり、これらも複合主キーを構成しています。

最後に、関連テーブルを__StarWarsFilm__エンティティに接続します。

[source,java,gutter:,true]

class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)

   //Other properties elided
    var actors by Actor via StarWarsFilmActors
}
これを書いている時点では、生成された識別子を持つエンティティを作成し、それを同じトランザクション内の多対多関連に含めることはできません。

実際には、複数のトランザクションを使用する必要があります。

[source,java,gutter:,true]

----//First, create the film
val film = transaction {
   StarWarsFilm.new {
    name = "The Last Jedi"
    sequelId = 8
    director = "Rian Johnson"r
  }
}//Then, create the actor
val actor = transaction {
  Actor.new {
    firstname = "Daisy"
    lastname = "Ridley"
  }
}//Finally, link the two together
transaction {
  film.actors = SizedCollection(listOf(actor))
}

ここでは、便宜上3つの異なるトランザクションを使用しました。ただし、2つでも十分でした。

9結論

この記事では、Kotlinの公開フレームワークの概要について説明しました。追加情報と例については、https://github.com/JetBrains/Exposed/wiki[Exposed wiki]を参照してください。

これらすべての例とコードスニペットの実装は、Mavenプロジェクトとしてhttps://github.com/eugenp/tutorials/tree/master/kotlin-libraries[GitHubプロジェクト]にあります。そのまま実行します。