1. 序章

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

Exposedは、JetBrainsによって開発されたオープンソースライブラリ(Apacheライセンス)です。これは、データベースベンダー間の違いを滑らかにしながら、一部のリレーショナルデータベース実装に慣用的なKotlinAPIを提供します。

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

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

必要なMaven依存関係を追加しましょう:

<dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed-core</artifactId>
    <version>0.37.3</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed-dao</artifactId>
    <version>0.37.3</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed-jdbc</artifactId>
    <version>0.37.3</version>
</dependency>

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

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

最新バージョンのExposed依存関係と最新バージョンのH2をMavenCentralで見つけることができます。

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

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

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

ユーザーパスワードを名前付きパラメーターとして指定することもできます。

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 {
    //Do cool stuff
}

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

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

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

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

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

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

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

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

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

transaction {
    addLogger(StdOutSqlLogger)
    //Do stuff
}

5. テーブルの定義

通常、Exposedでは、生のSQL文字列と名前を処理しません。 代わりに、高レベルのDSLを使用して、テーブル、列、キー、関係などを定義します。

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

object StarWarsFilms : Table()

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

object StarWarsFilms : Table("STAR_WARS_FILMS")

5.1. 列

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

Kotlinが推測できるため、簡潔にするためにタイプを省略しました。 とにかく、各列はタイプです名前、タイプ、場合によってはタイプパラメータがあります。

object StarWarsFilms_Simple : Table() {
    val id = integer("id").autoIncrement()
    val sequelId = integer("sequel_id").uniqueIndex()
    val name = varchar("name", 50)
    val director = varchar("director", 50)
    override val primaryKey = PrimaryKey(id, name = "PK_StarWarsFilms_Id")
}

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. すべて選択

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

val query = StarWarsFilms.selectAll()

クエリはIterable、であるため、 forEach:をサポートします

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

上記の例では暗黙的にitと呼ばれるクロージャパラメータは、ResultRowクラスのインスタンスです。 列でキー設定されたマップとして表示できます。

6.2. 列のサブセットの選択

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

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

スライスを使用して、列に関数を適用します。

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

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

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

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

これはwhere式です:

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

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

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

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

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

式は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. Order ByおよびGroup By

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つです。 最も単純なケースでは、外部キーがあり、結合条件がない場合、組み込みの結合演算子の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は異なる列です。

ただし、同じテーブルがクエリに複数回表示される場合は、エイリアスを指定することをお勧めします。 そのために、エイリアス関数を使用します。

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)
  }

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

sequel[StarWarsFilms.sequelId]

7. ステートメント

データベースにクエリを実行する方法を確認したので、DMLステートメントを実行する方法を見てみましょう。

7.1. データの挿入

データを挿入するために、挿入関数のバリアントの1つを呼び出します。すべてのバリアントはクロージャーを取ります:

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

上記のクロージャに関係する2つの注目すべきオブジェクトがあります。

  • この(クロージャー自体)は、StarWarsFilmsクラスのインスタンスです。 そのため、プロパティである列に、修飾されていない名前でアクセスできます。
  • it (クロージャーパラメーター)はInsertStatementです。 i tは、各列を挿入するためのスロットを備えたマップのような構造です。

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

自動生成された列(通常は自動インクリメントまたはシーケンス)を含む挿入ステートメントがある場合、生成された値を取得したい場合があります。

通常の場合、生成される値は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. データの更新

これで、クエリと挿入について学習した内容を使用して、データベース内の既存のデータを更新できます。 実際、単純な更新は、選択と挿入の組み合わせのように見えます:

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)
    }
}

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

7.4. データの削除

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

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

8. 軽量ORMであるDAOAPI

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

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

8.1. エンティティ

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

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

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
}

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

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

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

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

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

companion object : EntityClass<Int, StarWarsFilm>(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"
}

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

これは、必要に応じて自動的に行われます。 たとえば、生成された識別子を初めて読み取るとき、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 と同様に、更新と操作は遅延して実行されます。

更新と削除は、以前にロードされたオブジェクトに対してのみ実行できます。大規模な更新と削除のための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の関連付け

結合がリレーショナルデータベースの重要な機能であるように、参照への結合のマッピングは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<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
}

特に、関連付けを表すプロパティに対するreferencedOninfixメソッド呼び出しに注意してください。 パターンは次のとおりです。 var 宣言、参照されるエンティティ、 参照されたオン参照列。

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

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()

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

var user by User optionalReferencedOn UserRatings.user

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

8.7. 1対多の関連付け

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

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

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

パターンは多対1の関係のパターンに似ていますが、 referrersOn。 このように定義されたプロパティは反復可能、 でトラバースできます forEach:

theLastJedi.ratings.forEach { ... }

通常のプロパティとは異なり、定義したことに注意してください評価 val。 確かに、プロパティは不変であり、私たちはそれを読むことしかできません。

プロパティの値には、ミューテーション用のAPIもありません。 したがって、新しい評価を追加するには、映画を参照して評価を作成する必要があります。

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

次に、映画のレーティングリストに新しく追加されたレーティングが含まれます。

8.8. 多対多の協会

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

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
}

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

object StarWarsFilmActors : Table() {
    val starWarsFilm = reference("starWarsFilm", StarWarsFilms)
    val actor = reference("actor", Actors)
    override val primaryKey = PrimaryKey(
      starWarsFilm, actor, 
      name = "PK_StarWarsFilmActors_swf_act")
}

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

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

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

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

これらすべての例とコードスニペットの実装は、GitHubプロジェクトにあります。