ScalaでPlayFramework: モデル編

この記事は以下の記事の続きです。
[ScalaでPlayFramework: ビュー編](http://qiita.com/Yaruki00/items/1ff93cd2df9a008c2413)

公式ドキュメントはこちら
[Play 2.5.10 documentation](https://www.playframework.com/documentation/2.5.10/Home)
この記事の内容は、だいたい以下のページに書かれています。

* [Accessing an SQL database](https://www.playframework.com/documentation/2.5.10/ScalaDatabase)
* [Anorm, simple SQL data access](https://www.playframework.com/documentation/2.5.10/ScalaAnorm)

DBはMySQLを使います。入っていない方は`brew`とかで入れてください。

```
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.11, for osx10.11 (x86_64) using  EditLine wrapper
```

接続には`Anorm `というものを使います。
もう一つ`Slick`と言うものもありますが、全然追えてないので、この記事では対象外とします。
`Anorm `も内容が沢山あって、本記事ではとりあえずDBと繋ぐくらいのレベルですので、詳しくは公式ドキュメントを参照してください。

# DBに接続する
MySQLに接続してクエリを実行し、結果を取得することを目指します。

## いくつかの設定
コードを書く前に設定しなければならないことがあります。
まずは、`build.sbt`でMySQLの`JDBC`と`Anorm`を依存ライブラリに追加します。

```scala:build.sbt
...
libraryDependencies ++= Seq(
  jdbc,
  cache,
  ws,
  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test,
  "mysql" % "mysql-connector-java" % "5.1.40",
  "com.typesafe.play" %% "anorm" % "2.5.2"
)
```
バージョンはこちらで確認してください。
[Download Connector/J](https://dev.mysql.com/downloads/connector/j/)
[playframework/anorm](https://github.com/playframework/anorm/releases)

次に、`conf/application.conf`の下の方にあるJDBC Datasourceの部分にDB情報を追加します。

```scala:conf/application.conf
## JDBC Datasource
...
#
db {
  # You can declare as many datasources as you want.
  # By convention, the default datasource is named `default`

  ...
  # これを追加
  default.driver=com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/play_sample?characterEncoding=UTF8"
  default.username=root
  default.password=password

  ...
}
```

とりあえず名前は`default`にしておきましょう。
もし複数のDBに接続したい場合は、`default`の部分を任意の名前に変えてDB情報を列挙します。
上の例は、`localhost`にある`play_sample`というDBに、`root`というユーザ(パスワードは`password`)でアクセスします。

## モデルを作る
まずはパッケージを作りましょう。`app`以下に`models`というパッケージを作ります。
その下に`DBAccess`というファイルを作り、以下のように書きます。

```scala:app/models/DBAccess.scala
package models

import javax.inject._
import play.api.mvc._
import play.api.db._
import anorm._
import anorm.SqlParser._

@Singleton
class DBAccess @Inject()(db: Database) {
}
```

これで`conf/application.conf`で設定した`default`のDBを使用することができます。
`default`以外を使用するには以下のようにします。

```scala:app/models/DBAccess.scala
...
@Singleton
class DBAccess @Inject()(@NamedDatabase("DB名") db: Database) {
}
```

`import anorm._`のところでエラーが出ますが、問題なく動作します。テンプレートもそうだけど、どうにかならないのかな？

### 接続する
DBに接続するには以下のようにします。

```scala:
...
db.withConnection { implicit c =>
  // 何かしら処理
}
...
```

これを使っておけば、スコープを外れたときに勝手に接続をクローズしてくれます。

### レコードを取得してみる
以下のテーブルからレコードを取得してみます。

テーブル名：language

|id:Int(11)/auto_increment|name:text|experienece:Int(3)|
|:--:|:--:|:--:|
|1|Scala|0|
|2|Python|5|
|3|Swift|1|

コードは以下のようになります。

```scala:app/models/DBAccess.scala
...
class DBAccess @Inject()(db: Database) {

  val parser = int("id") ~ str("name") ~ int("experience")

  def languageList: List[Int~String~Int] = {
    db.withConnection { implicit c =>
      SQL("SELECT * FROM language ORDER BY id").as(parser *)
    }
  }
}
...
```

色々出てきましたね。
まず、これ。

```scala
SQL("SELECT * FROM language ORDER BY id")
```

SQLです。はい。SQL書きます。

次に、SQLの後についているこれ。

```scala
as(parser.*)
```

SQLの結果の各レコードをパースするパーサを渡します。
今回対象のクエリで取得できるレコードは、カラムが左から、Int(11), text, Int(3)となるので、これをパースできるパーサを作ります。
それがこれ。ダブルクオートで囲われているのはカラム名です。

```scala
val parser = int("id") ~ str("name") ~ int("experience")
```

`~`は`Anorm `の演算子らしく、イメージとしてはタプルを作ってると思えばいいみたいです。

後ろにくっついている`*`は、レコードが0個以上あるときにつけます。
必ず1個以上になる場合は`+`が使えます。正規表現と一緒ですね。
主キーによる検索など、結果が1個になるときは`single`、0または1個の場合は`singleOpt`になります。

```scala
as(parser.*)         // 0以上
as(parser.+)         // 1以上
as(parser.single)    // 1
as(parser.singleOpt) // 1 or 0
```

そして返り値はレコードの分だけ`Int~String~Int`ができるので`List[Int~String~Int]`になります。

```
def languageList: List[Int~String~Int] = {
```

ちゃんと取得できているか確認してみましょう。
コントローラに以下を書きます。インジェクションの部分変更があるので忘れずに。

```scala:app/controller/HomeController.scala
...
@Singleton
class HomeController @Inject()(db: DBAccess) extends Controller {
...
  def languageList = Action {
    Ok(db.languageList.toString)
  }
...
```

ルーティングにも追加します。

```scala:conf/routes
...
GET     /language_list              controllers.HomeController.languageList
...
```

`/language_list`にアクセスしてみると、こんなものが表示されました。

```
List(~(~(1,Scala),0), ~(~(2,Python),5), ~(~(3,Swift),1))
```

どうも取れてそうですが、使いにくそうです。
パーサを改善、と言うかマッパーを噛ませてみます。

```scala:app/models/DBAccess.scala
...
class DBAccess @Inject()(db: Database) {

  val parser = int("id") ~ str("name") ~ int("experience")
  val mapper = parser.map { 
    case id ~ name ~ experience => Map("id" -> id, "name" -> name, "experience" -> experience)
  }

  def languageList: List[Map[String,Any]] = {
    db.withConnection { implicit c =>
      SQL("SELECT * FROM language ORDER BY id").as(mapper.*)
    }
  }
}
...
```

マッパーが増えました。パース結果にカラム名を追加して、マップとして返します。

```scala
val mapper = parser.map { 
  case id ~ name ~ experience => Map("id" -> id, "name" -> name, "experience" -> experience)
}
```

SQLの後につけるのをパーサではなくマッパーにしました。

```scala
SQL("SELECT * FROM language ORDER BY id").as(mapper.*)
```

マッパーを使うようにしたので、返り値も変わりました。

```scala
def languageList: List[Map[String,Any]] = {
```

再び`/language_list`にアクセスしてみます。

```
List(Map(id -> 1, name -> Scala, experience -> 0), Map(id -> 2, name -> Python, experience -> 5), Map(id -> 3, name -> Swift, experience -> 1))
```

使えそうになりました。

### レコードを挿入してみる
取得に比べれば随分楽です。

```scala:app/models/DBAccess.scala
...
  def insert: Option[Long] = {
    db.withConnection { implicit c =>
      SQL("insert into language(name, experience) values('Java', 4)").executeInsert()
    }
  }
...
```

挿入はSQLの後に`executeInsert()`をくっつけます。
返り値は、主キーが`auto_increment`で単一レコードの挿入の場合は、挿入されたレコードの主キーになります。
それ以外の、複数レコードの更新だったり、主キーが文字列だったりの場合は、文字列のリストになります。

```scala:app/models/DBAccess.scala
...
  def insertTwo: List[String] = {
    db.withConnection { implicit c =>
      SQL("insert into language(name, experience) values('C', 5), ('Ruby', 2)").executeInsert(str.+)
    }
  }
...
```

あ、`executeInsert()`にもパーサを渡すのね。

### レコードを更新・削除してみる
今度は`executeUpdate()`を使います。予想通りかな？

```scala:app/models/DBAccess.scala
...
  def update: Int = {
    db.withConnection { implicit c =>
      SQL("update language set experience = 999 where name = 'Scala'").executeUpdate()
    }
  }

  def delete: Int = {
    db.withConnection { implicit c =>
      SQL("delete from language where name = 'C'").executeUpdate()
    }
  }
...
```

返り値は更新に成功したレコードの件数です。

### 値を後から入れるやつ(名前わからない)
SQLに`{id}`(idは任意の文字列)と書いておいて、あとで`on`を付け足すとできます。

```scala:app/models/DBAccess.scala
...
  def search(language: String): Option[Map[String,Any]] = {
    db.withConnection { implicit c =>
      SQL("SELECT * FROM language where name = {language}").on("language" -> language).as(mapper.singleOpt)
    }
  }
...
```

`on`はマップと同じ要領で書きます。

### トランザクション
`db.withConnection`を`db.withTransaction`に変えましょう。それだけ。

# おわりに
今回はレコードの取得ですごく苦労しました。
最初はパーサの書き方が全くわからず、公式ドキュメントとにらめっこしながら、書いてはエラー書いてはエラー・・・
パーサ書くにはカラムを全部書かなければいけない気がしますが、カラム数が多いと大変そうですね。
`Slick`だったらこんなに大変では無いのでしょうか？なんかやり方間違えてるのかな・・・？
一応目的は達成しましたが、不明点がとても残ってます。引き続き使ってみて徐々に理解していこうと思います。
記事書いている途中で疲れてきて、文体とか崩れてしまっているかと思いますが、直す気力が無いので許してください。。。
ScalaでPlayFrameworkシリーズは一旦これでおしまいです。
指摘とかアドバイスとかあったらぜひください。お願いします！