KotlinのExposedでDSLとDAOのどちらを使うべきか？

### はじめに
[Exposed](https://github.com/JetBrains/Exposed)を使う場合にDSLとDAOのどちらを使えば良いのか？
について整理したことを実行例を交えてメモしておきます。

### Exposedとは
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/a935ccdd-becb-3f61-cf10-16e2b5ef5444.png" width="200">

Kotlinで書かれたORMフレームワークです。
[Hibernate](http://hibernate.org/)などに相当しますがより簡潔に利用できる感じがします。

DSL形式とDAO形式の2通りのアクセス方法をサポートしているのが特徴なのですが、
そのせいで『どっちを使うのが良いの？』と迷いがちです。（迷った）


#### 環境
- Kotlin：1.3.61
- Exposed：0.25.1
- H2：1.4.199
- Intelli J：2019.3（Community Edition）




#### そもそも勘違いしていたこと
「DSLとDAOの二種類の方法がある」とあるため、これらは動作モードのようなもので排他だと思っていたのですが、実は共存/併用が可能でした。

クラス構成と合わせて考えるとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15083/ed8f893a-626f-0c1e-3c0f-ee85a71b72f2.png)


### その上でザックリと結論
以下の特徴と自分のユースケースを考えて好きな方を選べば良い

##### DSLの特徴
- SQLに似た構文のDSLを使うためSQLに慣れた人には簡単
- DSLはタイプセーフなのでコンパイラによって型チェックされ安全
- （DAOに比較して）データベースに近い目線で操作する
- 複雑なクエリ操作が可能

##### DAO特徴
- シンプルなCRUD操作によりデータを簡単に扱える
- ボイラプレートやコードの重複が少ない
- （DSLに比較して）アプリケーションドメインに近い目線で操作する
- 複雑なクエリ操作は出来ない場合がある


### 実行例


#### サンプルデータ
今回はサンプルとして以下のようなデータを使います。

| ID  | タイトル         | 著者  | 価格    |
| --- | ------------ | --- | ----- |
| 1   | アジャイル開発とスクラム | 平鍋  | 2,000 |
| 2   | Java3Dプログラミング・バイブル  | 平鍋  | 1,580 |
| 3   | 受託開発の極意      | 岡島  | 1,480  |

#### テーブルの定義
DSL/DAOに共通の準備としてテーブルを定義します。

```kotlin
object Books: IntIdTable() {
    val title = varchar("title", 50)
    val author = varchar("author", 50)
    val price = integer("price")
}
```

#### エンティティの定義
更にDAOで扱うためにエンティティも定義します。

```kotlin
class Book(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<Book>(Books)

    var title by Books.title
    var author by Books.author
    var price by Books.price
}
```

### 各種操作

#### Create, Drop
DSL/DAOに関係なく共通の書き方になります。

```kotlin
    transaction {
        SchemaUtils.drop (Books)
        SchemaUtils.create (Books)
    }
```

内部ではこのようなSQLが実行されます。

```sql
SQL: DROP TABLE IF EXISTS BOOKS
SQL: CREATE TABLE IF NOT EXISTS BOOKS (ID INT AUTO_INCREMENT PRIMARY KEY, TITLE VARCHAR(50) NOT NULL, AUTHOR VARCHAR(50) NOT NULL, PRICE INT NOT NULL)
```

#### Insert
##### DSL
テーブルに対して`insert()`を使用します。

```kotlin
    transaction {
        Books.insert {
            it[title] = "アジャイル開発とスクラム"
            it[author] = "平鍋"
            it[price] = 2000
        }
        Books.insert {
            it[title] = "Java3Dプログラミング・バイブル"
            it[author] = "平鍋"
            it[price] = 1580
        }
        Books.insert {
            it[title] = "受託開発の極意"
            it[author] = "岡島"
            it[price] = 1480
        }
    }
```

##### DAO
`new()`を使用してエンティティを作成します。

```kotlin
    transaction {
        Book.new {
            title = "アジャイル開発とスクラム"
            author = "平鍋"
            price = 2000
        }
        Book.new {
            title = "Java3Dプログラミング・バイブル"
            author = "平鍋"
            price = 1580
        }
        Book.new {
            title = "受託開発の極意"
            author = "岡島"
            price = 1480
        }
    }
```

どちらも同一のSQLが実行されます。

```sql
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('平鍋', 2000, 'アジャイル開発とスクラム')
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('平鍋', 1580, 'Java3Dプログラミング・バイブル')
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('岡島', 1480, '受託開発の極意')
```

#### Select（全件）

##### DSL
テーブルに対して`selectAll()`を使用します。


```kotlin
    transaction {
        val books: Query = Books.selectAll()
        books.forEach { book -> println("Books = $book") }
    }
```


```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
Books = Books.id=1, Books.title=アジャイル開発とスクラム, Books.author=平鍋, Books.price=2000
Books = Books.id=2, Books.title=Java3Dプログラミング・バイブル, Books.author=平鍋, Books.price=1580
Books = Books.id=3, Books.title=受託開発の極意, Books.author=岡島, Books.price=1480
```


##### DAO
エンティティに対して`all()`を使用します。


```kotlin
    transaction {
        val books: SizedIterable<Book> = Book.all()
        books.forEach { book -> println("Books = $book") }
    }
```


```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
Books = Book@479460a6
Books = Book@7164ca4c
Books = Book@4f3bbf68
```

`all`で返却されるのはエンティティである`Book`型のIterableになります。

#### Select（条件付き）

##### DSL
テーブルに対して`select { Op <Boolean> }`を使用します。
ここで`Op`は検索条件になります。


```kotlin
    transaction {
        val book: ResultRow = Books.select{ Books.price eq 2000}.single()
        println("1st book is $book")
        println("1st book title is ${book[Books.title]}")
    }
```


```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.PRICE = 2000
1st book is Books.id=1, Books.title=アジャイル開発とスクラム, Books.author=平鍋, Books.price=2000
1st book title is アジャイル開発とスクラム
```

`select`で返却されるのは`Query`型で、`single()`を使用して`ResultRow`型に変換できます。
`ResultRow`型に対しては、`[テーブル.メンバ名]`でアクセスできます。


##### DAO
エンティティに対して`find { Op <Boolean> }`を使用します。
ここで条件にテーブル定義である`Books.price`を書かなければいけないのがちょっと残念な感じです。


```kotlin
    transaction {
        val book = Book.find{ Books.price eq 1480 }.single()
        println("1st book is $book")
        println("1st book title is ${book.title}")
    }
```


```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.PRICE = 1480
1st book is Book@5c2375a9
1st book title is 受託開発の極意
```

`find`で返却されるのはエンティティである`Book`型になります。
普通にクラスのメンバとしてアクセスできます。


#### Update
##### DSL
テーブルに対して`update { Op <Boolean> }`を使用します。

```kotlin
    transaction { 
        Books.update({Books.title eq "アジャイル開発とスクラム"}) {it[author] = "Hiranabe"}
    }
```

```sql
SQL: UPDATE BOOKS SET AUTHOR='Hiranabe' WHERE BOOKS.TITLE = 'アジャイル開発とスクラム'
```

##### DAO
`find { Op <Boolean> }`を使用して取得したエンティティのメンバを更新します。


```kotlin
    transaction { 
        Book.find { Books.title eq "アジャイル開発とスクラム" }.single().apply {
            author = "Hirabane" 
        }
    }
```

```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.TITLE = 'アジャイル開発とスクラム'
SQL: UPDATE BOOKS SET AUTHOR='Hirabane' WHERE ID = 1
```

#### Delete（全件）
##### DSL
テーブルに対して`deleteAll()`を使用します。

```kotlin
    transaction {
        Books.deleteAll()
    }
```

```sql
    SQL: DELETE FROM BOOKS
```

##### DAO
`all()`で全件取得した各エンティティに対して`delete()`を使用します。

```kotlin
    transaction {
        Book.all().forEach { it.delete() }
    }
```

```sql
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 1
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 2
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 3
```




### 参考情報
- [Exposed（公式）](https://github.com/JetBrains/Exposed)
- [DAO vs DSL, pros and cons?](https://github.com/JetBrains/Exposed/issues/735)
- [Bits and blobs of Kotlin/Exposed JDBC framework](https://medium.com/@OhadShai/bits-and-blobs-of-kotlin-exposed-jdbc-framework-f1ee56dc8840)