Browse Source

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
master
Rohan Sircar 4 years ago
parent
commit
cb9524eac7
  1. 31
      app/Module.scala
  2. 25
      app/controllers/HomeController.scala
  3. 34
      app/controllers/MonixHomeController.scala
  4. 28
      app/services/MyService.scala
  5. 18
      app/util/Schedulers.scala
  6. 58
      app/util/Util.scala
  7. 32
      build.sbt
  8. 14
      conf/application.conf
  9. 26
      conf/logback.xml
  10. 3
      conf/routes
  11. 20
      modules/api/src/main/scala/com/example/services/TaskLibraryService.scala
  12. 0
      modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql
  13. 0
      modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql
  14. 0
      modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql
  15. 0
      modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql
  16. 0
      modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql
  17. 0
      modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql
  18. 0
      modules/flyway/src/main/resources/db/migration/default/V7__test.sql
  19. 3
      modules/slick/src/main/resources/application.conf
  20. 12
      modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala
  21. 15
      modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala
  22. 97
      modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala

31
app/Module.scala

@ -13,6 +13,11 @@ import com.example.user.CarDAO
import com.example.Car.slick.SlickCarDAO
import com.example.services.LibraryService
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.
@ -23,9 +28,13 @@ class Module(environment: Environment, configuration: Configuration)
extends AbstractModule {
override def configure(): Unit = {
bind(classOf[Database]).toProvider(classOf[DatabaseProvider])
bind(classOf[JdbcProfile]).toProvider(classOf[JdbcProfileProvider])
bind(classOf[UserDAO]).to(classOf[SlickUserDAO])
bind(classOf[CarDAO]).to(classOf[SlickCarDAO])
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()
}
}
@ -35,7 +44,27 @@ class DatabaseProvider @Inject() (config: Config) extends Provider[Database] {
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) {
lifecycle.addStopHook { () => Future.successful(db.close()) }
}

25
app/controllers/HomeController.scala

@ -9,21 +9,28 @@ import scala.concurrent.ExecutionContext
import com.example.user.CarDAO
import com.example.services.LibraryService
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
class HomeController @Inject() (
userDAO: UserDAO,
carDAO: CarDAO,
libraryService: LibraryService,
taskLibraryService: TaskLibraryService,
cc: ControllerComponents
)(implicit ec: ExecutionContext)
extends AbstractController(cc) {
def index = Action.async { implicit request =>
def index = Action.async {
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)) }
}
@ -36,10 +43,16 @@ class HomeController @Inject() (
} yield (Ok(Json.toJson(maybeBook)))
}
def authors(bookId: Long) = Action.async { implicit request =>
libraryService.getAuthorsForBook(bookId).map( t => {
Ok(Json.toJson(t))
})
def authors(bookId: Long) = Action.async {
libraryService
.getAuthorsForBook(bookId)
.map(t => {
Ok(Json.toJson(t))
})
}
def user = Action {
Ok(Json.toJson(UserForm("hello")))
}
}

34
app/controllers/MonixHomeController.scala

@ -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)
}
}

28
app/services/MyService.scala

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

18
app/util/Schedulers.scala

@ -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

@ -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))
}
}
)
}

32
build.sbt

@ -20,9 +20,8 @@ resolvers in ThisBuild += Resolver.sonatypeRepo("snapshots")
libraryDependencies in ThisBuild ++= Seq(
"javax.inject" % "javax.inject" % "1",
// "joda-time" % "joda-time" % "2.10.2",
// "org.joda" % "joda-convert" % "2.2.1",
"com.google.inject" % "guice" % "4.2.3"
"com.google.inject" % "guice" % "4.2.3",
"org.flywaydb" %% "flyway-play" % "6.0.0"
)
scalaVersion in ThisBuild := "2.13.1"
@ -35,13 +34,13 @@ scalacOptions in ThisBuild ++= Seq(
"-Xlint",
"-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"))
.enablePlugins(FlywayPlugin)
.settings(
libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion,
flywayLocations := Seq("classpath:db/migration"),
flywayLocations := Seq("classpath:conf/db/migration/default"),
flywayUrl := databaseUrl,
flywayUser := databaseUser,
flywayPassword := databasePassword,
@ -50,8 +49,13 @@ lazy val flyway = (project in file("modules/flyway"))
lazy val api = (project in file("modules/api")).settings(
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"))
@ -62,8 +66,12 @@ lazy val slick = (project in file("modules/slick"))
"com.typesafe.slick" %% "slick" % "3.3.2",
"com.typesafe.slick" %% "slick-hikaricp" % "3.3.2",
"io.bfil" %% "automapper" % "0.7.0",
"io.scalaland" %% "chimney" % "0.5.2"
// "com.github.tototoshi" %% "slick-joda-mapper" % "2.4.1"
"io.scalaland" %% "chimney" % "0.5.2",
"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,
slickCodegenDatabaseUser := databaseUser,
@ -74,9 +82,6 @@ lazy val slick = (project in file("modules/slick"))
slickCodegenExcludedTables := Seq("schema_version"),
slickCodegenCodeGenerator := { (model: m.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 Column = new Column(_) {
override def rawType = model.tpe match {
@ -104,7 +109,6 @@ lazy val root = (project in file("."))
"com.h2database" % "h2" % "1.4.199",
ws % Test,
"org.flywaydb" % "flyway-core" % FlywayVersion % Test,
"com.typesafe.play" %% "play-slick" % "5.0.0",
"org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test,
"com.typesafe.play" %% "play-json" % "2.8.1",
"io.bfil" %% "automapper" % "0.7.0"
@ -113,4 +117,4 @@ lazy val root = (project in file("."))
)
.aggregate(slick)
.dependsOn(slick)
// fork := true
.dependsOn(flyway)

14
conf/application.conf

@ -5,4 +5,18 @@ myapp = {
numThreads=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
}

26
conf/logback.xml

@ -1,6 +1,6 @@
<configuration>
<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel"/>
<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home:-.}/logs/application.log</file>
@ -11,29 +11,31 @@
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<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>
</appender>
<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<appender-ref ref="FILE" />
</appender>
<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT"/>
<appender-ref ref="STDOUT" />
</appender>
<logger name="play" level="INFO"/>
<logger name="application" level="INFO"/>
<logger name="play" level="DEBUG" />
<logger name="application" level="DEBUG" />
<!-- Useful debugging settings in slick -->
<logger name="slick.jdbc.JdbcBackend.statement" level="INFO"/>
<logger name="slick.jdbc.JdbcBackend.benchmark" level="INFO"/>
<logger name="com.zaxxer.hikari" level="WARN"/>
<logger name="slick.jdbc.JdbcBackend.statement" level="INFO" />
<logger name="slick.jdbc.JdbcBackend.benchmark" level="INFO" />
<logger name="com.zaxxer.hikari" level="INFO" />
<root level="WARN">
<appender-ref ref="ASYNCFILE"/>
<appender-ref ref="ASYNCSTDOUT"/>
<logger name="controllers" level="DEBUG" />
<root level="INFO">
<appender-ref ref="ASYNCFILE" />
<appender-ref ref="ASYNCSTDOUT" />
</root>
</configuration>

3
conf/routes

@ -6,7 +6,8 @@
GET / controllers.HomeController.index
GET /cars controllers.HomeController.cars
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
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

20
modules/api/src/main/scala/com/example/services/TaskLibraryService.scala

@ -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)]]
}

0
modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql → modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql

0
modules/flyway/src/main/resources/db/migration/V2__add_user.sql → modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql

0
modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql → modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql

0
modules/flyway/src/main/resources/db/migration/V4__add_car.sql → modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql

0
modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql → modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql

0
modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql → modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql

0
modules/flyway/src/main/resources/db/migration/default/V7__test.sql

3
modules/slick/src/main/resources/application.conf

@ -23,7 +23,8 @@ myapp = {
connectionTimeout = 5000
validationTimeout = 5000
connectionPool=disabled
connectionPool = disabled
keepAlive = true
}
}

12
modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala

@ -5,13 +5,11 @@ import com.example.models._
import io.scalaland.chimney.dsl._
import com.example.user.slick.Tables
import javax.inject.Singleton
// import slick.jdbc.H2Profile.api._
// import scala.concurrent.ExecutionContext
import javax.inject._
@Singleton
class SlickLibraryDbio extends Tables {
override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
class SlickLibraryDbio @Inject() (override val profile: JdbcProfile)
extends Tables {
import profile.api._
@ -110,12 +108,12 @@ class SlickLibraryDbio extends Tables {
}
} yield (author, book)
lazy val authorOfBook4 = (bookId: Long) =>
lazy val authorOfBook4 = (bookId: Long) =>
for {
book <- Books
author <- book.authorsFk
} yield (author, book)
}
case class BookWithoutId(title: String, authorId: Long)
// def test() = {

15
modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala

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

97
modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala

@ -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)
// )
// }
// })
// )
// }
}
Loading…
Cancel
Save