LoginSignup
4
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-14

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

UUIDをカラムに使いたい

とりあえずソース

データモデルはこちら

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に挿入するときはこう

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

ちょっと解説

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

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してなかったらこいつらも必要です

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

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

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

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

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

参考URL

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

前提

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

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

+----------------+------------+------+-----+---------+-------+
| 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_idtextしかないとします
つまりこう

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

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

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") }
      }
    }
  }
}
// 後略

解説

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

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データ受け取り用のモデルも定義します

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つのカラムだけ持ちます

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

再掲

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で返したいときとか

ソースコード

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

ここがポイント

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

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

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はこう

  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つのファイルを用意しておきます

PostDao.scala
package dao

// import あれこれ

trait PostDao extends PostTable with MySQLDBImpl {
  // 省略
}
UserDao.scala
package dao

// import あれこれ

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

やり方

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

Note sans titre - 16 nov. 2019 00.46 - Page 1.png

ソースコード

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できます
書き方は

trait A extends B with C

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

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

 import driver.api._

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

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

ソースコード

このコードは id, name フィールドを持つ users テーブルを作ります
```scala
class UserTable(tag: Tag) extends TableUser {
def id = columnUUID
def name = columnString

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

ちょっと解説

(tag, "users")

がポイント

Table[T] (tag, tableName)

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

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

ソースコード

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 が対象になります

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

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]

エラー吐いてたコード

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のあとに書く

修正コードはこう

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, '-', ''))"""
}
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4