play2 scalaを2年やって学んだこと
TRANSCRIPT
D3
創業以来、高い技術力と戦略的なUI/UXを武器に、世の中に価値あるサービスを
生み出しているビズリーチ。
サービスの数が増えるにつれ、技術の幅が広がったため、そのスキルやノウハウ
を社内のみならず、
世の中のエンジニアやデザイナーとも共有すべく、
私たちは「D3(ディーキューブ)※」というプロジェクトチームを立ち上げま
した。
D3では、たくさんのイベントや勉強会を開催し、
世のエンジニア・デザイナーと共に、さらなる高みを目指します。
※D3=DESIGNER & DEVELOPER DIVISION
スタンバイ
• https://jp.stanby.com
• 400万件以上の求人をまとめて探せる日本最大級の求人検索エンジン。
• 企業公式サイトや求人サイトのありとあらゆる求人情報が探せます。
スタンバイ・カンパニー
• https://stanby.co/
• スタンバイ・カンパニーは、誰でもかんたんに無料で求人を作成できるサービスです。
• 作成した求人はスタンバイに掲載されます。
• また、応募者とチャットでやりとりをしたり、動画で面接を行うこともできます。
スタンバイアプリ
• 掲載求人数は400万件以上で日本最大級。
• 1000以上の求人・転職サイトや企業サイトを横断検索
• 正社員からアルバイト・パートまでのあらゆる働き方やこだわり条件で仕事を探すことができます。
• 自分のプロフィールを登録しておくと、お店や企業から仕事のお誘いが届きます。
• 興味があれば、そのままお店・企業の方とチャットでやりとりをしていただき、不明点などを気軽に聞くことができます。
• そこで働いてみたいと思ったら、そのままチャットで面接の日程調整をしたり、スタンバイアプリのビデオ通話機能を使えば、自宅にいながらスマホで面接もできます。
使っている技術(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…
スタンバイ
• ElasticSeachから求人情報を検索する
• クライアントはブラウザ(PC,Mobile)、Android、iOS
• ブラウザは別サーバー
• ユーザーの情報を管理する
• 履歴書、検索履歴、閲覧履歴
• スカウトされるための情報
• 求人広告を配信して稼いでいます。
• Yahoo!しごと検索にもAPI提供しています
クローラー
• 求人サイトや、企業HPの求人ページをクローリングして、ElasticSearchにインデックスします。
• よりよい検索結果をユーザーに提供できるように、求人の内容を学習しているようです。
サーバーアプリケーションのパッケージ構成
• 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_version\G
*************************** 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
使い方
• マッピングを自動生成する
package repositories.slick
import slick.jdbc.GetResultimport 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.Singletonimport repositories.slick.Tables._import slick.jdbc.MySQLProfile.api._
/*** Created by hiroyuki.ikawa on 2017/05/09.*/
@Singletonclass Players {
def getAllPlayers: DBIO[Seq[Player]] = {Players.result
}
}
Service
• レポジトリからデータを取得して、コントローラーに返す
• Future型で返す
package services
import com.google.inject.{Inject, Singleton}import repositories.slick.Playersimport repositories.slick.Tables.Playerimport slick.jdbc.MySQLProfile.api._
import scala.concurrent.Future
/*** Created by hiroyuki.ikawa on 2017/05/09.*/
@Singletonclass 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.*/
@Singletonclass 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.Singletonimport play.api.libs.ws.WS
import scala.concurrent.Future
/*** Created by hiroyuki.ikawa on 2017/05/09.*/
@Singletonclass OutsideApi {
def battingAverage(name: String): Future[Double] = {
// WSで他のAPIサービスからデータを取ってくる// WS.url("https://api.outside.internal/batting-average/:name")
// 今回はモックで適当に。Future.successful(0.321)
}
}
Service
• ネストが深くなってくる、、
@Singletonclass 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)
@Singletonclass 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式を使う• ネストがなくなって読みやすくなった
• 並列で実行できないか?
@Singletonclass 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が追加された
@Singletonclass 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の罠
• 全員のデータをいっぺんに取ってくる
@Singletonclass 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を実行する
@Singletonclass 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 <- playersFutureaverages <- 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で返す
@Singletonclass 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 <- playersFutureaveragesEither <- 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で返すようになったらどうする?
@Singletonclass 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式の中はスッキリしたけど、、、
@Singletonclass 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 <- playersFutureEitheraverages <- averagesFuture
} yield {// データを生成players.map(player => (player, averages(player.name)))
}}
}
FunctionalSyntaxHelper
• 全部importするとcompileが重くなるので、必要なパッケージだけimportしたtraitを用意しておいてそれだけを使うようにする
• scalazを便利に使えるようにする
package utils
import models.Errors
import scala.concurrent.Futureimport 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.EitherTimport scalaz.Scalaz._
@Singletonclass 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").toEitherTfor {
players <- playersFutureaverages <- averagesFuture
} yield {// データを生成players.map(player => (player, averages(player.name)))
}}
}
EitherT
• EitherTのTはTransformerのT
• トランスフォーマーは2つのモナドを組み合わせて別のモナドをつくるためのもの
• FutureとEitherから別のモナドを作っている
• なので、forできれいに書けた
もう少し
• 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も減らせる
@Singletonclass 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").toEitherTfor {
players <- playersFutureaverages <- averagesFuture
} yield {// データを生成players.map(player => (player, averages(player.name)))
}}
}
scalazおまけ
• ToBooleanOps
• if文を減らせる
trait FunctionalSyntaxHelper extends ToEitherOpswith ToBooleanOpswith 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 settingsgenerator.packageName=repositories.scalikejdbc.jdbc# generator.lineBreak: LF/CRLFgenerator.lineBreak=LF# generator.template: interpolation/queryDslgenerator.template=queryDsl# generator.testTemplate: specs2unit/specs2acceptance/ScalaTestFlatSpecgenerator.testTemplate=# File Encodinggenerator.encoding=UTF-8# When you're using Scala 2.11 or higher, you can use case classes for 22+ columns tablesgenerator.caseClassOnly=true# Set AutoSession for implicit DBSession parameter's default valuegenerator.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.Playersimport 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
}
}
servicepackage services
import com.google.inject.{Inject, Singleton}import models.Errorsimport repositories.api.OutsideApiimport repositories.scalikejdbc.PlayerRepositoryimport repositories.scalikejdbc.jdbc.Playersimport utils.FunctionalSyntaxHelper
import scala.concurrent.{ExecutionContext, Future}
/*** Created by hiroyuki.ikawa on 2017/05/09.*/
@Singletonclass 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についてあまり考えなくて良くなるので、アプリケーションの設計や、ビジネスロジックのことを考えられるようになる