nova
4 years ago
commit
2956829f9d
39 changed files with 3725 additions and 0 deletions
-
6.gitignore
-
1.scalafmt.conf
-
43README.md
-
41app/Module.scala
-
39app/controllers/HomeController.scala
-
17app/services/MyService.scala
-
20app/views/cars.scala.html
-
22app/views/index.scala.html
-
15app/views/main.scala.html
-
116build.sbt
-
8conf/application.conf
-
39conf/logback.xml
-
11conf/routes
-
31logs/application.log
-
14modules/api/src/main/scala/com/example/models/Library.scala
-
26modules/api/src/main/scala/com/example/services/LibraryService.scala
-
28modules/api/src/main/scala/com/example/user/CarDAO.scala
-
30modules/api/src/main/scala/com/example/user/UserDAO.scala
-
6modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql
-
6modules/flyway/src/main/resources/db/migration/V2__add_user.sql
-
6modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql
-
6modules/flyway/src/main/resources/db/migration/V4__add_car.sql
-
12modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql
-
14modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql
-
30modules/slick/src/main/resources/application.conf
-
69modules/slick/src/main/scala/com/example/user/slick/SlickCarDAO.scala
-
67modules/slick/src/main/scala/com/example/user/slick/SlickUserDAO.scala
-
103modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala
-
55modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala
-
1project/build.properties
-
4project/metals.sbt
-
14project/plugins.sbt
-
BINpublic/images/favicon.png
-
3public/javascripts/hello.js
-
0public/stylesheets/main.css
-
6scripts/test-sbt
-
2750test.trace.db
-
21test/controller/FunctionalSpec.scala
-
45test/controller/MyApplicationFactory.scala
@ -0,0 +1,6 @@ |
|||
test.mv.db |
|||
.idea |
|||
.vscode |
|||
.bloop |
|||
.metals |
|||
target/ |
@ -0,0 +1 @@ |
|||
version = "2.4.2" |
@ -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. |
@ -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()) } |
|||
} |
@ -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))) |
|||
} |
|||
|
|||
} |
@ -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 |
|||
} |
@ -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> |
|||
} |
@ -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> |
|||
} |
@ -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> |
@ -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 |
@ -0,0 +1,8 @@ |
|||
http.port=8080 |
|||
myapp = { |
|||
database = { |
|||
|
|||
numThreads=20 |
|||
maxConnections=20 |
|||
} |
|||
} |
@ -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> |
@ -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) |
@ -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 |
@ -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] |
|||
} |
@ -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)] |
|||
} |
@ -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]) |
@ -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]) |
@ -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 |
|||
); |
@ -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') |
|||
); |
@ -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 |
|||
); |
@ -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') |
|||
); |
@ -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 |
|||
); |
@ -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); |
@ -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 |
|||
} |
|||
} |
|||
|
|||
|
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
// } |
|||
} |
@ -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) |
@ -0,0 +1 @@ |
|||
sbt.version=1.3.4 |
@ -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") |
@ -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") |
After Width: 16 | Height: 16 | Size: 687 B |
@ -0,0 +1,3 @@ |
|||
if (window.console) { |
|||
console.log("Welcome to your Play application's JavaScript!"); |
|||
} |
@ -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
File diff suppressed because it is too large
View File
@ -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") |
|||
} |
|||
} |
|||
|
|||
} |
@ -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() |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue