Scala + Akka-HTTP + Slick あれこれ（サンプル集 + 解説）

Scala + Akka-HTTP + slick のwebAPIを作るときのサンプル集 + 解説です

## UUIDをカラムに使いたい

### とりあえずソース

データモデルはこちら

```scala
package model

import connection.MySQLDBImpl
import spray.json.DefaultJsonProtocol
import spray.json.DeserializationException
import spray.json.JsString
import spray.json.JsValue
import spray.json.JsonFormat
import java.util.UUID

trait PostTable extends DefaultJsonProtocol {
  this: MySQLDBImpl =>
  import driver.api._

  // custom JsonFormat to use UUID
  implicit object UuidFormat extends JsonFormat[UUID] {
    def write(uuid: UUID) = JsString(uuid.toString)
    def read(value: JsValue) = {
      value match {
        case JsString(uuid) => UUID.fromString(uuid)
        case _              => throw new DeserializationException("Expected hexadecimal UUID string")
      }
    }
  }
  implicit lazy val postFormat = jsonFormat2(Post)
  implicit lazy val postListFormat = jsonFormat1(PostList)

  class PostTable(tag: Tag) extends Table[Post](tag, "post") {
    def id = column[UUID]("id", O.PrimaryKey)
    def name = column[String]("name")

    def * = (id, name) <> (Post.tupled, Post.unapply)
  }
}
case class Post(id: UUID, name: String)
case class PostList(posts: List[Post])
```

DBに挿入するときはこう

```scala
create(Post(UUID.randomUUID()
        , "good morning"))
```

### ちょっと解説

まず spray の JsonFormat はUUIDを認識してくれない (StringとかIntならok)
ので、カスタマイズします
それがこれ

```scala
implicit object UuidFormat extends JsonFormat[UUID] {
  def write(uuid: UUID) = JsString(uuid.toString)
  def read(value: JsValue) = {
    value match {
      case JsString(uuid) => UUID.fromString(uuid)
      case _              => throw new DeserializationException("Expected hexadecimal UUID string")
    }
  }
}
```

これを**jsonFormatを使う前に**挿入してあげると、データモデルのカラムがUUID型でも大丈夫になります

importしてなかったらこいつらも必要です

```scala
import spray.json.DeserializationException
import spray.json.JsString
import spray.json.JsValue
import spray.json.JsonFormat
import java.util.UUID
```

カラムの方はUUID型にするだけでok

```scala
def id = column[UUID]("id", O.PrimaryKey)
```

データベースにデータを挿入したいときは、当たり前かもですが**Stringだとだめです**
`java.util.UUID`の`randomUUID()`というUUIDを生成する関数をつかいます

```scala
create(Post(UUID.randomUUID()
        , "good morning"))
```

### 参考URL

https://stackoverflow.com/questions/31406825/spray-json-implicit-uuid-conversion

## POSTで入ってくる JSON に NOT NULL なカラムの値がない

### 前提

たとえば、`created_date`という NOT NULL というカラムがあるけど、 POSTを受け取ったあとに `created_date`の値を生成したいとき

今回想定しているテーブル

```bash
+----------------+------------+------+-----+---------+-------+
| Field          | Type       | Null | Key | Default | Extra |
+----------------+------------+------+-----+---------+-------+
| id             | binary(16) | NO   | PRI | NULL    |       |
| user_id        | binary(16) | NO   |     | NULL    |       |
| text           | text       | NO   |     | NULL    |       |
| parent_post_id | binary(16) | YES  |     | NULL    |       |
| comment_count  | int(11)    | NO   |     | NULL    |       |
| posted_at      | date       | NO   |     | NULL    |       |
+----------------+------------+------+-----+---------+-------+
```

このようなテーブルに、ユーザがPOSTしてくることを考えます
ただし、POSTのrequest body には`user_id`と`text`しかないとします
つまりこう

```json
{"user_id": "D8E265B7-AF8F-4F2C-AEEF-2430CC609417", 
"text": "テキストだよ"}
```

これを、ルーティングで受け取るためにはデータモデルが2つ必要になります
ソースまるごとはこれ

```Scala:Post.scala
package model

import connection.MySQLDBImpl
import spray.json.DefaultJsonProtocol
import spray.json.DeserializationException
import spray.json.JsString
import spray.json.JsValue
import spray.json.JsonFormat
import java.text.SimpleDateFormat
import java.util.UUID
import java.sql.Date

trait PostTable extends DefaultJsonProtocol {
  this: MySQLDBImpl =>
  import driver.api._

  // custom JsonFormat to use UUID
  implicit object UuidFormat extends JsonFormat[UUID] {
    def write(uuid: UUID) = JsString(uuid.toString)
    def read(value: JsValue) = {
      value match {
        case JsString(uuid) => UUID.fromString(uuid)
        case _              => throw new DeserializationException("Expected hexadecimal UUID string")
      }
    }
  }
  // custom JsonFormat to use Date
  implicit object DateFormat extends JsonFormat[Date] {
    val formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
    def write(date: Date) = JsString(formatter.format(date))
    def read(value: JsValue) = {
      value match {
        case JsString(date) => new Date(formatter.parse(date).getTime())
        
        case _ => throw new DeserializationException("Expected JsString")
      }
    }
  }

  implicit lazy val postFormat = jsonFormat6(Post)
  implicit lazy val postListFormat = jsonFormat1(PostList)

  class PostTable(tag: Tag) extends Table[Post](tag, "post") {
    def id = column[UUID]("id", O.PrimaryKey)
    def userId = column[UUID]("user_id")
    def text = column[String]("text")
    def parentPostId = column[Option[UUID]]("parent_post_id", O.Default(None))
    def commentCount = column[Int]("comment_count")
    def postedAt = column[Date]("posted_at")

    def * = (id, userId, text, parentPostId, commentCount, postedAt) <> (Post.tupled, Post.unapply)
  }


  implicit lazy val userPostFormat = jsonFormat2(UserPost)

  class UserPostTable(tag: Tag) extends Table[UserPost](tag, "postTable") {
    def userId = column[UUID]("user_id")
    def text = column[String]("text")

    def * = (userId, text) <> (UserPost.tupled, UserPost.unapply)
  }
}

case class Post(id:UUID, userId: UUID, text: String, parentPostId: Option[UUID], commentCount: Int, postedAt: Date)
case class PostList(posts: List[Post])

// models to retrieve data from POST
case class UserPost(user_id: UUID, text: String)
```

```PostRoutes.scala
// 前略
pathPrefix("create"){  
  post {
    entity(as[UserPost]) { up =>
      complete {
        val newPost = Post(
          UUID.randomUUID()
          , up.user_id
          , up.text
          , Option(parentId)
          , 0
          , new Date(System.currentTimeMillis())
        )
        create(newPost).map{ result => HttpResponse(entity = "created new posts") }
      }
    }
  }
}
// 後略
```

### 解説

テーブルに格納するためのモデル

```scala
implicit lazy val postFormat = jsonFormat6(Post)
implicit lazy val postListFormat = jsonFormat1(PostList)

class PostTable(tag: Tag) extends Table[Post](tag, "post") {
  def id = column[UUID]("id", O.PrimaryKey)
  def userId = column[UUID]("user_id")
  def text = column[String]("text")
  def parentPostId = column[Option[UUID]]("parent_post_id", O.Default(None))
  def commentCount = column[Int]("comment_count")
  def postedAt = column[Date]("posted_at")

  def * = (id, userId, text, parentPostId, commentCount, postedAt) <> (Post.tupled, Post.unapply)
}

case class Post(id:UUID, userId: UUID, text: String, parentPostId: Option[UUID], commentCount: Int, postedAt: Date)
case class PostList(posts: List[Post])
```

`Option[T]`になってるカラムを見ると、`parentPostId`だけ
なので、ユーザのJSONをそのままこのデータモデルにつっこんでしまうと、`id, commentCount, postedAt` のカラムの値が NULLABLEでないにも関わらず、値が無いので怒られます

そのために、次のPOSTデータ受け取り用のモデルも定義します

```scala
implicit lazy val userPostFormat = jsonFormat2(UserPost)

class UserPostTable(tag: Tag) extends Table[UserPost](tag, "postTable") {
  def userId = column[UUID]("user_id")
  def text = column[String]("text")

  def * = (userId, text) <> (UserPost.tupled, UserPost.unapply)
}
}

// models to retrieve data from POST
case class UserPost(user_id: UUID, text: String)
```
この `UserPost` は、bodyに入ってる userIdとtextの2つのカラムだけ持ちます

２つのデータモデルを繋げるのは、ルーティングです

再掲

```scala
pathPrefix("create"){  
  post {
    entity(as[UserPost]) { up =>
      complete {
        val newPost = Post(
          UUID.randomUUID()
          , up.user_id
          , up.text
          , Option(parentId)
          , 0
          , new Date(System.currentTimeMillis())
        )
        create(newPost).map{ result => HttpResponse(entity = "created new posts") }
      }
    }
  }
}
// 後略
```

まず、`entity(as[UserPost])`で、POSTデータを UserPostにいれます
その上で、UserPost型変数`up`のデータを用いて新たに`Post`のインスタンスをつくります
それを、データベースにいれます

おわり

## responseを JSON で返したい

たとえば　`{"result" : "OK" }`というJSONをresponseで返したいときとか

### ソースコード

```Scala
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.{ HttpResponse, HttpEntity, ContentTypes } // 忘れずに
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.MethodDirectives
import java.util.UUID
import java.sql.Date
// 自分で定義したファイルたち
import foo.FooDao
import model.Foo


post {
  entity(as[Foo]) { f => 
    complete {
      val resOk = """{"result": "OK"}"""
      val newFoo = Foo(Option(UUID.randomUUID()), f.name)
      create(newFoo).map{ result => 
        HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, resOk)) }
    }
  }
}

```

ここがポイント

```scala
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, resOk)) }
```

返す中身は生の値だとなぜか怒られたので、とりあえず 

```scala
val resOk = """{"result": "OK"}"""
```

と変数に入れてみました

### 参考URL

https://doc.akka.io/docs/akka-http/current/common/json-support.html
https://doc.akka.io/docs/akka-http/current/routing-dsl/index.html

## SELECTの結果を複数取得したい

DAOはこう

```scala
  def getByColor(color: String): Future[List[Person]] = db.run {
    postTableQuery.filter(_.color === color).to[List].result
  }
```

ポイントは、メソッドの return type が `Future[List[Person]]` であること
取得結果が0のこともありえるし、`Future[Option[List[Person]]]` かなと思いましたが、List型にしてるからその必要はないみたい
あとは、queryする --> filterする --> Listにする --> resultメソッド
という順番を忘れがち（自分が）

## 複数のテーブルのDAOを一箇所にまとめたい

###　前提

掲示板への投稿データを表す `PostDao.scala` と掲示板のユーザデータを表す `UserDao.scala` という2つのファイルを用意しておきます

```scala:PostDao.scala
package dao

// import あれこれ

trait PostDao extends PostTable with MySQLDBImpl {
  // 省略
}
```

```scala:UserDao.scala
package dao

// import あれこれ

trait UserDao extends PostTable with MySQLDBImpl {
  // 省略
}
```

### やり方

`UserDao`と`PostDao`の2つのtraitをまとめて1つのtrait `PostUserDao` にします
`db.run { ... }` するメソッドだけは1箇所にまとめないと、同じ名前の関数が定義されてるよエラーが出てしまうので、それは `PostUserDao` だけに書きます
イメージこんなん

![Note sans titre - 16 nov. 2019 00.46 - Page 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430849/2760c1b4-06c1-ccb6-aecc-fd16e3da431d.png)


### ソースコード

```scala:PostUserDao.scala
package dao

import connection.MySQLDBImpl

trait PostUserDao extends UserDao with PostDao {
 import driver.api._

  def ddl = db.run {
    postTableQuery.schema.create
    userTableQuery.schema.create
  }
}
```

### ちょっと解説

traitは複数のtraitをextendできます
書き方は

```scala
trait A extends B with C
```

順番があるらしいですが、とりあえずここでは問題にならないはず

あと、下のimportを忘れると `schema.create`なんてないぞと怒られます

```scala
 import driver.api._
```

## テーブルの名前を設定したい

ユーザーを格納する `users` テーブルをつくります

### ソースコード

このコードは `id, name` フィールドを持つ `users` テーブルを作ります
```scala
class UserTable(tag: Tag) extends Table[User](tag, "users") {
  def id = column[UUID]("id", O.PrimaryKey)
  def name = column[String]("name")

  override def * = (id, name) <> (User.tupled, User.unapply)
}
```

### ちょっと解説

```scala
(tag, "users")
```

がポイント

```scala
Table[T] (tag, tableName)
```

という文法らしいです
公式のdocsが見つからんかったです
教えて

## 例外ハンドリングをカスタマイズ

### ソースコード

```scala
import akka.http.scaladsl.server.ExceptionHandler

implicit def myExceptionHandler: ExceptionHandler = 
  ExceptionHandler { 
    case e: Exception =>
      // println(e.getMessage)
      complete(HttpResponse(BadRequest, entity = HttpEntity(ContentTypes.`application/json`, noUser)))
  }
```

### 参考うらる

https://www.programcreek.com/scala/akka.http.scaladsl.server.ExceptionHandler
https://doc.akka.io/docs/akka-http/current/routing-dsl/exception-handling.html

## `entity(as[T])` で通らない

ユーザーのリクエストが指定した型でないときに `Invalid ... ` とかエラーが出ます
これのハンドリングするときは、 `ValidationRejection` が対象になります

カスタムハンドリングの例


```scala
implicit def myRejectionHandler =
RejectionHandler.newBuilder()
  .handle { 
    case ValidationRejection(msg, e) => complete(HttpResponse(BadRequest, entity =  s"えらーだよ ${msg}"))
  }
  .result()
```

marshallingがそもそも通らないので、`entity(as[T])`より後ろのコードでハンドルできません！

### 参考うらる

https://doc.akka.io/api/akka-http/10.1.10/akka/http/scaladsl/server/ValidationRejection.html
https://doc.akka.io/docs/akka-http/current/common/marshalling.html

# error いろいろ

## UUID のエラーが出た

### エラーの内容

エラーメッセージ

```
could not find implicit value for parameter e: slick.jdbc.SetParameter[java.util.UUID]
```

エラー吐いてたコード

```scala
def addComment(id: UUID): Future[Int] = db.run {
  val strId = id.toString
  sqlu"""UPDATE post SET comment_count = comment_count + 1 
    WHERE id = $id"""
}
```

### 対処

エラーの意味: 「SQLの中で変数SETしようとしてるけど、UUID型の変数はSQLにはないよ！」

ということなので、sqlにぶちこむ前にちゃんと String に変換してあげる
--> sqlだったら、`REPLACE`して`UNHEX`するので、その通りに `sqlu`のあとに書く

修正コードはこう

```scala
def addComment(id: UUID): Future[Int] = db.run {
  val strId = id.toString
  sqlu"""UPDATE post SET comment_count = comment_count + 1 
    WHERE id = UNHEX(REPLACE($strId, '-', ''))"""
}
```