diff --git a/app/Module.scala b/app/Module.scala index 51cf614..83f1a46 100644 --- a/app/Module.scala +++ b/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()) } } diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index f965d80..05aff42 100644 --- a/app/controllers/HomeController.scala +++ b/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"))) } } diff --git a/app/controllers/MonixHomeController.scala b/app/controllers/MonixHomeController.scala new file mode 100644 index 0000000..1eeeb74 --- /dev/null +++ b/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) + } + +} diff --git a/app/services/MyService.scala b/app/services/MyService.scala index 6b8a512..0dc9239 100644 --- a/app/services/MyService.scala +++ b/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 -} \ No newline at end of file +// import profile.api._ +// db +// } diff --git a/app/util/Schedulers.scala b/app/util/Schedulers.scala new file mode 100644 index 0000000..6ecae7a --- /dev/null +++ b/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 diff --git a/app/util/Util.scala b/app/util/Util.scala new file mode 100644 index 0000000..eba5e37 --- /dev/null +++ b/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)) + } + } + ) +} diff --git a/build.sbt b/build.sbt index 3f95e1e..daa47e7 100644 --- a/build.sbt +++ b/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) diff --git a/conf/application.conf b/conf/application.conf index 12e82d7..0705c64 100644 --- a/conf/application.conf +++ b/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 } \ No newline at end of file diff --git a/conf/logback.xml b/conf/logback.xml index de7e962..6abc32e 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -1,6 +1,6 @@ - + ${application.home:-.}/logs/application.log @@ -11,29 +11,31 @@ - %coloredLevel %logger{15} - %message%n%xException{10} + %date{HH:mm:ss.SSS} %thread %coloredLevel %logger{15} - %message%n%xException{10} - + - + - - + + - - - + + + - - - + + + + + diff --git a/conf/routes b/conf/routes index 2f57c98..8b7b1d8 100644 --- a/conf/routes +++ b/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) diff --git a/modules/api/src/main/scala/com/example/services/TaskLibraryService.scala b/modules/api/src/main/scala/com/example/services/TaskLibraryService.scala new file mode 100644 index 0000000..a8d73f5 --- /dev/null +++ b/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)]] +} diff --git a/modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql b/modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V2__add_user.sql b/modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V2__add_user.sql rename to modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql diff --git a/modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql b/modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V4__add_car.sql b/modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V4__add_car.sql rename to modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql diff --git a/modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql b/modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql b/modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql rename to modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql diff --git a/modules/flyway/src/main/resources/db/migration/default/V7__test.sql b/modules/flyway/src/main/resources/db/migration/default/V7__test.sql new file mode 100644 index 0000000..e69de29 diff --git a/modules/slick/src/main/resources/application.conf b/modules/slick/src/main/resources/application.conf index 2f644fe..e6d284b 100644 --- a/modules/slick/src/main/resources/application.conf +++ b/modules/slick/src/main/resources/application.conf @@ -23,7 +23,8 @@ myapp = { connectionTimeout = 5000 validationTimeout = 5000 - connectionPool=disabled + connectionPool = disabled + keepAlive = true } } diff --git a/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala b/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala index e66f64d..57cc29e 100644 --- a/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala +++ b/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() = { diff --git a/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala b/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala index e7a62d4..98f9e27 100644 --- a/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala +++ b/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]] = diff --git a/modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala b/modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala new file mode 100644 index 0000000..1c3693b --- /dev/null +++ b/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) + // ) + // } + // }) + // ) + // } +}