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

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

1. 前書き

このチュートリアルでは、Exposedを使用してリレーショナルデータベースにクエリを実行する方法を見ていきます。

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

Exposed can be used both as a high-level DSL over SQL and as a lightweight ORM (Object-Relational Mapping).したがって、このチュートリアルでは、両方の使用法について説明します。

2. 公開されたフレームワークのセットアップ

ExposedはまだMavenCentralにないため、専用のリポジトリを使用する必要があります。


    
        exposed
        exposed
        https://dl.bintray.com/kotlin/exposed
    

次に、ライブラリを含めることができます。


    org.jetbrains.exposed
    exposed
    0.10.4

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


    com.h2database
    h2
    1.4.197

Bintrayで最新バージョンのExposedを、Maven Centralで最新バージョンのH2を見つけることができます。

3. データベースへの接続

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

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

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

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

Note that invoking connect doesn’t establish a connection to the DB right away.接続パラメータを後で使用するために保存するだけです。

3.1. 追加パラメーター

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

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

このバージョンのconnectには、クロージャパラメーターが必要です。 Exposed invokes the closure whenever it needs a new connection to the database.

3.2. DataSourceの使用

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

Database.connect(datasource)

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

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

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

transaction {
    //Do cool stuff
}

transaction は、クロージャーが返すものは何でも返します。 Then, Exposed automatically closes the transaction when the execution of the block terminates.

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

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

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

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

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

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

フレームワークを学習したりデバッグしたりするときに、Exposedがデータベースに送信するSQLステートメントとクエリを調べると便利な場合があります。

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

transaction {
    addLogger(StdOutSqlLogger)
    //Do stuff
}

5. テーブルの定義

通常、Exposedでは、生のSQL文字列と名前を処理しません。 Instead, we define tables, columns, keys, relationships, etc., using a high-level 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. 主キー

前のセクションの例からわかるように、we can easily define indexes and primary keys with a fluent 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())

多くの場合、countavg,などの集計関数を使用する場合、クエリにgroupby句が必要になります。 グループについてはセクション6.5で説明します。

6.3. Where式によるフィルタリング

Exposed contains a dedicated DSL for where expressions。クエリやその他のタイプのステートメントをフィルタリングするために使用されます。 これは、以前に遭遇した列のプロパティと一連のブール演算子に基づくミニ言語です。

これはwhere式です。

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

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

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

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

型推論のおかげで、上記の例のようにselectメソッドに直接渡されるときに、where式の複雑な型を詳しく説明する必要はありません。

Since where expressions are Kotlin objects, there are no special provisions for query parameters.単純に変数を使用します。

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. OrderByおよびGroupBy

Query.orderBy メソッドは、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つです。 最も単純なケースでは、外部キーがあり、結合条件がない場合、組み込みの結合演算子のいずれかを使用できます。

(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.sequelIdPlayers.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. データの挿入

To insert data, we call one of the variants of the insert function.すべてのバリアントはクロージャを取ります:

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

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

  • this(クロージャー自体)はStarWarsFilmsクラスのインスタンスです。そのため、プロパティである列に非修飾名でアクセスできます

  • it(クロージャパラメーター)はInsertStatement; itであり、各列に挿入するスロットがあるマップのような構造です

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

自動生成された列(通常は自動インクリメントまたはシーケンス)を含むinsertステートメントがある場合、生成された値を取得することができます。

通常の場合、生成される値は1つだけであり、insertAndGetId:と呼びます。

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

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

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. データの更新

これで、クエリと挿入について学習したことを使用して、データベース内の既存のデータを更新できます。 Indeed, a simple update looks like a combination of a select with an insert:

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

where式をUpdateStatementクロージャと組み合わせて使用​​していることがわかります。 実際、UpdateStatementInsertStatementは、慣用的な角括弧を使用して列の値を設定する機能を提供する共通のスーパークラスUpdateBuilder,を介してほとんどのAPIとロジックを共有します。

古い値から新しい値を計算して列を更新する必要がある場合は、SqlExpressionBuilder:を利用します

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

これは、更新命令の作成に使用できる中置演算子(plusminusなど)を提供するオブジェクトです。

7.4. データを削除する

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

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

8. 軽量ORMであるDAOAPI

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

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

8.1. 実体

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

さらにステップを進めると、これらのテーブルクラスに基づいてエンティティを定義できます。エンティティの各インスタンスはデータベース行を表します。

class StarWarsFilm(id: EntityID) : Entity(id) {
    companion object : EntityClass(StarWarsFilms)

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

上記の定義を少しずつ分析してみましょう。

最初の行では、エンティティがEntityを拡張するクラスであることがわかります。 特定のタイプ(この場合はInt)のIDがあります。

class StarWarsFilm(id: EntityID) : Entity(id) {

次に、コンパニオンオブジェクト定義に遭遇します。 The companion object represents the entity class, that is, the static metadata defining the entity and the operations we can perform on it.

さらに、コンパニオンオブジェクトの宣言では、エンティティStarWarsFilm –を単数形で接続します。これは、単一の行を表すため、StarWarsFilms –複数形です。これはコレクションを表すためです。すべての行の。

companion object : EntityClass(StarWarsFilms)

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

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

以前は、列が不変のメタデータであるため、valで列を宣言したことに注意してください。 代わりに、エンティティプロパティはデータベース行の変更可能なスロットであるため、var,で宣言しています。

8.2. データの挿入

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

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

Note that operations against the database are performed lazily; they’re only issued when the warm cache is flushed.比較のために、Hibernateはウォームキャッシュをsessionと呼びます。

これは、必要なときに自動的に行われます。たとえば、生成された識別子を初めて読み取ると、Exposedはサイレントに挿入ステートメントを実行します。

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

この動作を、データベースに対してステートメントをすぐに発行するセクション7.1のinsertメソッドと比較してください。 ここでは、より高いレベルの抽象化に取り組んでいます。

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

行を更新するには、単にそのプロパティに割り当てます。

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

オブジェクトを削除するときに、そのオブジェクトでdeleteを呼び出します。

theLastJedi.delete()

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

Updates and deletions can only be performed on a previously loaded object.大規模な更新と削除のためのAPIはありません。 代わりに、セクション7で見た低レベルのAPIを使用する必要があります。 それでも、2つのAPIは同じトランザクションで一緒に使用できます。

8.4. 問い合わせ

DAO APIを使用すると、3種類のクエリを実行できます。

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

val movies = StarWarsFilm.all()

IDで単一のオブジェクトをロードするには、findById:を呼び出します。

val theLastJedi = StarWarsFilm.findById(1)

そのIDを持つオブジェクトがない場合、findByIdnull.を返します

最後に、一般的なケースでは、where式でfindを使用します。

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

8.5. 多対1の関連付け

結合がリレーショナルデータベースの重要な機能であるように、the mapping of joins to references is an important aspect of an ORM.では、Exposedが提供するものを見てみましょう。

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

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クラスに直接移動しましょう。

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

    var value by UserRatings.value
    var film  by StarWarsFilm referencedOn UserRatings.film
    var user  by User         referencedOn UserRatings.user
}

In particular, note the referencedOn infix method call on properties that represent associations.パターンは次のとおりです:var宣言、by参照エンティティ、referencedOn参照列。

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

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

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

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

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

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

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

var user by User optionalReferencedOn UserRatings.user

そうすれば、userプロパティはnull許容になります。

8.7. 1対多の関連付け

また、関連付けの反対側をマップすることもできます。 評価は映画に関するものであり、データベースで外部キーを使用してモデル化したものです。その結果、映画には多くの評価があります。

映画の評価をマッピングするには、関連付けの「一方」側、つまりこの例では映画エンティティにプロパティを追加するだけです。

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

パターンは多対1の関係のパターンに似ていますが、referrersOn.を使用します。このように定義されたプロパティはIterable,であるため、forEach:でトラバースできます。

theLastJedi.ratings.forEach { ... }

通常のプロパティとは異なり、ratingsval.Indeed, the property is immutable, we can only read it.で定義していることに注意してください

プロパティの値には、突然変異用のAPIもありません。 したがって、新しいレーティングを追加するには、映画への参照を使用して作成する必要があります。

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

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

8.8. 多対多の関連付け

場合によっては、多対多の関連付けが必要になることがあります。 Actorsテーブルへの参照をStarWarsFilmクラスに追加するとします。

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

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

    var firstname by Actors.firstname
    var lastname by Actors.lastname
}

テーブルとエンティティを定義したら、関連付けを表す別のテーブルが必要です。

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

テーブルには、両方が外部キーであり、複合主キーも構成する2つの列があります。

最後に、関連付けテーブルをStarWarsFilmエンティティに接続できます。

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

    //Other properties elided
    var actors by Actor via StarWarsFilmActors
}

執筆時点では、生成された識別子を使用してエンティティを作成し、それを同じトランザクションの多対多の関連付けに含めることはできません。

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

//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のExposedフレームワークの概要を説明しました。 追加情報と例については、Exposed wikiを参照してください。

これらすべての例とコードスニペットの実装は、Mavenプロジェクトとしてthe GitHub projectにあるため、そのままインポートして実行するのは簡単です。