Mais conteúdo relacionado
Semelhante a Play2 scalaを2年やって学んだこと (20)
Play2 scalaを2年やって学んだこと
- 19. サーバーアプリケーションの
パッケージ構成
• app
• controllers
• models
• アプリケーション内でつかうモデル(ユーザー、求人、応募、閲覧履歴、エラー)
• repositories
• 永続化
• DBや他のAPI、AWSのサービスとつなぐ役割
• services
• コントローラーとレポジトリをつなぐ
• DBトランザクションの管理(リポジトリをまたいだトランザクションの為と、repositoryのテストがし易いため)
• utils
• views
• twirl用のhtmlや、レスポンスがjsonの場合はcase classやwritesを置く
- 25. Flyway
mysql> desc players;
+------------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(64) | NO | | NULL | |
| defence_position | varchar(64) | YES | | NULL | |
+------------------+-------------+------+-----+---------+-------+
mysql> select * from schema_versionG
*************************** 1. row ***************************
installed_rank: 1
version: 1
description: Create players table
type: SQL
script: V1__Create_players_table.sql
checksum: -2124815084
installed_by: root
installed_on: 2017-05-07 21:35:01
execution_time: 43
success: 1
- 27. 使い方
• マッピングを自動生成する
package repositories.slick
import slick.jdbc.GetResult
import slick.jdbc.MySQLProfile.api._
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
trait Tables{
case class Player(id: Int, name: String, defencePosition: Option[String] = None)
implicit val getPlayerResult = GetResult { r =>
Player(r.<<, r.<<, r.<<)
}
class Players(tag: Tag) extends Table[Player](tag, "players") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def defencePosition = column[Option[String]]("defence_position")
def * = (id, name, defencePosition) <> (Player.tupled, Player.unapply)
}
object Players extends TableQuery(new Players(_))
}
object Tables extends Tables
- 28. Repository
• DBから取得する
• DBIO型で返す
• テストやりやすくする
• トランザクションの制御はservice層でする
package repositories.slick
import com.google.inject.Singleton
import repositories.slick.Tables._
import slick.jdbc.MySQLProfile.api._
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
@Singleton
class Players {
def getAllPlayers: DBIO[Seq[Player]] = {
Players.result
}
}
- 29. Service
• レポジトリからデータを取得して、コントローラー
に返す
• Future型で返す
package services
import com.google.inject.{Inject, Singleton}
import repositories.slick.Players
import repositories.slick.Tables.Player
import slick.jdbc.MySQLProfile.api._
import scala.concurrent.Future
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
@Singleton
class PlayerService @Inject()(
val repo: Players
) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Seq[Player]] = {
db.run(repo.getAllPlayers)
}
}
- 30. Controller
• データを取得してViewを表示
package controllers
import com.google.inject.{Inject, Singleton}
import play.api.mvc.{Action, Controller}
import services.PlayerService
import scala.concurrent.ExecutionContext
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
@Singleton
class PlayersController @Inject()(
val service: PlayerService
)(implicit val ed: ExecutionContext) extends Controller {
def list = Action.async {
service.getPlayers.map { players =>
Ok(views.html.player.list(players))
}
}
}
- 31. 他のAPIとの連携
• 他のAPIサーバーからデータを取得する
package repositories.api
import com.google.inject.Singleton
import play.api.libs.ws.WS
import scala.concurrent.Future
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
@Singleton
class OutsideApi {
def battingAverage(name: String): Future[Double] = {
// WSで他のAPIサービスからデータを取ってくる
// WS.url("https://api.outside.internal/batting-average/:name")
// 今回はモックで適当に。
Future.successful(0.321)
}
}
- 32. Service
• ネストが深くなってくる、、
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Seq[(Player, Double)]] = {
// DBから一覧を取得
db.run(repo.getAllPlayers).flatMap { players =>
// 選手毎に、
Future.sequence(players.map { player =>
// データを取得して、
api.battingAverage(player.name).map { average =>
// レスポンスするデータを作る
(player, average)
}
})
}
}
}
- 33. (APIを修正しておく)
• 複数取得できるAPIを用意する
• もしくは、repositoryで吸収する(Future.sequence)
@Singleton
class OutsideApi {
def battingAverage(name: String): Future[Double] = {
// WSで他のAPIサービスからデータを取ってくる
// WS.url("https://api.outside.internal/batting-average/:name")
// 今回はモックで適当に。
Future.successful(0.321)
}
// 複数の選手の打率を返す
def battingAverages(names: Seq[String]): Future[Map[String, Double]] = {
Future.successful(
names.map { name =>
name -> 0.321
}.toMap
)
}
}
- 34. for式を使う
• ネストがなくなって読みやすくなった
• 並列で実行できないか?
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Seq[(Player, Double)]] = {
for {
// DBから取得
players <- db.run(repo.getAllPlayers)
// APIから取得
averages <- api.battingAverages(players.map(_.name))
} yield {
// データを生成
players.map { player =>
(player, averages(player.name))
}
}
}
}
- 35. APIが追加される
• チーム全員の打率を返すAPIが追加された
@Singleton
class OutsideApi {
// 複数の選手の打率を返す
def battingAverages(names: Seq[String]): Future[Map[String, Double]] = {
Future.successful(
names.map { name =>
name -> 0.321
}.toMap
)
}
// チームの全選手の打率を返す
def battingAveragesByTeam(name: String): Future[Map[String, Double]] = {
Future.successful(
Map(
"鳥谷" -> 0.321
)
)
}
}
- 36. forの罠
• 全員のデータをいっぺんに取ってくる
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Seq[(Player, Double)]] = {
for {
// DBから取得
players <- db.run(repo.getAllPlayers)
// APIから取得
averages <- api.battingAveragesByTeam("tigers")
} yield {
// データを生成
players.map { player =>
(player, averages(player.name))
}
}
}
}
- 37. 並列
• 先にFutureを実行する
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Seq[(Player, Double)]] = {
// DBから取得
val playersFuture = db.run(repo.getAllPlayers)
// APIから取得
val averagesFuture = api.battingAveragesByTeam("tigers")
for {
players <- playersFuture
averages <- averagesFuture
} yield {
// データを生成
players.map(player => (player, averages(player.name)))
}
}
}
- 38. エラーを扱いたい
• 外部APIがエラーを返してきたらどうする?
• エラークラス
package models
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
trait Errors {
override def toString: String = this match {
case e: InternalServerError => e.msg
}
}
final case class InternalServerError(msg: String) extends Errors
object Errors {
def internalServerError(msg: String) = InternalServerError(msg)
}
- 39. Either
• repository
• APIがエラーだったとき、Eitherで返すようにする
// チームの全選手の打率を返す
def battingAveragesByTeam(name: String): Future[Either[Errors, Map[String, Double]]] = {
Future(
Map(
"鳥谷" -> 0.321
)
).map(Right(_))
}
// チームの全選手の打率を返す
def battingAveragesByTeam(name: String): Future[Map[String, Double]] = {
Future.successful(
Map(
"鳥谷" -> 0.321
)
)
}
- 40. Service
• エラーをハンドリングして、LeftかRightで返す
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: Future[Either[Errors, Seq[(Player, Double)]]] = {
// DBから取得
val playersFuture = db.run(repo.getAllPlayers)
// APIから取得
val averagesFuture = api.battingAveragesByTeam("tigers")
for {
players <- playersFuture
averagesEither <- averagesFuture
} yield {
// データを生成
averagesEither.fold(
error => Left(Errors.internalServerError("api error")),
averages => Right(players.map(player => (player, averages(player.name))))
)
}
}
}
- 41. Controller
• 適切なエラーを返せるようになる
• Future[Either[Errors, T]]とか面倒
• DBがEitherで返すようになったらどうする?
@Singleton
class PlayersController @Inject()(
val service: PlayerService
)(implicit val ed: ExecutionContext) extends Controller
{
def list = Action.async {
service.getPlayers.map { result =>
result.fold(
e => InternalServerError(e.toString),
result => Ok(views.html.player.list(result))
)
}
}
}
- 42. scalaz.EitherT
• repository
• /,/-
// チームの全選手の打率を返す
def battingAveragesByTeam(name: String): Future[/[Errors, Map[String, Double]]] = {
Future(
Map(
"鳥谷" -> 0.321
)
).map(/-(_))
}
// チームの全選手の打率を返す
def battingAveragesByTeam(name: String): Future[Either[Errors, Map[String, Double]]] = {
Future(
Map(
"鳥谷" -> 0.321
)
).map(Right(_))
}
- 43. service
• EitherT
• for式の中はスッキリしたけど、、、
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
)(implicit val ec: ExecutionContext) {
val db = Database.forConfig("db.default")
def getPlayers: EitherT[Future, Errors, Seq[(Player, Double)]] = {
// DBから取得
val playersFuture: Future[/[Errors, Seq[Player]]] = db.run(repo.getAllPlayers).map(/-(_))
val playersFutureEither: EitherT[Future, Errors, Seq[Player]] = EitherT.eitherT(playersFuture)
// APIから取得
val averagesFuture: EitherT[Future, Errors, Map[String, Double]] =
EitherT.eitherT(api.battingAveragesByTeam("tigers"))
for {
players <- playersFutureEither
averages <- averagesFuture
} yield {
// データを生成
players.map(player => (player, averages(player.name)))
}
}
}
- 45. package utils
import models.Errors
import scala.concurrent.Future
import scalaz.{Applicative, EitherT, /, /-}
import scalaz.syntax.ToEitherOps
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
trait FunctionalSyntaxHelper extends ToEitherOps {
implicit class ToEitherT[A](a: A) {
/**
* A を EitherT[F, Errors, A] に変換する
*/
def toEitherT[F[_]](implicit F: Applicative[F]): EitherT[F, Errors, A] = {
val either: /[Errors, A] = /-(a)
EitherT(F.point(either))
}
}
implicit class RichEither[A, B](either: A / B) {
/**
* A / B を EitherT[F, A, B] に変換する
*/
def toEitherT[F[_]](implicit F: Applicative[F]): EitherT[F, A, B] = {
EitherT(F.point(either))
}
}
implicit class RichEitherFuture[A, B](eitherF: Future[A / B]) {
/**
* Future[/[A, B]] を EitherT[[Future A, B]] に変換する
*/
def toEitherT: EitherT[Future, A, B] = EitherT[Future, A, B](eitherF)
}
}
- 46. EitherT[Future,Errors,T]
• きれいになりました。
import scalaz.EitherT
import scalaz.Scalaz._
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
) (implicit val ec: ExecutionContext)
extends FunctionalSyntaxHelper {
val db = Database.forConfig("db.default")
def getPlayers: EitherT[Future, Errors, Seq[(Player, Double)]] = {
// DBから取得
val playersFuture = db.run(repo.getAllPlayers).map(_.right[Errors]).toEitherT
// APIから取得
val averagesFuture = api.battingAveragesByTeam("tigers").toEitherT
for {
players <- playersFuture
averages <- averagesFuture
} yield {
// データを生成
players.map(player => (player, averages(player.name)))
}
}
}
- 49. もう少し
• importを減らす努力
trait FunctionalSyntaxHelper extends ToEitherOps with FutureInstances {
// import を省略するためのショートカット
type /[+A, +B] = scalaz./[A, B]
type -/[+A] = scalaz.-/[A]
type /-[+B] = scalaz./-[B]
type EitherT[F[_], A, B] = scalaz.EitherT[F, A, B]
val / : scalaz./.type = scalaz./
val -/ : scalaz.-/.type = scalaz.-/
val /- : scalaz./-.type = scalaz./-
- 50. importも減らせる
@Singleton
class PlayerService @Inject()(
val repo: Players,
val api: OutsideApi
) (implicit val ec: ExecutionContext)
extends FunctionalSyntaxHelper {
val db = Database.forConfig("db.default")
def getPlayers: EitherT[Future, Errors, Seq[(Player, Double)]] = {
// DBから取得
val playersFuture = db.run(repo.getAllPlayers).map(_.right[Errors]).toEitherT
// APIから取得
val averagesFuture = api.battingAveragesByTeam("tigers").toEitherT
for {
players <- playersFuture
averages <- averagesFuture
} yield {
// データを生成
players.map(player => (player, averages(player.name)))
}
}
}
- 51. scalazおまけ
• ToBooleanOps
• if文を減らせる
trait FunctionalSyntaxHelper extends ToEitherOps
with ToBooleanOps
with FutureInstances {
def useIf(is: Boolean): /[Errors, Boolean] = {
if (is) {
true.right
} else {
Errors.internalServerError("false").left
}
}
def useEither(is: Boolean): /[Errors, Boolean] = {
is either true or Errors.internalServerError("false")
}
- 53. scalikeJDBCの使い方
sbt
• project/plugins.sbt
• build.sbt
libraryDependencies += "mysql" % "mysql-connector-java" % "5.1.26"
addSbtPlugin("org.scalikejdbc" %% "scalikejdbc-mapper-generator" % "2.5.0")
libraryDependencies ++= Seq(
jdbc, cache, ws,
"com.typesafe.play" % "play-slick_2.11" % "2.1.0",
"com.typesafe.slick" % "slick_2.11" % "3.2.0",
"mysql" % "mysql-connector-java" % "6.0.6",
"org.scalaz" %% "scalaz-core" % "7.2.12",
// Scalikejdbc
"org.scalikejdbc" %% "scalikejdbc" % "2.5.0",
"org.scalikejdbc" %% "scalikejdbc-config" % "2.5.0",
"org.scalikejdbc" %% "scalikejdbc-play-dbapi-adapter" % "2.5.1",
specs2 % Test )
scalikejdbcSettings
- 54. 自動生成
• project/scalikejdbc.properties
• $ sbt “scalikejdbcGen players”
# ---
# jdbc settings
jdbc.driver="com.mysql.jdbc.Driver"
jdbc.url="jdbc:mysql://localhost:3306/tigers"
jdbc.username="root"
jdbc.password=""
# ---
# source code generator settings
generator.packageName=repositories.scalikejdbc.jdbc
# generator.lineBreak: LF/CRLF
generator.lineBreak=LF
# generator.template: interpolation/queryDsl
generator.template=queryDsl
# generator.testTemplate: specs2unit/specs2acceptance/ScalaTestFlatSpec
generator.testTemplate=
# File Encoding
generator.encoding=UTF-8
# When you're using Scala 2.11 or higher, you can use case classes for 22+ columns tables
generator.caseClassOnly=true
# Set AutoSession for implicit DBSession parameter's default value
generator.defaultAutoSession=false
# Use autoConstruct macro (default: false)
generator.autoConstruct=false
# joda-time (org.joda.time.DateTime) or JSR-310 (java.time.ZonedDateTime java.time.OffsetDateTime)
generator.dateTimeClass=org.joda.time.DateTime
- 55. package repositories.scalikejdbc.jdbc
import scalikejdbc._
case class Players(
id: Int,
name: String,
defencePosition: Option[String] = None) {
def save()(implicit session: DBSession): Players = Players.save(this)(session)
def destroy()(implicit session: DBSession): Int = Players.destroy(this)(session)
}
object Players extends SQLSyntaxSupport[Players] {
override val tableName = "players"
override val columns = Seq("id", "name", "defence_position")
def apply(p: SyntaxProvider[Players])(rs: WrappedResultSet): Players = apply(p.resultName)(rs)
def apply(p: ResultName[Players])(rs: WrappedResultSet): Players = new Players(
id = rs.get(p.id),
name = rs.get(p.name),
defencePosition = rs.get(p.defencePosition)
)
val p = Players.syntax("p")
override val autoSession = AutoSession
def find(id: Int)(implicit session: DBSession): Option[Players] = {
withSQL {
select.from(Players as p).where.eq(p.id, id)
}.map(Players(p.resultName)).single.apply()
}
def findAll()(implicit session: DBSession): List[Players] = {
withSQL(select.from(Players as p)).map(Players(p.resultName)).list.apply()
}
def countAll()(implicit session: DBSession): Long = {
withSQL(select(sqls.count).from(Players as p)).map(rs => rs.long(1)).single.apply().get
}
- 56. def findBy(where: SQLSyntax)(implicit session: DBSession): Option[Players] = {
withSQL {
select.from(Players as p).where.append(where)
}.map(Players(p.resultName)).single.apply()
}
def findAllBy(where: SQLSyntax)(implicit session: DBSession): List[Players] = {
withSQL {
select.from(Players as p).where.append(where)
}.map(Players(p.resultName)).list.apply()
}
def countBy(where: SQLSyntax)(implicit session: DBSession): Long = {
withSQL {
select(sqls.count).from(Players as p).where.append(where)
}.map(_.long(1)).single.apply().get
}
def create(
id: Int,
name: String,
defencePosition: Option[String] = None)(implicit session: DBSession): Players = {
withSQL {
insert.into(Players).namedValues(
column.id -> id,
column.name -> name,
column.defencePosition -> defencePosition
)
}.update.apply()
Players(
id = id,
name = name,
defencePosition = defencePosition)
}
- 57. def batchInsert(entities: Seq[Players])(implicit session: DBSession): List[Int] = {
val params: Seq[Seq[(Symbol, Any)]] = entities.map(entity =>
Seq(
'id -> entity.id,
'name -> entity.name,
'defencePosition -> entity.defencePosition))
SQL("""insert into players(
id,
name,
defence_position
) values (
{id},
{name},
{defencePosition}
)""").batchByName(params: _*).apply[List]()
}
def save(entity: Players)(implicit session: DBSession): Players = {
withSQL {
update(Players).set(
column.id -> entity.id,
column.name -> entity.name,
column.defencePosition -> entity.defencePosition
).where.eq(column.id, entity.id)
}.update.apply()
entity
}
def destroy(entity: Players)(implicit session: DBSession): Int = {
withSQL { delete.from(Players).where.eq(column.id, entity.id) }.update.apply()
}
}
- 59. service
package services
import com.google.inject.{Inject, Singleton}
import models.Errors
import repositories.api.OutsideApi
import repositories.scalikejdbc.PlayerRepository
import repositories.scalikejdbc.jdbc.Players
import utils.FunctionalSyntaxHelper
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by hiroyuki.ikawa on 2017/05/09.
*/
@Singleton
class PlayerService @Inject()(
val repo: PlayerRepository,
val api: OutsideApi
) (implicit val ec: ExecutionContext)
extends FunctionalSyntaxHelper {
def getPlayers: Future[/[Errors, Seq[(Players, Double)]]] = {
// DBから取得
val players = repo.getAllPlayers
// APIから取得
api.battingAveragesByTeam("tigers").map { averagesEither =>
averagesEither.map { averages =>
players.map(player => (player, averages(player.name)))
}
}
}
}