Browse Source

first commit

master
nova 4 years ago
commit
2956829f9d
  1. 6
      .gitignore
  2. 1
      .scalafmt.conf
  3. 43
      README.md
  4. 41
      app/Module.scala
  5. 39
      app/controllers/HomeController.scala
  6. 17
      app/services/MyService.scala
  7. 20
      app/views/cars.scala.html
  8. 22
      app/views/index.scala.html
  9. 15
      app/views/main.scala.html
  10. 116
      build.sbt
  11. 8
      conf/application.conf
  12. 39
      conf/logback.xml
  13. 11
      conf/routes
  14. 31
      logs/application.log
  15. 14
      modules/api/src/main/scala/com/example/models/Library.scala
  16. 26
      modules/api/src/main/scala/com/example/services/LibraryService.scala
  17. 28
      modules/api/src/main/scala/com/example/user/CarDAO.scala
  18. 30
      modules/api/src/main/scala/com/example/user/UserDAO.scala
  19. 6
      modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql
  20. 6
      modules/flyway/src/main/resources/db/migration/V2__add_user.sql
  21. 6
      modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql
  22. 6
      modules/flyway/src/main/resources/db/migration/V4__add_car.sql
  23. 12
      modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql
  24. 14
      modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql
  25. 30
      modules/slick/src/main/resources/application.conf
  26. 69
      modules/slick/src/main/scala/com/example/user/slick/SlickCarDAO.scala
  27. 67
      modules/slick/src/main/scala/com/example/user/slick/SlickUserDAO.scala
  28. 103
      modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala
  29. 55
      modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala
  30. 1
      project/build.properties
  31. 4
      project/metals.sbt
  32. 14
      project/plugins.sbt
  33. BIN
      public/images/favicon.png
  34. 3
      public/javascripts/hello.js
  35. 0
      public/stylesheets/main.css
  36. 6
      scripts/test-sbt
  37. 2750
      test.trace.db
  38. 21
      test/controller/FunctionalSpec.scala
  39. 45
      test/controller/MyApplicationFactory.scala

6
.gitignore

@ -0,0 +1,6 @@
test.mv.db
.idea
.vscode
.bloop
.metals
target/

1
.scalafmt.conf

@ -0,0 +1 @@
version = "2.4.2"

43
README.md

@ -0,0 +1,43 @@
# Play with Slick 3.3
This project shows Play working with Slick.
This project is configured to keep all the modules self-contained.
* Slick is isolated from Play, not using play-slick.
* Database migration is done using [Flyway](https://flywaydb.org/), not Play Evolutions.
* Slick's classes are auto-generated following database migration.
## Database Migration
```bash
sbt flyway/flywayMigrate
```
## Slick Code Generation
You will need to run the flywayMigrate task first, and then you will be able to generate tables using sbt-codegen.
```bash
sbt slickCodegen
```
## Testing
You can run functional tests against an in memory database and Slick easily with Play from a clean slate:
```bash
sbt clean flyway/flywayMigrate slickCodegen compile test
```
## Running
To run the project, start up Play:
```bash
sbt run
```
And that's it!
Now go to <http://localhost:9000>, and you will see the list of users in the database.

41
app/Module.scala

@ -0,0 +1,41 @@
import javax.inject.{Inject, Provider, Singleton}
import com.example.user.UserDAO
import com.example.user.slick.SlickUserDAO
import com.google.inject.AbstractModule
import com.typesafe.config.Config
import play.api.inject.ApplicationLifecycle
import play.api.{Configuration, Environment}
import slick.jdbc.JdbcBackend.Database
import scala.concurrent.Future
import com.example.user.CarDAO
import com.example.Car.slick.SlickCarDAO
import com.example.services.LibraryService
import com.example.user.slick.services.SlickLibraryService
/**
* This module handles the bindings for the API to the Slick implementation.
*
* https://www.playframework.com/documentation/latest/ScalaDependencyInjection#Programmatic-bindings
*/
class Module(environment: Environment, configuration: Configuration)
extends AbstractModule {
override def configure(): Unit = {
bind(classOf[Database]).toProvider(classOf[DatabaseProvider])
bind(classOf[UserDAO]).to(classOf[SlickUserDAO])
bind(classOf[CarDAO]).to(classOf[SlickCarDAO])
bind(classOf[LibraryService]).to(classOf[SlickLibraryService])
bind(classOf[DBCloseHook]).asEagerSingleton()
}
}
@Singleton
class DatabaseProvider @Inject() (config: Config) extends Provider[Database] {
lazy val get = Database.forConfig("myapp.database", config)
}
/** Closes database connections safely. Important on dev restart. */
class DBCloseHook @Inject() (db: Database, lifecycle: ApplicationLifecycle) {
lifecycle.addStopHook { () => Future.successful(db.close()) }
}

39
app/controllers/HomeController.scala

@ -0,0 +1,39 @@
package controllers
import javax.inject.{Inject, Singleton}
import com.example.user.UserDAO
import play.api.mvc._
import scala.concurrent.ExecutionContext
import com.example.user.CarDAO
import com.example.services.LibraryService
import play.api.libs.json.Json
@Singleton
class HomeController @Inject() (userDAO: UserDAO, carDAO: CarDAO, libraryService: LibraryService, cc: ControllerComponents)
(implicit ec: ExecutionContext)
extends AbstractController(cc) {
def index = Action.async { implicit request =>
userDAO.all.map { users =>
Ok(views.html.index(users))
}
}
def cars = Action.async { implicit request =>
carDAO.all.map { cars =>
Ok(views.html.cars(cars))
}
}
def book = Action.async{
// libraryService.findBookById(1).map(e => Ok(Json.toJson(e)))
// libraryService.insertBookAndAuthor(Book("new book"), Author(2, "Some retard"))
for {
maybeBook <- libraryService.findBookById(1)
} yield (Ok(Json.toJson(maybeBook)))
}
}

17
app/services/MyService.scala

@ -0,0 +1,17 @@
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
@Singleton
class MyService @Inject() (
protected val dbConfigProvider: DatabaseConfigProvider,
implicit val ec: ExecutionContext
) extends HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
db
}

20
app/views/cars.scala.html

@ -0,0 +1,20 @@
@import com.example.user.Car
@(cars: Seq[Car])
@main("Cars Page") {
<h2>Cars</h2>
<table>
<tr>
<th>Id</th>
<th>Model</th>
</tr>
@for(car <- cars){
<tr>
<td>@car.id</td>
<td>@car.model</td>
</tr>
}
</table>
}

22
app/views/index.scala.html

@ -0,0 +1,22 @@
@(users: Seq[User])
@main("Title Page") {
<h2>Users</h2>
<table>
<tr>
<th>Id</th>
<th>Email</th>
<th>Created At</th>
<th>Updated At</th>
</tr>
@for(user <- users){
<tr>
<td>@user.id</td>
<td>@user.email</td>
<td>@user.createdAt</td>
<td>@user.updatedAt</td>
</tr>
}
</table>
}

15
app/views/main.scala.html

@ -0,0 +1,15 @@
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<script src="@routes.Assets.versioned("javascripts/hello.js")" type="text/javascript"></script>
</head>
<body>
@content
</body>
</html>

116
build.sbt

@ -0,0 +1,116 @@
import com.github.tototoshi.sbt.slick.CodegenPlugin.autoImport.{
slickCodegenDatabasePassword,
slickCodegenDatabaseUrl,
slickCodegenJdbcDriver
}
import play.core.PlayVersion.{current => playVersion}
import _root_.slick.codegen.SourceCodeGenerator
import _root_.slick.{model => m}
lazy val databaseUrl = sys.env.getOrElse("DB_DEFAULT_URL", "jdbc:h2:./test")
lazy val databaseUser = sys.env.getOrElse("DB_DEFAULT_USER", "sa")
lazy val databasePassword = sys.env.getOrElse("DB_DEFAULT_PASSWORD", "")
val FlywayVersion = "6.2.2"
version in ThisBuild := "1.1-SNAPSHOT"
resolvers in ThisBuild += Resolver.sonatypeRepo("releases")
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"
)
scalaVersion in ThisBuild := "2.13.1"
scalacOptions in ThisBuild ++= Seq(
"-encoding",
"UTF-8", // yes, this is 2 args
"-deprecation",
"-feature",
"-unchecked",
"-Xlint",
"-Ywarn-numeric-widen"
)
javacOptions in ThisBuild ++= Seq("-source", "1.8", "-target", "1.8")
lazy val flyway = (project in file("modules/flyway"))
.enablePlugins(FlywayPlugin)
.settings(
libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion,
flywayLocations := Seq("classpath:db/migration"),
flywayUrl := databaseUrl,
flywayUser := databaseUser,
flywayPassword := databasePassword,
flywayBaselineOnMigrate := true
)
lazy val api = (project in file("modules/api")).settings(
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % "2.8.1",
),
)
lazy val slick = (project in file("modules/slick"))
.enablePlugins(CodegenPlugin)
.settings(
libraryDependencies ++= Seq(
"com.zaxxer" % "HikariCP" % "3.4.2",
"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"
),
slickCodegenDatabaseUrl := databaseUrl,
slickCodegenDatabaseUser := databaseUser,
slickCodegenDatabasePassword := databasePassword,
slickCodegenDriver := _root_.slick.jdbc.H2Profile,
slickCodegenJdbcDriver := "org.h2.Driver",
slickCodegenOutputPackage := "com.example.user.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 {
case "java.sql.Timestamp" =>
"java.time.Instant" // kill j.s.Timestamp
case _ =>
super.rawType
}
}
}
}
},
sourceGenerators in Compile += slickCodegen.taskValue
)
.aggregate(api)
.dependsOn(api)
lazy val root = (project in file("."))
.enablePlugins(PlayScala)
.settings(
name := """play-scala-isolated-slick-example""",
TwirlKeys.templateImports += "com.example.user.User",
libraryDependencies ++= Seq(
guice,
"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"
),
fork in Test := true
)
.aggregate(slick)
.dependsOn(slick)
// fork := true

8
conf/application.conf

@ -0,0 +1,8 @@
http.port=8080
myapp = {
database = {
numThreads=20
maxConnections=20
}
}

39
conf/logback.xml

@ -0,0 +1,39 @@
<configuration>
<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>
<encoder>
<pattern>%date [%level] from %logger in %thread - %message%n%xException</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%coloredLevel %logger{15} - %message%n%xException{10}</pattern>
</encoder>
</appender>
<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT"/>
</appender>
<logger name="play" level="INFO"/>
<logger name="application" level="INFO"/>
<!-- 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"/>
<root level="WARN">
<appender-ref ref="ASYNCFILE"/>
<appender-ref ref="ASYNCSTDOUT"/>
</root>
</configuration>

11
conf/routes

@ -0,0 +1,11 @@
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.HomeController.index
GET /cars controllers.HomeController.cars
GET /book controllers.HomeController.book
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

31
logs/application.log

@ -0,0 +1,31 @@
2020-05-14 01:04:22,774 [INFO] from play.api.http.EnabledFilters in play-dev-mode-akka.actor.default-dispatcher-7 - Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>):
play.filters.csrf.CSRFFilter
play.filters.headers.SecurityHeadersFilter
play.filters.hosts.AllowedHostsFilter
2020-05-14 01:04:22,780 [INFO] from play.api.Play in play-dev-mode-akka.actor.default-dispatcher-7 - Application started (Dev) (no global state)
2020-05-14 01:04:41,915 [INFO] from play.api.http.EnabledFilters in play-dev-mode-akka.actor.default-dispatcher-7 - Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>):
play.filters.csrf.CSRFFilter
play.filters.headers.SecurityHeadersFilter
play.filters.hosts.AllowedHostsFilter
2020-05-14 01:04:41,917 [INFO] from play.api.Play in play-dev-mode-akka.actor.default-dispatcher-7 - Application started (Dev) (no global state)
2020-05-14 01:04:46,364 [INFO] from play.api.http.EnabledFilters in play-dev-mode-akka.actor.default-dispatcher-7 - Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>):
play.filters.csrf.CSRFFilter
play.filters.headers.SecurityHeadersFilter
play.filters.hosts.AllowedHostsFilter
2020-05-14 01:04:46,366 [INFO] from play.api.Play in play-dev-mode-akka.actor.default-dispatcher-7 - Application started (Dev) (no global state)
2020-05-14 10:15:22,298 [INFO] from play.core.server.AkkaHttpServer in play-dev-mode-shutdown-hook-1 - Stopping Akka HTTP server...
2020-05-14 10:15:22,300 [INFO] from play.core.server.AkkaHttpServer in play-dev-mode-akka.actor.internal-dispatcher-5 - Terminating server binding for /0:0:0:0:0:0:0:0:8080
2020-05-14 10:15:32,165 [INFO] from play.api.http.EnabledFilters in play-dev-mode-akka.actor.internal-dispatcher-15 - Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>):
play.filters.csrf.CSRFFilter
play.filters.headers.SecurityHeadersFilter
play.filters.hosts.AllowedHostsFilter
2020-05-14 10:15:32,176 [INFO] from play.api.Play in play-dev-mode-akka.actor.internal-dispatcher-15 - Application started (Dev) (no global state)
2020-05-14 10:15:32,221 [INFO] from play.core.server.AkkaHttpServer in play-dev-mode-akka.actor.internal-dispatcher-18 - Running provided shutdown stop hooks

14
modules/api/src/main/scala/com/example/models/Library.scala

@ -0,0 +1,14 @@
package com.example.models
import play.api.libs.json.Json
import java.time.Instant
final case class Book(id: Long, title: String, authorId: Long, createdAt: Instant)
final case class NewBook(title: String, authorId: Long)
final case class BookDTO(title: String, authorId: Long, createdAt: Instant)
final case class Author(id: Long, name: String)
final case class NewAuthor(name: String)
object Book {
implicit val bookJsonWrite = Json.format[Book]
}

26
modules/api/src/main/scala/com/example/services/LibraryService.scala

@ -0,0 +1,26 @@
package com.example.services
import scala.concurrent.Future
import com.example.models._
// trait LibraryService {
// def findBookById(id: Long): DBIO[Option[BooksRow]]
// def findBooksWithAuthor: DBIO[Seq[(BooksRow, AuthorsRow)]]
// def insertAuthor(author: Author): DBIO[AuthorsRow]
// def authorToRow(author: Author): AuthorsRow
// def bookToRow(book: Book): BooksRow
// }
trait LibraryService {
// Simple function that returns a book
def findBookById(id: Long): Future[Option[Book]]
// Simple function that returns a list of books with it's author
def findBooksWithAuthor: Future[Seq[(Book, Author)]]
// Insert a book and an author composing two DBIOs in a transaction
def insertBookAndAuthor(book: NewBook, author: NewAuthor): Future[(Long, Long)]
}

28
modules/api/src/main/scala/com/example/user/CarDAO.scala

@ -0,0 +1,28 @@
package com.example.user
import scala.concurrent.Future
import java.time.Instant
/**
* An implementation dependent DAO. This could be implemented by Slick, Cassandra, or a REST API.
*/
trait CarDAO {
def lookup(id: String): Future[Option[Car]]
def all: Future[Seq[Car]]
def update(user: Car): Future[Int]
def delete(id: String): Future[Int]
def create(user: Car): Future[Int]
def close(): Future[Unit]
}
/**
* Implementation independent aggregate root.
*/
case class Car(id: String, model: String, createdAt: Instant, updatedAt: Option[Instant])

30
modules/api/src/main/scala/com/example/user/UserDAO.scala

@ -0,0 +1,30 @@
package com.example.user
import scala.concurrent.Future
import java.time.Instant
// import com.example.Car.slick.SlickCarDAO
/**
* An implementation dependent DAO. This could be implemented by Slick, Cassandra, or a REST API.
*/
// @ImplementedBy(classOf[SlickCarDAO])
trait UserDAO {
def lookup(id: String): Future[Option[User]]
def all: Future[Seq[User]]
def update(user: User): Future[Int]
def delete(id: String): Future[Int]
def create(user: User): Future[Int]
def close(): Future[Unit]
}
/**
* Implementation independent aggregate root.
*/
case class User(id: String, email: String, createdAt: Instant, updatedAt: Option[Instant])

6
modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql

@ -0,0 +1,6 @@
create table "users" (
"id" VARCHAR(255) PRIMARY KEY NOT NULL,
"email" VARCHAR(1024) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NULL
);

6
modules/flyway/src/main/resources/db/migration/V2__add_user.sql

@ -0,0 +1,6 @@
INSERT INTO "users" VALUES (
'd074bce8-a8ca-49ec-9225-a50ffe83dc2f',
'myuser@example.com',
(TIMESTAMP '2013-03-26T17:50:06Z'),
(TIMESTAMP '2013-03-26T17:50:06Z')
);

6
modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql

@ -0,0 +1,6 @@
create table "cars" (
"id" VARCHAR(255) PRIMARY KEY NOT NULL,
"model" VARCHAR(1024) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NULL
);

6
modules/flyway/src/main/resources/db/migration/V4__add_car.sql

@ -0,0 +1,6 @@
INSERT INTO "cars" VALUES (
'd074bce8-a8ca-49ec-9225-a50ffe83dc2f',
'gxxer',
(TIMESTAMP '2013-03-26T17:50:06Z'),
(TIMESTAMP '2013-03-26T17:50:06Z')
);

12
modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql

@ -0,0 +1,12 @@
create table authors (
id IDENTITY PRIMARY KEY,
name VARCHAR(15) NOT NULL
);
create table books (
id IDENTITY PRIMARY KEY,
title VARCHAR(50) NOT NULL,
author_id BIGINT NOT NULL,
FOREIGN KEY(author_id) REFERENCES authors(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

14
modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql

@ -0,0 +1,14 @@
-- create table authors (
-- id INTEGER PRIMARY KEY NOT NULL,
-- name VARCHAR(15)
-- );
-- create table books (
-- id INTEGER PRIMARY KEY NOT NULL,
-- title VARCHAR(15) NOT NULL,
-- author_id INTEGER NOT NULL,
-- FOREIGN KEY(author_id) REFERENCES authors(id)
-- );
INSERT INTO authors (name) VALUES ('Jane Austen');
INSERT INTO books (title, author_id) VALUES ('Pride and Prejudice', 1);

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

@ -0,0 +1,30 @@
myapp = {
database = {
driver = org.h2.Driver
url = "jdbc:h2:./test"
user = "sa"
password = ""
// The number of threads determines how many things you can *run* in parallel
// the number of connections determines you many things you can *keep in memory* at the same time
// on the database server.
// numThreads = (core_count (hyperthreading included))
numThreads = 20
// queueSize = ((core_count * 2) + effective_spindle_count)
// on a MBP 13, this is 2 cores * 2 (hyperthreading not included) + 1 hard disk
queueSize = 10
// https://blog.knoldus.com/2016/01/01/best-practices-for-using-slick-on-production/
// make larger than numThreads + queueSize
maxConnections = 20
connectionTimeout = 5000
validationTimeout = 5000
connectionPool=disabled
}
}

69
modules/slick/src/main/scala/com/example/user/slick/SlickCarDAO.scala

@ -0,0 +1,69 @@
package com.example.Car.slick
import javax.inject.{Inject, Singleton}
// import org.joda.time.DateTime
import slick.jdbc.JdbcProfile
import slick.jdbc.JdbcBackend.Database
import com.example.user._
import java.time.Instant
import scala.concurrent.{ExecutionContext, Future}
import com.example.user.slick.Tables
/**
* A Car DAO implemented with Slick, leveraging Slick code gen.
*
* Note that you must run "flyway/flywayMigrate" before "compile" here.
*
* @param db the slick database that this Car DAO is using internally, bound through Module.
* @param ec a CPU bound execution context. Slick manages blocking JDBC calls with its
* own internal thread pool, so Play's default execution context is fine here.
*/
@Singleton
class SlickCarDAO @Inject()(db: Database)(implicit ec: ExecutionContext) extends CarDAO with Tables {
override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
import profile.api._
private val queryById = Compiled(
(id: Rep[String]) => Cars.filter(_.id === id))
def lookup(id: String): Future[Option[Car]] = {
val f: Future[Option[CarsRow]] = db.run(queryById(id).result.headOption)
f.map(maybeRow => maybeRow.map(carsRowToCar))
}
def all: Future[Seq[Car]] = {
val f = db.run(Cars.result)
f.map(seq => seq.map(carsRowToCar))
}
def update(car: Car): Future[Int] = {
db.run(queryById(car.id).update(carToCarsRow(car)))
}
def delete(id: String): Future[Int] = {
db.run(queryById(id).delete)
}
def create(car: Car): Future[Int] = {
db.run(
Cars += carToCarsRow(car.copy(createdAt = Instant.now()))
)
}
def close(): Future[Unit] = {
Future.successful(db.close())
}
private def carToCarsRow(car: Car): CarsRow = {
CarsRow(car.id, car.model, car.createdAt, car.updatedAt)
}
private def carsRowToCar(carsRow: CarsRow): Car = {
Car(carsRow.id, carsRow.model, carsRow.createdAt, carsRow.updatedAt)
}
}

67
modules/slick/src/main/scala/com/example/user/slick/SlickUserDAO.scala

@ -0,0 +1,67 @@
package com.example.user.slick
import javax.inject.{Inject, Singleton}
// import org.joda.time.DateTime
import slick.jdbc.JdbcProfile
import slick.jdbc.JdbcBackend.Database
import com.example.user._
import java.time.Instant
import scala.concurrent.{ExecutionContext, Future}
/**
* A User DAO implemented with Slick, leveraging Slick code gen.
*
* Note that you must run "flyway/flywayMigrate" before "compile" here.
*
* @param db the slick database that this user DAO is using internally, bound through Module.
* @param ec a CPU bound execution context. Slick manages blocking JDBC calls with its
* own internal thread pool, so Play's default execution context is fine here.
*/
@Singleton
class SlickUserDAO @Inject()(db: Database)(implicit ec: ExecutionContext) extends UserDAO with Tables {
override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
import profile.api._
private val queryById = Compiled(
(id: Rep[String]) => Users.filter(_.id === id))
def lookup(id: String): Future[Option[User]] = {
val f: Future[Option[UsersRow]] = db.run(queryById(id).result.headOption)
f.map(maybeRow => maybeRow.map(usersRowToUser))
}
def all: Future[Seq[User]] = {
val f = db.run(Users.result)
f.map(seq => seq.map(usersRowToUser))
}
def update(user: User): Future[Int] = {
db.run(queryById(user.id).update(userToUsersRow(user)))
}
def delete(id: String): Future[Int] = {
db.run(queryById(id).delete)
}
def create(user: User): Future[Int] = {
db.run(
Users += userToUsersRow(user.copy(createdAt = Instant.now()))
)
}
def close(): Future[Unit] = {
Future.successful(db.close())
}
private def userToUsersRow(user: User): UsersRow = {
UsersRow(user.id, user.email, user.createdAt, user.updatedAt)
}
private def usersRowToUser(usersRow: UsersRow): User = {
User(usersRow.id, usersRow.email, usersRow.createdAt, usersRow.updatedAt)
}
}

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

@ -0,0 +1,103 @@
package com.example.user.slick.dbios
import slick.jdbc.JdbcProfile
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
@Singleton
class SlickLibraryDbio extends Tables {
override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
import profile.api._
def findBookById(id: Long): DBIO[Option[BooksRow]] =
Query.bookById(id).result.headOption
def findBookById2(id: Long): DBIO[Option[BookWithoutId]] =
Query.test(id).result.headOption
def findBooksWithAuthor: DBIO[Seq[(BooksRow, AuthorsRow)]] =
Query.booksWithAuthor.result
def insertBook(book: Book): DBIO[BooksRow] =
Query.writeBooks += bookToRow(book)
def insertBook2(book: NewBook): DBIO[Long] =
Query.writeBooks3 += book
//
def insertAuthor(author: Author): DBIO[AuthorsRow] =
Query.writeAuthors += authorToRow(author)
def insertAuthor2(author: NewAuthor): DBIO[Long] =
Query.writeAuthors2 += author
def authorToRow(author: Author) = author.transformInto[AuthorsRow]
def bookToRow(book: Book) = book.transformInto[BooksRow]
def authorsRowToAuthor(author: AuthorsRow) = author.transformInto[Author]
def booksRowToBooks(book: BooksRow) = book.transformInto[Book]
def booksRowToBooks2(book: BooksRow) = book.transformInto[BookWithoutId]
// As mentioned under #2, we do encapsulate our queries
object Query {
// Return the book / author with it's auto incremented
// id instead of an insert count
lazy val writeBooks = Books returning Books
.map(_.id) into ((book, id) => book.copy(id))
lazy val writeBooks2 =
Books.map(b => (b.title, b.authorId).mapTo[BookWithoutId])
lazy val writeBooks3 = Books
.map(b => (b.title, b.authorId).mapTo[NewBook])
.returning(Books.map(_.id))
lazy val writeAuthors = Authors returning Authors
.map(_.id) into ((author, id) => author.copy(id))
lazy val writeAuthors2 =
Authors.map(a => (a.name).mapTo[NewAuthor]) returning Authors.map(_.id)
lazy val test = (givenId: Long) =>
Books
.filter(_.id === givenId)
.map(toBooksWithoutID)
lazy val bookById = Books.findBy(_.id)
lazy val toBooksWithoutID = (table: Books) =>
(table.title, table.authorId).mapTo[BookWithoutId]
lazy val booksWithAuthor = for {
b <- Books
a <- Authors if b.authorId === a.id
} yield (b, a)
lazy val authorOfBook = (bookId: Long) =>
for {
(authors, books) <- Authors join Books on (_.id === _.authorId) filter {
case (authors, books) => books.id === bookId
}
} yield (authors, books)
lazy val authorOfBook2 = (bookId: Long) =>
for {
authorId <- Books.filter(_.id === bookId).take(1).map(_.authorId)
authors <- Authors filter (_.id === authorId)
} yield (authors.name)
// lazy val authorOfBook3 = (bookId: Long) =>
// for {
// authorId <- bookById(bookId).map(_.map(_.authorId))
// (authors) <- Authors filter (_.id === authorId)
// } yield (authors)
}
case class BookWithoutId(title: String, authorId: Long)
// def test() = {
// val maybeBook = findBookById(1)
// val x = maybeBook.map(_.map(_.title))
// db.run(x)
// }
}

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

@ -0,0 +1,55 @@
package com.example.user.slick.services
import javax.inject._
import scala.concurrent.ExecutionContext
// import slick.jdbc.JdbcProfile
import slick.jdbc.JdbcBackend.Database
import scala.concurrent.Future
import com.example.models._
import com.example.user.slick.dbios.SlickLibraryDbio
import com.example.services.LibraryService
// import slick.jdbc.H2Profile.api._
@Singleton
class SlickLibraryService @Inject() (
db: Database,
libraryDbio: SlickLibraryDbio
)(implicit ec: ExecutionContext)
extends LibraryService {
import libraryDbio.profile.api._
// Simple function that returns a book
def findBookById(id: Long): Future[Option[Book]] =
db.run(libraryDbio.findBookById(id).map(_.map(libraryDbio.booksRowToBooks)))
// Simple function that returns a list of books with it's author
def findBooksWithAuthor: Future[Seq[(Book, Author)]] =
db.run(libraryDbio.findBooksWithAuthor.map { lst =>
lst.map(tup => {
val (x, y) = tup
(libraryDbio.booksRowToBooks(x), libraryDbio.authorsRowToAuthor(y))
})
})
// Insert a book and an author composing two DBIOs in a transaction
def insertBookAndAuthor(
book: NewBook,
author: NewAuthor
): Future[(Long, Long)] = {
val action = for {
authorId <- libraryDbio.insertAuthor2(author)
bookId <- libraryDbio.insertBook2(book.copy(authorId = authorId))
} yield (
// libraryDbio.booksRowToBooks(book),
// libraryDbio.authorsRowToAuthor(author)
bookId,
authorId
)
db.run(action.transactionally)
}
def findBookById2(id: Long) =
db.run(libraryDbio.findBookById(id).map(_.map(_.title)))
}
case class Book2(title: String)

1
project/build.properties

@ -0,0 +1 @@
sbt.version=1.3.4

4
project/metals.sbt

@ -0,0 +1,4 @@
// DO NOT EDIT! This file is auto-generated.
// This file enables sbt-bloop to create bloop config files.
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-229-b7c15aa9")

14
project/plugins.sbt

@ -0,0 +1,14 @@
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
libraryDependencies += "com.h2database" % "h2" % "1.4.196"
// Database migration
// https://github.com/flyway/flyway-sbt
addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "6.2.2")
// Slick code generation
// https://github.com/tototoshi/sbt-slick-codegen
addSbtPlugin("com.github.tototoshi" % "sbt-slick-codegen" % "1.4.0")
// The Play plugin
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.1")

BIN
public/images/favicon.png

After

Width: 16  |  Height: 16  |  Size: 687 B

3
public/javascripts/hello.js

@ -0,0 +1,3 @@
if (window.console) {
console.log("Welcome to your Play application's JavaScript!");
}

0
public/stylesheets/main.css

6
scripts/test-sbt

@ -0,0 +1,6 @@
#!/usr/bin/env bash
echo "+----------------------------+"
echo "| Executing tests using sbt |"
echo "+----------------------------+"
sbt ++$TRAVIS_SCALA_VERSION clean flyway/flywayMigrate slickCodegen test

2750
test.trace.db
File diff suppressed because it is too large
View File

21
test/controller/FunctionalSpec.scala

@ -0,0 +1,21 @@
package controller
import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec}
import play.api.test.FakeRequest
import play.api.test.Helpers._
/**
* Runs a functional test with the application, using an in memory
* database. Migrations are handled automatically by play-flyway
*/
class FunctionalSpec extends PlaySpec with BaseOneAppPerSuite with MyApplicationFactory {
"HomeController" should {
"work with in memory h2 database" in {
val future = route(app, FakeRequest(GET, "/")).get
contentAsString(future) must include("myuser@example.com")
}
}
}

45
test/controller/MyApplicationFactory.scala

@ -0,0 +1,45 @@
package controller
import java.util.Properties
import com.google.inject.Inject
import org.flywaydb.core.Flyway
import org.flywaydb.core.internal.jdbc.DriverDataSource
import org.scalatestplus.play.FakeApplicationFactory
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.inject.{Binding, Module}
import play.api.{Application, Configuration, Environment}
/**
* Set up an application factory that runs flyways migrations on in memory database.
*/
trait MyApplicationFactory extends FakeApplicationFactory {
def fakeApplication(): Application = {
new GuiceApplicationBuilder()
.configure(Map("myapp.database.url" -> "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"))
.bindings(new FlywayModule)
.build()
}
}
class FlywayModule extends Module {
override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = {
Seq(bind[FlywayMigrator].toSelf.eagerly() )
}
}
class FlywayMigrator @Inject()(env: Environment, configuration: Configuration) {
def onStart(): Unit = {
val driver = configuration.get[String]("myapp.database.driver")
val url = configuration.get[String]("myapp.database.url")
val user = configuration.get[String]("myapp.database.user")
val password = configuration.get[String]("myapp.database.password")
Flyway.configure()
.dataSource(new DriverDataSource(env.classLoader, driver, url, user, password, new Properties()))
.locations("filesystem:modules/flyway/src/main/resources/db/migration")
.load()
.migrate()
}
onStart()
}
Loading…
Cancel
Save