first commit
This commit is contained in:
commit
2956829f9d
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
test.mv.db
|
||||
.idea
|
||||
.vscode
|
||||
.bloop
|
||||
.metals
|
||||
target/
|
1
.scalafmt.conf
Normal file
1
.scalafmt.conf
Normal file
@ -0,0 +1 @@
|
||||
version = "2.4.2"
|
43
README.md
Normal file
43
README.md
Normal file
@ -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
Normal file
41
app/Module.scala
Normal file
@ -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
Normal file
39
app/controllers/HomeController.scala
Normal file
@ -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
Normal file
17
app/services/MyService.scala
Normal file
@ -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
Normal file
20
app/views/cars.scala.html
Normal file
@ -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
Normal file
22
app/views/index.scala.html
Normal file
@ -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
Normal file
15
app/views/main.scala.html
Normal file
@ -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
Normal file
116
build.sbt
Normal file
@ -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
Normal file
8
conf/application.conf
Normal file
@ -0,0 +1,8 @@
|
||||
http.port=8080
|
||||
myapp = {
|
||||
database = {
|
||||
|
||||
numThreads=20
|
||||
maxConnections=20
|
||||
}
|
||||
}
|
39
conf/logback.xml
Normal file
39
conf/logback.xml
Normal file
@ -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
Normal file
11
conf/routes
Normal file
@ -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
Normal file
31
logs/application.log
Normal file
@ -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
Normal file
14
modules/api/src/main/scala/com/example/models/Library.scala
Normal file
@ -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)]
|
||||
}
|
28
modules/api/src/main/scala/com/example/user/CarDAO.scala
Normal file
28
modules/api/src/main/scala/com/example/user/CarDAO.scala
Normal file
@ -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
Normal file
30
modules/api/src/main/scala/com/example/user/UserDAO.scala
Normal file
@ -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);
|
30
modules/slick/src/main/resources/application.conf
Normal file
30
modules/slick/src/main/resources/application.conf
Normal file
@ -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)
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
||||
sbt.version=1.3.4
|
4
project/metals.sbt
Normal file
4
project/metals.sbt
Normal file
@ -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
Normal file
14
project/plugins.sbt
Normal file
@ -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
Normal file
BIN
public/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 687 B |
3
public/javascripts/hello.js
Normal file
3
public/javascripts/hello.js
Normal file
@ -0,0 +1,3 @@
|
||||
if (window.console) {
|
||||
console.log("Welcome to your Play application's JavaScript!");
|
||||
}
|
0
public/stylesheets/main.css
Normal file
0
public/stylesheets/main.css
Normal file
6
scripts/test-sbt
Executable file
6
scripts/test-sbt
Executable file
@ -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
Normal file
2750
test.trace.db
Normal file
File diff suppressed because it is too large
Load Diff
21
test/controller/FunctionalSpec.scala
Normal file
21
test/controller/FunctionalSpec.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
45
test/controller/MyApplicationFactory.scala
Normal file
45
test/controller/MyApplicationFactory.scala
Normal file
@ -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…
Reference in New Issue
Block a user