Play2 Scalaを2年やっ
て学んだこと
D3
創業以来、高い技術力と戦略的なUI/UXを武器に、世の中に価値あるサービスを
生み出しているビズリーチ。
サービスの数が増えるにつれ、技術の幅が広がったため、そのスキルやノウハウ
を社内のみならず、
世の中のエンジニアやデザイナーとも共有すべく、
私たちは「D3(ディーキューブ)※」というプロジェクトチームを立ち上げま
した。
D3では、たくさんのイベントや勉強会を開催し、
世のエンジニア・デザイナーと共に、さらなる高みを目指します。
※D3=DESIGNER & DEVELOPER DIVISION
自己紹介
• 名前:伊川弘之樹
• 職業:プログラマー
• 株式会社ビズリーチ インキュベーションカンパニー スタンバイアプリ事業部 マネージャー
• 趣味:子供と散歩、タイガースの応援
長男
2才
次男
2ヶ月
今日話すこと
• スタンバイの紹介
• 全体の設計
• Slick3
• scalaz
• scalikeJDBC
スタンバイ
• https://jp.stanby.com
• 400万件以上の求人をまとめて探せる日本最大級の
求人検索エンジン。
• 企業公式サイトや求人サイトの ありとあらゆる求
人情報が探せます。
スタンバイ・カンパニー
• https://stanby.co/
• スタンバイ・カンパニーは、誰でもかんたんに無料
で求人を作成できるサービスです。
• 作成した求人はスタンバイに掲載されます。
• また、応募者とチャットでやりとりをしたり、動画
で面接を行うこともできます。
スタンバイアプリ
• 掲載求人数は400万件以上で日本最大級。
• 1000以上の求人・転職サイトや企業サイトを横断検索
• 正社員からアルバイト・パートまでのあらゆる働き方やこだわり条件で仕事を探す
ことができます。
• 自分のプロフィールを登録しておくと、お店や企業から仕事のお誘いが届きます。
• 興味があれば、そのままお店・企業の方とチャットでやりとりをしていただき、不
明点などを気軽に聞くことができます。
• そこで働いてみたいと思ったら、そのままチャットで面接の日程調整をしたり、ス
タンバイアプリのビデオ通話機能を使えば、自宅にいながらスマホで面接もできま
す。
カンパニーアプリ
• たった3分 無料で求人掲載
• 働きたいひとにスカウトを送信
• 応募が来たらお知らせ
使っている技術(Client)
• Web
• HTML,CSS,JavaScript
• React,jQuery,Angular,TypeScript
• Android
• Java
• iOS
• Swift3
使っている技術(AWS)
• Scala(2.11.x)
• PlayFramework(2.3,2.5)
• MySQL(RDS)
• Redis,Memcache(ElastiCache)
• S3,SQS,SNS,Kinesis…
使っている技術(Firebase)
• RealTimeDatabase
• BigQuery
• ストレージ
• Remote Config
アーキテクチャ
(マイクロサービスの図)
それぞれがRDSやCache、S3などを持っています。
他にも、バッチサーバーや、
管理画面用のサーバーや、
ログ集計用のサーバーがあります
スタンバイ
• ElasticSeachから求人情報を検索する
• クライアントはブラウザ(PC,Mobile)、Android、iOS
• ブラウザは別サーバー
• ユーザーの情報を管理する
• 履歴書、検索履歴、閲覧履歴
• スカウトされるための情報
• 求人広告を配信して稼いでいます。
• Yahoo!しごと検索にもAPI提供しています
スタンバイ カンパニー
• 企業やお店が求人を作成したら、ElasticSearchに
インデックスします
• 求職者から応募があったときに応募情報を管理しま
す。
• スカウトします
クローラー
• 求人サイトや、企業HPの求人ページをクローリン
グして、ElasticSearchにインデックスします。
• よりよい検索結果をユーザーに提供できるように、
求人の内容を学習しているようです。
サーバーサイド
• 基本的にはPlay2 Scala
• ORM:Slick3 or ScalikeJDBC
• DBMigration:DBFlute or Flyway
サーバーアプリケーションの
パッケージ構成
• app
• controllers
• models
• アプリケーション内でつかうモデル(ユーザー、求人、応募、閲覧履歴、エラー)
• repositories
• 永続化
• DBや他のAPI、AWSのサービスとつなぐ役割
• services
• コントローラーとレポジトリをつなぐ
• DBトランザクションの管理(リポジトリをまたいだトランザクションの為と、repositoryのテストがし易いため)
• utils
• views
• twirl用のhtmlや、レスポンスがjsonの場合はcase classやwritesを置く
• 下の層が上の層に依存しないように。
• repository層が受け取るのはできるだけプリミテ
ィブな型
• 上の層は下の層を知らなくてもいいように。
• repository層が返すのはエンティティ
controllers
• 機能毎にパッケージを分ける
• リクエストのバリデーション
• メイン処理の呼び出し
• レスポンスの組み立て
controllers/
players/
PlayerController
param/
Request
view/
Response
jobs/
applications/
DBFlute
• http://dbflute.seasar.org/
• EclipseでER図を作って、DDLや、Alter文を生成し
、DBをマイグレーションする
• 手順が難しく、覚えられないのでやめた。
Flyway
• project/plugins.sbt
• build.sbt
resolvers += "Flyway" at "https://flywaydb.org/repo"
addSbtPlugin("org.flywaydb" % "flyway-sbt" % "4.2.0")
flywayUrl := "jdbc:mysql://localhost:3306/tigers"
flywayUser := "root"
Flyway
• src/main/resources/db/migration/V1__Create_players_table.sql
• $ sbt flywayMigrate
CREATE TABLE players(
id INT NOT NULL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
defence_position VARCHAR(64)
);
mysql> show tables;
+------------------+
| Tables_in_tigers |
+------------------+
| players |
| schema_version |
+------------------+
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
Slick3
• Play2.4からSlick3になった。
• MonadicでReactiveになった。
• db.run() して DBIO を Future にする
使い方
• マッピングを自動生成する
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
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
}
}
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)
}
}
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))
}
}
}
他の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)
}
}
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)
}
})
}
}
}
(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
)
}
}
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))
}
}
}
}
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
)
)
}
}
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))
}
}
}
}
並列
• 先に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)))
}
}
}
エラーを扱いたい
• 外部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)
}
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
)
)
}
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))))
)
}
}
}
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))
)
}
}
}
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(_))
}
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)))
}
}
}
FunctionalSyntaxHelper
• 全部importするとcompileが重くなるので、必要なパッケージだけ
importしたtraitを用意しておいてそれだけを使うようにする
• scalazを便利に使えるようにする
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)
}
}
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)))
}
}
}
EitherT
• EitherTのTはTransformerのT
• トランスフォーマーは2つのモナドを組み合わせて
別のモナドをつくるためのもの
• FutureとEitherから別のモナドを作っている
• なので、forできれいに書けた
/, -/, /-
• /: Either
• -/: Left
• /-: Right
もう少し
• 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./-
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)))
}
}
}
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")
}
Slick3のつらいところ
• DBIOとか、EitherTとか難しい。
• 無駄に非同期処理になって難易度が高い
• 発行されるSQLがよくわからない(Slick3でマシなっ
たけど)
• SQLの組み立て方も難しい
• ScalikeJDBCでええやん
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
自動生成
• 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
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
}
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)
}
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()
}
}
repository
package repositories.scalikejdbc
import repositories.scalikejdbc.jdbc.Players
import scalikejdbc._
/**
* Created by hiroyuki.ikawa on 2017/05/10.
*/
class PlayerRepository {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.singleton("jdbc:mysql://localhost:3306/tigers?useSSL=false", "root", "")
implicit val session = AutoSession
def getAllPlayers: Seq[Players] = {
Players.findAll
}
}
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)))
}
}
}
}
ScalikeJDBC
• SQLがわりと直感的に書ける
• bindとかいらない
• 無駄に非同期処理じゃないので、DBIOとか考えなくていい
• DBIOとかbindがあるかとか考えなくていいので、レビューし
やすい
• DBについてあまり考えなくて良くなるので、アプリケーショ
ンの設計や、ビジネスロジックのことを考えられるようになる
ありがとうございました

Play2 scalaを2年やって学んだこと