Many changes

Rewrote slick db service using monix task instead of future
Added cats IO to controller to make use of above db service
Added action methods that support effect wrappers other than future
Added play-flyway to auto-manage migrations
Moved migration files to appropriate directory as a result of above
Added cors filter to work with separately hosted frontend
Made logger log at debug
This commit is contained in:
Rohan Sircar 2020-08-22 16:18:11 +05:30
parent a9c6cfc96c
commit cb9524eac7
22 changed files with 353 additions and 63 deletions

View File

@ -13,6 +13,11 @@ import com.example.user.CarDAO
import com.example.Car.slick.SlickCarDAO import com.example.Car.slick.SlickCarDAO
import com.example.services.LibraryService import com.example.services.LibraryService
import com.example.user.slick.services.SlickLibraryService import com.example.user.slick.services.SlickLibraryService
import com.example.services.TaskLibraryService
import com.example.user.slick.services.TaskSlickLibraryService
import monix.execution.Scheduler
import util.{Schedulers, SchedulersImpl}
import slick.jdbc.JdbcProfile
/** /**
* This module handles the bindings for the API to the Slick implementation. * This module handles the bindings for the API to the Slick implementation.
@ -23,9 +28,13 @@ class Module(environment: Environment, configuration: Configuration)
extends AbstractModule { extends AbstractModule {
override def configure(): Unit = { override def configure(): Unit = {
bind(classOf[Database]).toProvider(classOf[DatabaseProvider]) bind(classOf[Database]).toProvider(classOf[DatabaseProvider])
bind(classOf[JdbcProfile]).toProvider(classOf[JdbcProfileProvider])
bind(classOf[UserDAO]).to(classOf[SlickUserDAO]) bind(classOf[UserDAO]).to(classOf[SlickUserDAO])
bind(classOf[CarDAO]).to(classOf[SlickCarDAO]) bind(classOf[CarDAO]).to(classOf[SlickCarDAO])
bind(classOf[LibraryService]).to(classOf[SlickLibraryService]) bind(classOf[LibraryService]).to(classOf[SlickLibraryService])
bind(classOf[TaskLibraryService]).to(classOf[TaskSlickLibraryService])
bind(classOf[Scheduler]).toProvider(classOf[SchedulerProvider])
bind(classOf[Schedulers]).toProvider(classOf[AppSchedulersProvider])
bind(classOf[DBCloseHook]).asEagerSingleton() bind(classOf[DBCloseHook]).asEagerSingleton()
} }
} }
@ -35,7 +44,27 @@ class DatabaseProvider @Inject() (config: Config) extends Provider[Database] {
lazy val get = Database.forConfig("myapp.database", config) lazy val get = Database.forConfig("myapp.database", config)
} }
/** Closes database connections safely. Important on dev restart. */ @Singleton
class SchedulerProvider() extends Provider[Scheduler] {
lazy val get = Scheduler.io()
}
@Singleton
class AppSchedulersProvider() extends Provider[Schedulers] {
lazy val get =
new SchedulersImpl(
dbScheduler = Scheduler.io(),
cpuScheduler = Scheduler.global
)
}
@Singleton
class JdbcProfileProvider() extends Provider[JdbcProfile] {
lazy val get: JdbcProfile = _root_.slick.jdbc.H2Profile
}
@Singleton
class DBCloseHook @Inject() (db: Database, lifecycle: ApplicationLifecycle) { class DBCloseHook @Inject() (db: Database, lifecycle: ApplicationLifecycle) {
lifecycle.addStopHook { () => Future.successful(db.close()) } lifecycle.addStopHook { () => Future.successful(db.close()) }
} }

View File

@ -9,21 +9,28 @@ import scala.concurrent.ExecutionContext
import com.example.user.CarDAO import com.example.user.CarDAO
import com.example.services.LibraryService import com.example.services.LibraryService
import play.api.libs.json.Json import play.api.libs.json.Json
import com.example.services.TaskLibraryService
case class UserForm(name: String)
object UserForm {
implicit val userFormFormat = Json.format[UserForm]
}
@Singleton @Singleton
class HomeController @Inject() ( class HomeController @Inject() (
userDAO: UserDAO, userDAO: UserDAO,
carDAO: CarDAO, carDAO: CarDAO,
libraryService: LibraryService, libraryService: LibraryService,
taskLibraryService: TaskLibraryService,
cc: ControllerComponents cc: ControllerComponents
)(implicit ec: ExecutionContext) )(implicit ec: ExecutionContext)
extends AbstractController(cc) { extends AbstractController(cc) {
def index = Action.async { implicit request => def index = Action.async {
userDAO.all.map { users => Ok(views.html.index(users)) } userDAO.all.map { users => Ok(views.html.index(users)) }
} }
def cars = Action.async { implicit request => def cars = Action.async {
carDAO.all.map { cars => Ok(views.html.cars(cars)) } carDAO.all.map { cars => Ok(views.html.cars(cars)) }
} }
@ -36,10 +43,16 @@ class HomeController @Inject() (
} yield (Ok(Json.toJson(maybeBook))) } yield (Ok(Json.toJson(maybeBook)))
} }
def authors(bookId: Long) = Action.async { implicit request => def authors(bookId: Long) = Action.async {
libraryService.getAuthorsForBook(bookId).map( t => { libraryService
.getAuthorsForBook(bookId)
.map(t => {
Ok(Json.toJson(t)) Ok(Json.toJson(t))
}) })
} }
def user = Action {
Ok(Json.toJson(UserForm("hello")))
}
} }

View File

@ -0,0 +1,34 @@
package controllers
import javax.inject.{Inject, Singleton}
import play.api.mvc._
import play.api.libs.json.Json
import com.example.services.TaskLibraryService
import util.IOHttp._
import com.typesafe.scalalogging.LazyLogging
import cats.effect.IO
import scala.concurrent.ExecutionContext
import util.Schedulers
@Singleton
class MonixHomeController @Inject() (
taskLibraryService: TaskLibraryService,
cc: ControllerComponents
)(schedulers: Schedulers, ec: ExecutionContext)
extends AbstractController(cc)
with LazyLogging {
private implicit val defaultScheduler = schedulers.dbScheduler
def authors(bookId: Long) = Action.asyncF {
for {
_ <- IO(logger.debug("Getting Authors"))
authors <- taskLibraryService.getAuthorsForBook(bookId).to[IO]
_ <- IO(logger.debug("Where am I now?"))
_ <- IO.shift(ec)
_ <- IO(logger.info("Got Authors"))
res <- IO.pure(Ok(Json.toJson(authors)))
} yield (res)
}
}

View File

@ -1,17 +1,17 @@
package service // package service
import javax.inject._ // import javax.inject._
import play.api.db.slick.DatabaseConfigProvider // import play.api.db.slick.DatabaseConfigProvider
import scala.concurrent.ExecutionContext // import scala.concurrent.ExecutionContext
import play.api.db.slick.HasDatabaseConfigProvider // import play.api.db.slick.HasDatabaseConfigProvider
import slick.jdbc.JdbcProfile // import slick.jdbc.JdbcProfile
@Singleton // @Singleton
class MyService @Inject() ( // class MyService @Inject() (
protected val dbConfigProvider: DatabaseConfigProvider, // protected val dbConfigProvider: DatabaseConfigProvider,
implicit val ec: ExecutionContext // implicit val ec: ExecutionContext
) extends HasDatabaseConfigProvider[JdbcProfile] { // ) extends HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._ // import profile.api._
db // db
} // }

18
app/util/Schedulers.scala Normal file
View File

@ -0,0 +1,18 @@
package util
import monix.execution.Scheduler
/**
* Class containing various schedulers for offloading blocking tasks
* from play's default thread pool
*
* @param dbScheduler
* @param cpuScheduler
*/
trait Schedulers {
def dbScheduler: Scheduler
def cpuScheduler: Scheduler
}
class SchedulersImpl(val dbScheduler: Scheduler, val cpuScheduler: Scheduler)
extends Schedulers

58
app/util/Util.scala Normal file
View File

@ -0,0 +1,58 @@
package util
import scala.concurrent.Future
import cats.effect.IO
import cats.implicits._
import scala.util.Success
import scala.util.Failure
import cats.effect.Effect
import play.api.mvc._
import scala.concurrent.ExecutionContext
object IOHttp {
/**
* Actions to convert an effect wrapper F such as cats IO
* or monix Task into a future implicitly
*
* @param ab
*/
implicit class ActionBuilderOps[+R[_], B](ab: ActionBuilder[R, B]) {
import cats.effect.implicits._
/**
* Action to convert an effect wrapper F such as cats IO
* or monix Task into a future implicitly
*
* @param ab
*/
def asyncFR[F[_]: Effect](cb: R[B] => F[Result]): Action[B] = ab.async {
c => cb(c).toIO.unsafeToFuture()
}
/**
* Action to convert an effect wrapper F such as cats IO
* or monix Task into a future implicitly
*
* @param ab
*/
def asyncF[F[_]: Effect](cb: => F[Result]): Action[AnyContent] =
ab.async { cb.toIO.unsafeToFuture() }
}
}
object RepoUtil {
def fromFuture[IOEffect[A], A](
f: => Future[A]
)(implicit ec: ExecutionContext): IO[A] =
IO.delay(f) >>= (f =>
IO.async[A] { cb =>
f.onComplete {
case Success(a) => cb(Right(a))
case Failure(ex) => cb(Left(ex))
}
}
)
}

View File

@ -20,9 +20,8 @@ resolvers in ThisBuild += Resolver.sonatypeRepo("snapshots")
libraryDependencies in ThisBuild ++= Seq( libraryDependencies in ThisBuild ++= Seq(
"javax.inject" % "javax.inject" % "1", "javax.inject" % "javax.inject" % "1",
// "joda-time" % "joda-time" % "2.10.2", "com.google.inject" % "guice" % "4.2.3",
// "org.joda" % "joda-convert" % "2.2.1", "org.flywaydb" %% "flyway-play" % "6.0.0"
"com.google.inject" % "guice" % "4.2.3"
) )
scalaVersion in ThisBuild := "2.13.1" scalaVersion in ThisBuild := "2.13.1"
@ -35,13 +34,13 @@ scalacOptions in ThisBuild ++= Seq(
"-Xlint", "-Xlint",
"-Ywarn-numeric-widen" "-Ywarn-numeric-widen"
) )
javacOptions in ThisBuild ++= Seq("-source", "1.8", "-target", "1.8") javacOptions in ThisBuild ++= Seq("-source", "11", "-target", "11")
lazy val flyway = (project in file("modules/flyway")) lazy val flyway = (project in file("modules/flyway"))
.enablePlugins(FlywayPlugin) .enablePlugins(FlywayPlugin)
.settings( .settings(
libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion, libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion,
flywayLocations := Seq("classpath:db/migration"), flywayLocations := Seq("classpath:conf/db/migration/default"),
flywayUrl := databaseUrl, flywayUrl := databaseUrl,
flywayUser := databaseUser, flywayUser := databaseUser,
flywayPassword := databasePassword, flywayPassword := databasePassword,
@ -51,7 +50,12 @@ lazy val flyway = (project in file("modules/flyway"))
lazy val api = (project in file("modules/api")).settings( lazy val api = (project in file("modules/api")).settings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % "2.8.1", "com.typesafe.play" %% "play-json" % "2.8.1",
), "org.typelevel" %% "cats-core" % "2.1.1",
"org.typelevel" %% "cats-effect" % "2.1.4",
"io.monix" %% "monix" % "3.2.2",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"ch.qos.logback" % "logback-classic" % "1.2.3"
)
) )
lazy val slick = (project in file("modules/slick")) lazy val slick = (project in file("modules/slick"))
@ -62,8 +66,12 @@ lazy val slick = (project in file("modules/slick"))
"com.typesafe.slick" %% "slick" % "3.3.2", "com.typesafe.slick" %% "slick" % "3.3.2",
"com.typesafe.slick" %% "slick-hikaricp" % "3.3.2", "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2",
"io.bfil" %% "automapper" % "0.7.0", "io.bfil" %% "automapper" % "0.7.0",
"io.scalaland" %% "chimney" % "0.5.2" "io.scalaland" %% "chimney" % "0.5.2",
// "com.github.tototoshi" %% "slick-joda-mapper" % "2.4.1" "org.typelevel" %% "cats-core" % "2.1.1",
"org.typelevel" %% "cats-effect" % "2.1.4",
"io.monix" %% "monix" % "3.2.2",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"ch.qos.logback" % "logback-classic" % "1.2.3"
), ),
slickCodegenDatabaseUrl := databaseUrl, slickCodegenDatabaseUrl := databaseUrl,
slickCodegenDatabaseUser := databaseUser, slickCodegenDatabaseUser := databaseUser,
@ -74,9 +82,6 @@ lazy val slick = (project in file("modules/slick"))
slickCodegenExcludedTables := Seq("schema_version"), slickCodegenExcludedTables := Seq("schema_version"),
slickCodegenCodeGenerator := { (model: m.Model) => slickCodegenCodeGenerator := { (model: m.Model) =>
new SourceCodeGenerator(model) { new SourceCodeGenerator(model) {
// override def code =
// "import com.github.tototoshi.slick.H2JodaSupport._\n" + "import org.joda.time.DateTime\n" + super.code
override def Table = new Table(_) { override def Table = new Table(_) {
override def Column = new Column(_) { override def Column = new Column(_) {
override def rawType = model.tpe match { override def rawType = model.tpe match {
@ -104,7 +109,6 @@ lazy val root = (project in file("."))
"com.h2database" % "h2" % "1.4.199", "com.h2database" % "h2" % "1.4.199",
ws % Test, ws % Test,
"org.flywaydb" % "flyway-core" % FlywayVersion % Test, "org.flywaydb" % "flyway-core" % FlywayVersion % Test,
"com.typesafe.play" %% "play-slick" % "5.0.0",
"org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test, "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test,
"com.typesafe.play" %% "play-json" % "2.8.1", "com.typesafe.play" %% "play-json" % "2.8.1",
"io.bfil" %% "automapper" % "0.7.0" "io.bfil" %% "automapper" % "0.7.0"
@ -113,4 +117,4 @@ lazy val root = (project in file("."))
) )
.aggregate(slick) .aggregate(slick)
.dependsOn(slick) .dependsOn(slick)
// fork := true .dependsOn(flyway)

View File

@ -6,3 +6,17 @@ myapp = {
maxConnections=20 maxConnections=20
} }
} }
play.filters.enabled += "play.filters.cors.CORSFilter"
play.modules.enabled += "org.flywaydb.play.PlayModule"
db.default.driver=${myapp.database.driver}
db.default.url=${myapp.database.url}
db.default.username=${myapp.database.user}
db.default.password=${myapp.database.password}
play.filters.cors {
allowedHttpMethods = ["GET", "POST", "OPTION"]
allowedHttpHeaders = ["Accept"]
preflightMaxAge = 3 days
}

View File

@ -11,7 +11,7 @@
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%coloredLevel %logger{15} - %message%n%xException{10}</pattern> <pattern>%date{HH:mm:ss.SSS} %thread %coloredLevel %logger{15} - %message%n%xException{10}</pattern>
</encoder> </encoder>
</appender> </appender>
@ -23,15 +23,17 @@
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</appender> </appender>
<logger name="play" level="INFO"/> <logger name="play" level="DEBUG" />
<logger name="application" level="INFO"/> <logger name="application" level="DEBUG" />
<!-- Useful debugging settings in slick --> <!-- Useful debugging settings in slick -->
<logger name="slick.jdbc.JdbcBackend.statement" level="INFO" /> <logger name="slick.jdbc.JdbcBackend.statement" level="INFO" />
<logger name="slick.jdbc.JdbcBackend.benchmark" level="INFO" /> <logger name="slick.jdbc.JdbcBackend.benchmark" level="INFO" />
<logger name="com.zaxxer.hikari" level="WARN"/> <logger name="com.zaxxer.hikari" level="INFO" />
<root level="WARN"> <logger name="controllers" level="DEBUG" />
<root level="INFO">
<appender-ref ref="ASYNCFILE" /> <appender-ref ref="ASYNCFILE" />
<appender-ref ref="ASYNCSTDOUT" /> <appender-ref ref="ASYNCSTDOUT" />
</root> </root>

View File

@ -6,7 +6,8 @@
GET / controllers.HomeController.index GET / controllers.HomeController.index
GET /cars controllers.HomeController.cars GET /cars controllers.HomeController.cars
GET /book controllers.HomeController.book GET /book controllers.HomeController.book
GET /authors/:bookId controllers.HomeController.authors(bookId: Long) GET /authors/:bookId controllers.MonixHomeController.authors(bookId: Long)
GET /user controllers.HomeController.user
# Map static resources from the /public folder to the /assets URL path # Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

View File

@ -0,0 +1,20 @@
package com.example.services
import monix.eval.Task
import com.example.models.Book
import com.example.models.Author
import com.example.models.NewAuthor
import com.example.models.NewBook
trait TaskLibraryService {
// Simple function that returns a book
def findBookById(id: Long): Task[Option[Book]]
// Simple function that returns a list of books with it's author
def findBooksWithAuthor: Task[Seq[(Book, Author)]]
// Insert a book and an author composing two DBIOs in a transaction
def insertBookAndAuthor(book: NewBook, author: NewAuthor): Task[(Long, Long)]
def getAuthorsForBook(id: Long): Task[Seq[(Author, Book)]]
}

View File

@ -24,6 +24,7 @@ myapp = {
validationTimeout = 5000 validationTimeout = 5000
connectionPool = disabled connectionPool = disabled
keepAlive = true
} }
} }

View File

@ -5,13 +5,11 @@ import com.example.models._
import io.scalaland.chimney.dsl._ import io.scalaland.chimney.dsl._
import com.example.user.slick.Tables import com.example.user.slick.Tables
import javax.inject.Singleton import javax.inject.Singleton
// import slick.jdbc.H2Profile.api._ import javax.inject._
// import scala.concurrent.ExecutionContext
@Singleton @Singleton
class SlickLibraryDbio extends Tables { class SlickLibraryDbio @Inject() (override val profile: JdbcProfile)
extends Tables {
override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
import profile.api._ import profile.api._

View File

@ -16,10 +16,13 @@ class SlickLibraryService @Inject() (
libraryDbio: SlickLibraryDbio libraryDbio: SlickLibraryDbio
)(implicit ec: ExecutionContext) )(implicit ec: ExecutionContext)
extends LibraryService { extends LibraryService {
import libraryDbio.profile.api._
override def getAuthorsForBook(id: Long): Future[Seq[(Author, Book)]] = { override def getAuthorsForBook(id: Long): Future[Seq[(Author, Book)]] = {
db.run { db.run {
libraryDbio.getAuthorsForBook(id).map(_.map { libraryDbio
.getAuthorsForBook(id)
.map(_.map {
case (x, y) => { case (x, y) => {
(libraryDbio.authorsRowToAuthor(x), libraryDbio.booksRowToBooks(y)) (libraryDbio.authorsRowToAuthor(x), libraryDbio.booksRowToBooks(y))
} }
@ -27,8 +30,6 @@ class SlickLibraryService @Inject() (
} }
} }
import libraryDbio.profile.api._
// Simple function that returns a book // Simple function that returns a book
def findBookById(id: Long): Future[Option[Book]] = def findBookById(id: Long): Future[Option[Book]] =
db.run(libraryDbio.findBookById(id).map(_.map(libraryDbio.booksRowToBooks))) db.run(libraryDbio.findBookById(id).map(_.map(libraryDbio.booksRowToBooks)))

View File

@ -0,0 +1,97 @@
package com.example.user.slick.services
import javax.inject._
// import slick.jdbc.JdbcProfile
import slick.jdbc.JdbcBackend.Database
import com.example.models._
import com.example.user.slick.dbios.SlickLibraryDbio
import monix.eval.Task
import com.example.services.TaskLibraryService
@Singleton
class TaskSlickLibraryService @Inject() (
db: Database,
libraryDbio: SlickLibraryDbio
) extends TaskLibraryService {
import libraryDbio.profile.api._
override def findBookById(id: Long): Task[Option[Book]] =
Task
.deferFuture {
db.run(
libraryDbio.findBookById(id)
)
}
.flatMap(record => Task.eval(record.map(libraryDbio.booksRowToBooks)))
override def findBooksWithAuthor: Task[Seq[(Book, Author)]] =
Task.deferFuture(db.run(libraryDbio.findBooksWithAuthor)).flatMap {
records =>
Task.traverse(records) {
case (x, y) =>
Task.eval {
(
libraryDbio.booksRowToBooks(x),
libraryDbio.authorsRowToAuthor(y)
)
}
}
}
// override def storeMulti(aggregates: Seq[AggregateType]): Task[Long] =
// for {
// records <- Task.traverse(aggregates) { aggregate =>
// convertToRecord(aggregate)
// }
// result <- Task.deferFutureAction { implicit ec =>
// import profile.api._
// db.run(DBIO.sequence(records.foldLeft(Seq.empty[DBIO[Long]]) {
// case (result, record) =>
// result :+ dao.insertOrUpdate(record).map(_.toLong)
// }))
// .map(_.sum)
// }
// } yield result
override def insertBookAndAuthor(
book: NewBook,
author: NewAuthor
): Task[(Long, Long)] =
Task.deferFutureAction { implicit scheduler =>
val action = for {
authorId <- libraryDbio.insertAuthor2(author)
bookId <- libraryDbio.insertBook2(book.copy(authorId = authorId))
} yield (bookId, authorId)
db.run(action.transactionally)
}
override def getAuthorsForBook(id: Long): Task[Seq[(Author, Book)]] =
for {
records <- Task.deferFuture(db.run(libraryDbio.getAuthorsForBook((id))))
result <- Task.traverse(records) {
case (authorsRow, booksRow) =>
Task.eval(
(
libraryDbio.authorsRowToAuthor(authorsRow),
libraryDbio.booksRowToBooks(booksRow)
)
)
}
} yield (result)
// Task.deferFutureAction { implicit scheduler =>
// db.run(
// libraryDbio
// .getAuthorsForBook(id)
// .map(_.map {
// case (x, y) => {
// (
// libraryDbio.authorsRowToAuthor(x),
// libraryDbio.booksRowToBooks(y)
// )
// }
// })
// )
// }
}