【Kotlin】ExposedでCRUDを試す

# Exposedとは？
[Exposed](https://github.com/JetBrains/Exposed)はKotlinで書かれたSQLライブラリです。
JetBrainsが主体となって開発を行っています。
2018/02/19現在の最新版は0.9.1であり、prototypeであることを留意してください。

## サポートしているDB
以下のRDBMSに対応しています。
- PostgreSQL
- MySQL
- Oracle
- SQLite
- H2
- SQL Server

主要なRDBMSに対応しているため、普段使用しているDBに適用することができます。

# データアクセス方式
Exposedでは2つのデータアクセス方式を提供しています。

## DSL
DSL(Domain Specific Language)はSQLに似た記述を行うことができます。
そのためSQLに触れたことのある人であれば直感的に書くことができるのではないかと思います。
またKotlinの型チェックがはたらくため、type safetyとなります。

## DAO
DAO(Data Access Object)ではHibernateのようなORMと近い働きをします。
また軽量であることを謳っています。

# CRUDを試す
ここからは基本的な使用方法を紹介していきます。

## 環境
- Kotlin: 1.2.21
- Exposed: 0.9.1
- MySQL: 5.7.21

DBに関しましてはお好きなものに置き換えてお試しください。

## 依存関係追加
Gradle, Maven共にrepositoryとdependencyを追加するだけです。

```gradle:Gradle
repositories {
    maven {
        url "https://dl.bintray.com/kotlin/exposed”
    }
}

dependencies {
    compile 'org.jetbrains.exposed:exposed:0.9.1’
}
```

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

<dependencies>
  <dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed</artifactId>
    <version>0.9.1</version>
  </dependency>
</dependencies>
```

またお使いのDBのドライバも追加しておきましょう。
私の場合は以下のようになりました。

```gradle:build.gradle
dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.21"
    compile "org.jetbrains.exposed:exposed:0.9.1"
    compile "mysql:mysql-connector-java:5.1.45"
}
```

## Exposedを使うために必要なこと
Exposedを使うために3つ必要なことがあります。

- 依存関係の追加  
前述の通りです
- コネクション  
当然ですがDBとのコネクションが必要です。以下の記述を用います。

```kotlin
Database.connect("url", "driver", "user", "password")

// たとえばMySQLのexposed_sampleへ接続するには以下のように記述します。
Database.connect("jdbc:mysql://localhost/exposed_sample", "com.mysql.jdbc.Driver", "root", "password")
```

- トランザクション  
Exposedでの操作はtransactionブロック内に記述する必要があります。

```kotlin
transaction {
    // ここに処理を書く
}
```

また必須ではありませんが以下の記述をしておくことでSQLの標準出力を表示することができます。

```kotlin
logger.addLogger(StdOutSqlLogger)
```

# DSLでの操作
## テーブル定義
カラーコード(主キー)と名前を持つcolorsテーブルと
カラーコード(主キー、外部キー)とRGB値を持つRgbsテーブルを定義します。

```kotlin:Colors.kt
object Colors: Table("colors") {
    val code = varchar("code", 6).primaryKey()
    val name = varchar("name", 255)
}
```

```kotlin:Rgbs.kt
object Rgbs: Table("rgbs") {
    val code = (varchar("code", 6) references Colors.code).primaryKey()
    val r = integer("r")
    val g = integer("g")
    val b = integer("b")
}
```

Exposedでテーブル定義をするにはTableクラスを継承したシングルトンオブジェクトを作成します。
デフォルトではオブジェクト名がそのままテーブル名として作成されるため、
上記ではTable("colors")とすることでColorsテーブルではなくcolorsテーブルが作成されます。

各プロパティがカラムに相当し、制約はメソッドチェインで記述していきます。

## Create, Drop

```kotlin
transaction {
    create(Colors, Rgbs)
    drop(Colors, Rgbs)
}
```

このように1行で記述できます。
この際以下のSQLが実行されます。

```sql
SQL: CREATE TABLE IF NOT EXISTS colors (code VARCHAR(6) PRIMARY KEY, name VARCHAR(255) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS rgbs (code VARCHAR(6) PRIMARY KEY, r INT NOT NULL, g INT NOT NULL, b INT NOT NULL,  FOREIGN KEY (code) REFERENCES Colors(code) ON DELETE RESTRICT)
SQL: DROP TABLE  IF EXISTS Colors
SQL: DROP TABLE  IF EXISTS Rgbs
```

SQLからわかるように、デフォルトではIF NOT EXISTSのオプションがつきます。

## Insert
```kotlin
transaction {
    Colors.insert {
        it[code] = "40E0D0"
        it[name] = "turquoise"
    }
    Rgbs.insert {
        it[code] = "40E0D0"
        it[r] = 64
        it[g] = 224
        it[b] = 208
    }
}
````

```sql
SQL: INSERT INTO colors (code, name) VALUES ('40E0D0', 'turquoise')
SQL: INSERT INTO rgbs (code, r, g, b) VALUES ('40E0D0', 64, 224, 208)
```

MySQLではINSERT IGNORE用の関数を利用出来ます。

```kotlin
transaction {
    Colors.insertIgnore {
        it[code] = "40E0D0"
        it[name] = "turquoise"
    }
}
```

```sql
SQL: INSERT IGNORE INTO colors (code, name) VALUES ('40E0D0', 'turquoise')
```

## Select

```kotlin
transaction {
    val allColors = Colors.selectAll()
    val turquoise = Colors.select { Colors.name eq "turquoise" }
    val blackCode = Rgbs.select { (Rgbs.r eq 0) and (Rgbs.g eq 0) and (Rgbs.b eq 0) }
}
```

```sql
SQL: SELECT colors.code, colors.name FROM colors
SQL: SELECT colors.code, colors.name FROM colors WHERE colors.name = 'turquoise'
SQL: SELECT rgbs.code, rgbs.r, rgbs.g, rgbs.b FROM rgbs WHERE rgbs.r = 0 and rgbs.g = 0 and rgbs.b = 0
```
select()ではラムダの中に条件を指定してデータを取得します。
またselect()やselectAllはIterableを継承したQuery型を返すため、取得した結果をコレクション操作することが可能です。

## Update

```kotlin
transaction {
    Colors.update({ Colors.name eq "turquoise" }) { it[name] = "ターコイズ" }
}
```

```sql
SQL: UPDATE colors SET name='ターコイズ' WHERE colors.name = 'turquoise'
```

## Delete

```kotlin
transaction {
    Rgbs.deleteAll()
    Colors.deleteWhere { Colors.name eq "ターコイズ" }
    Colors.deleteIgnoreWhere { Colors.name eq "black" }
}
````

```sql
SQL: DELETE FROM rgbs
SQL: DELETE FROM colors WHERE colors.name = 'ターコイズ'
SQL: DELETE IGNORE FROM colors WHERE colors.name = 'black'
```

## Join

```kotlin
transaction {
    (Rgbs innerJoin Colors)
        .slice(Colors.name, Rgbs.r, Rgbs.g, Rgbs.b)
        .select { Colors.name eq "ターコイズ" }
        .forEach{ println(it) }
}
```

```sql
SQL: SELECT colors.name, rgbs.r, rgbs.g, rgbs.b FROM rgbs INNER JOIN colors ON colors.code = rgbs.code WHERE colors.name = 'ターコイズ'

> colors.name=ターコイズ, rgbs.g=224, rgbs.r=64, rgbs.b=208
```

JoinはinnnerJoin, leftJoin, crossJoinのキーワードを用いることができます。
またslice()関数でJoin後に残すカラムを指定することができます。

# DAOでの操作
## テーブル定義
今度はUsersテーブルとCountriesテーブルを定義します。
両テーブルともInteger型のidを主キーに持ちます。

```kotlin:Users.kt
object Users : IntIdTable() {
    val name = varchar("name", 255)
    val country = reference("country", Countries)
    val age = integer("age")
}
```

```kotlin:Countries.kt
object Countries: IntIdTable() {
    val name = varchar("name", 255)
}
```

テーブル定義にはDSLと同様に、Tableクラスを継承したobjectを作成します。
ただし、今回はInteger型のidを主キーに持つテーブルですので、IntIdTableクラスを継承しています。

## エンティティインスタンス
DAOではエンティティインスタンスを作成します。
Usersテーブルに対してUserインスタンス、Countriesテーブルに対してCountryインスタンスをそれぞれ以下のように作成します。

```kotlin:User.kt
class User(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<User>(Users)

    var name by Users.name
    var country by Country referencedOn Users.country
    var age by Users.age
}
```

```kotlin:Country.kt
class Country(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<Country>(Countries)

    var name by Countries.name
    val users by User referrersOn Users.country
}
```

エンティティクラス作成時のポイントとして、先程作成したテーブル定義のオブジェクトに紐づくEntitiyClass型のcompanion objectを内包する必要があります。

## Join
先に定義したUsersテーブルではcountryカラムに関して外部キー制約を以下のように設けていました。

```kotlin
val country = reference("country", Countries)
```

そのためUserインスタンスではcountryプロパティで以下のような記述を行いました。

```kotlin
var country by Country referencedOn Users.country
```

またCountriesテーブルではreferrersOnというキーワードと共にusersプロパティを定義しています。

```kotlin
val users by User referrersOn Users.country
```

これにより以下の記述で該当のCountryエンティティに紐づくUserデータを取得することができます。

```kotlin
// japanはCountryエンティティのインスタンス
japan.users
```

なお、referrersOnを用いる場合にはvalで定義する必要があります。

## Create, Drop
こちらはDSLと同様です。

```kotlin
transaction {
    create (Countries, Users)
    drop (Countries, Users)
}
```

実行されるSQLは以下の通りです。

```sql
SQL: CREATE TABLE IF NOT EXISTS Users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, country INT NOT NULL, age INT NOT NULL,  FOREIGN KEY (country) REFERENCES Countries(id) ON DELETE RESTRICT)
SQL: CREATE TABLE IF NOT EXISTS Countries (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL)
SQL: DROP TABLE  IF EXISTS Users
SQL: DROP TABLE  IF EXISTS Countries
```

## Insert
DAOではエンティティ作成にnewという拡張関数を使用します。

```kotlin
        val japan = Country.new {
            name = "Japan"
        }

        User.new {
            name = "gumimin"
            country = japan
            age = 25
        }
```

```sql
SQL: INSERT INTO Countries (name) VALUES ('Japan')
SQL: INSERT INTO Users (age, country, name) VALUES (25, 1, 'gumimin')
```

外部参照しているcountryプロパティは該当するidに変換してSQLが発行されるようです。

## Select

```kotlin
transaction {
    println("Countries: ${Country.all().joinToString {it.name}}")
    println("Adults: ${User.find { Users.age greaterEq 18 }.joinToString {it.name}}")
}
```

all(), find(), findById()といった関数でデータを取得することができます。
こちらもDSL同様、返却されるデータの型はItelableを継承しています。

## Update, Delete

```kotlin
transaction {
    val gumimin = User.find {Users.name eq "gumimin"}.single()
    gumimin.name = "choco"
    gumimin.delete()
}
```

 ```sql
SQL: SELECT Users.id, Users.name, Users.country, Users.age FROM Users WHERE Users.name = 'gumimin'
SQL: DELETE FROM Users WHERE Users.id = 1
SQL: UPDATE Users SET name='choco' WHERE id = 1
```

エンティティのプロパティを変更することで更新することができます。
こちらもSQLではWHERE句でidを用いているようです。

# さいごに
本投稿ではExposedを用いてCRUDを試してみました。
感想としましては、DSLで直感的に記述できるところがいいなと感じました。
DAOにおいては、テーブルにidという名の主キーが存在しない場合にマッピングが煩雑になるため、そこら辺がもう少し簡素にできると使いやすいなと感じました。
あくまでも現状ではprototypeなので今後に期待していきたいと思います。

投稿内容に間違いなどございましたらご指摘頂けると幸いです。