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.UUID
のrandomUUID()
という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_id
とtext
しかないとします
つまりこう
{"user_id": "D8E265B7-AF8F-4F2C-AEEF-2430CC609417",
"text": "テキストだよ"}
これを、ルーティングで受け取るためにはデータモデルが2つ必要になります
ソースまるごとはこれ
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)
// 前略
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つのファイルを用意しておきます
package dao
// import あれこれ
trait PostDao extends PostTable with MySQLDBImpl {
// 省略
}
package dao
// import あれこれ
trait UserDao extends PostTable with MySQLDBImpl {
// 省略
}
やり方
UserDao
とPostDao
の2つのtraitをまとめて1つのtrait PostUserDao
にします
db.run { ... }
するメソッドだけは1箇所にまとめないと、同じ名前の関数が定義されてるよエラーが出てしまうので、それは PostUserDao
だけに書きます
イメージこんなん
ソースコード
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
テーブルを作ります
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)
}
ちょっと解説
(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, '-', ''))"""
}