/finatra-mysql-seed

finatra + mysql + quill or slick seed project

Primary LanguageScalaOtherNOASSERTION

Finatra Mysql Seed Project

Finatra + TypesafeConfig + Swagger + Slick + Quill with Mysql seed Project

Development Environments

Requirements

Dependencies

  • Finatra - Fast, testable, Scala services built on TwitterServer and Finagle.
  • Guice - a lightweight dependency injection framework for Java 6 and above by Google.
  • Slick - Functional Relational Mapping for Scala.
  • HikariCP - A high-performance JDBC connection pool.
  • Quill - Alternative to Slick, finagle-mysql
  • ScalaTest - A testing tool for Scala and Java developers.
  • TypesafeConfig - Configuration library for JVM languages.
  • Logback - The Generic, Reliable, Fast & Flexible Logging Framework.
  • Swagger - Add Swagger support for Finatra web framework.

Development Resource Links

How to use This Template

Option 1. if you have activator

activator new PROJECTNAME finatra-mysql-seed

Option 2. Just git clone this project and use sbt

git clone git@github.com:ikhoon/finatra-mysql-seed.git

Getting Started

Install MySQL on Mac OSX using Homebrew

brew install mysql
# start mysql-server
mysqld

Start Server with activator

# clone repostitory
git clone git@github.com:ikhoon/finatra-mysql-seed.git

cd finatra-mysql-seed/

# initialize mysql table & data
mysql -u root < sql/1.sql

# Start finatra server
./activator run
...
[info] Loading project definition from finatra-mysql-seed/project
[info] Set current project to finatra-mysql-seed(in build file:finatra-mysql-seed/)
[info] Running com.github.ikhoon.FinatraServerMain
...

Example Codes

Add Sample Controller

// add new file SampleController.scala
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller

class SampleController extends Controller {

  // GET register /ping uri
  get("/ping") { request: Request =>
    // Define Response
    "pong"
  }
}

Register Controller and Router on FinatraServer

object FinatraServerMain extends FinatraServer

class FinatraServer extends HttpServer {

  override def configureHttp(router: HttpRouter) {
    router
      .add[SampleController]  // Register your Controller
  }
}

Mysql Access with Quill

Define Quill Module

import com.google.inject.{ Singleton, Provides }
import com.twitter.inject.TwitterModule
import com.typesafe.config.Config
import io.getquill.FinagleMysqlSourceConfig
import io.getquill.naming.SnakeCase
import io.getquill._
import io.getquill.sources.finagle.mysql.FinagleMysqlSource

object QuillDatabaseModule extends TwitterModule {

  type QuillDatabaseSource = FinagleMysqlSource[SnakeCase]

  @Provides @Singleton
  def provideDataBaseSource(conf: Config): QuillDatabaseSource = source(new FinagleMysqlSourceConfig[SnakeCase]("") {
    override def config = conf.getConfig("quill.db")
  })

}

Inject Quill using Guice

// Define model
case class Users( id: Int, name: String, createdAt: Date)

// Define repository
import javax.inject.{ Inject, Singleton }

import com.github.ikhoon.modules.QuillDatabaseModule.QuillDatabaseSource
import com.twitter.util.Future
import io.getquill._

@Singleton
class QuillUserRepository @Inject() (db: QuillDatabaseSource) {

  def findById(id: Int): Future[Option[Users]] = {
    val q = quote { (id: Int) =>
      query[Users].filter(i => i.id == id).take(1)
    }
    db.run(q)(id).map(_.headOption)
  }

}

Mysql Access with Slick

Define Slick Module

import javax.inject.Singleton

import com.google.inject.Provides
import com.twitter.inject.TwitterModule
import com.typesafe.config.Config

object SlickDatabaseModule extends TwitterModule {
  import slick.driver.MySQLDriver.api._
  type SlickDatabaseSource = slick.driver.MySQLDriver.api.Database

  @Singleton @Provides
  def provideDatabase(config: Config): SlickDatabaseSource = Database.forConfig("slick.db", config)

}

Inject Slick using Guice

// Define model
import org.joda.time.DateTime
case class Users(id: Long, name: String, createdAt: DateTime)

// Define slick table & repository
import javax.inject.Inject

import com.github.ikhoon.modules.SlickDatabaseModule.SlickDatabaseSource
import com.github.tototoshi.slick.MySQLJodaSupport._
import org.joda.time.DateTime

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

class SlickUserRepository @Inject() (db: SlickDatabaseSource) {

  import driver.api._
  private class UserTable(tag: Tag) extends Table[Users](tag, "users") {
    def id = column[Long]("id", O.PrimaryKey)
    def name = column[String]("name")
    def createdAt = column[DateTime]("created_at")
    def * = (id, name, createdAt) <> ((Users.apply _).tupled, Users.unapply)
  }

  private val users = TableQuery[UserTable]

  def findById(id: Long): Future[Option[Users]] = {
    db.run {
      users.filter(_.id === id).take(1).result
    }.map(_.headOption)
  }
}

Http Programming with Finagle Http Client

Define default HTTP Host header for finagle http client

Otherwise finatra http client request builder doesn't work.

abstract class BasicHttpClientModule() extends TwitterModule {

  protected def provideHttpClient(mapper: FinatraObjectMapper, host: String, port: Int = 80): HttpClient = {
    val httpClientModule = new HttpClientModule {
      override def dest: String = s"$host:$port"
      override def defaultHeaders: Map[String, String] = Map("Host" -> host)
    }
    httpClientModule.provideHttpClient(mapper, httpClientModule.provideHttpService)
  }
}

Create HttpClient Module with host and port

object FakeHttpClientModule {
  def apply() = new BasicHttpClientModule {
    @Named("fake") @Provides @Singleton
    def provideHttpClient(mapper: FinatraObjectMapper, config: Config) =
      super.provideHttpClient(mapper, config.as[String]("fake.host"), config.as[Int]("fake.port"))
  }
}

Resister HttpClient on FinatraServer

class FinatraServer extends HttpServer {
  override def modules = Seq(FakeHttpClientModule())
  ...
}

// Use Http Client
// file : FakeService.scala
import javax.inject.{ Inject, Named }

import com.fasterxml.jackson.databind.JsonNode
import com.twitter.finatra.httpclient.{ HttpClient, RequestBuilder }
import com.twitter.util.Future
import com.typesafe.config.Config
import net.ceedubs.ficus.Ficus._

// inject httpClient using @Named("fake") annotation
class FakeService @Inject() (@Named("fake") httpClient: HttpClient, config: Config) {
  def withSleep(sec: Int): Future[JsonNode] = {
    val url = config.as[String]("fake.host") + s"?sleep=$sec"
    httpClient.executeJson[JsonNode](RequestBuilder.get(url))
  }
}

Auto Restart FinatraServer using spray/sbt-resolver

./activator "~re-start"
...
[info] Loading project definition from finatra-mysql-seed/project
[info] Set current project to finatra-mysql-seed(in build file:finatra-mysql-seed/)
...

Create a fat JAR with compact all of its dependencies

# build single jar
./activator assembly
...
[info] Assembly up to date: finatra-mysql-seed/target/scala-2.11/finatra-mysql-seed.jar
[success] Total time: 10 s, completed Dec 3, 2015 5:08:01 PM

Start Standalone Server

cd target/scala-2.11/
# Development mode with default logback.xml
java -jar finatra-mysql-seed.jar -mode dev
# Production mode with specified logback config file
java -jar -Dlogback.configurationFile=conf/real-logback.xml finatra-mysql-seed.jar -mode real

Run modes

dev(default)

Run with resources/conf/dev.conf & resources/logback.xml

Custom run mode needs two files

  • typesafe config : src/main/resources/conf/xxx.conf
  • logback.xml : src/main/resources/conf/xxx-logback.xml
  • Run java -jar -Dlogback.configurationFile=conf/xxx-logback.xml finatra-mysql-seed.jar -mode xxx

Show Twitter Server flags

java -jar finatra-mysql-seed.jar -help
...
flags:
  -help='false': Show this help
  -http.announce='java.lang.String': Address to announce HTTP server to
  -http.name='http': Http server name
  -http.port=':9999': External HTTP server port
  -https.announce='java.lang.String': Address to announce HTTPS server to
  -https.name='https': Https server name
  -https.port='': HTTPs Port
  -key.path='': path to SSL key
  -local.doc.root='': File serving directory for local development
  -log.append='true': If true, appends to existing logfile. Otherwise, file is truncated.
  -log.async='true': Log asynchronously
  -log.async.maxsize='4096': Max queue size for async logging
  -log.level='INFO': Log level
  -log.output='/dev/stderr': Output file
  -maxRequestSize='5242880.bytes': HTTP(s) Max Request Size
# set run mode with `-mode`
  -mode='dev': application run mode [dev:default, alpha, sandbox, beta, real]
  -mustache.templates.dir='templates': templates resource directory
  -shutdown.time='1.minutes': Maximum amount of time to wait for pending requests to complete on shutdown
  -tracingEnabled='true': Tracing enabled

RESTful API Document using Swagger

Remote Debugging

./activator -jvm-debug <port> run

Reference : http://stackoverflow.com/questions/19473941/how-to-debug-play-application-using-activator

Troubleshooting

sbt run or activator run raise dependency errors, clear ivy's cache files and retry run.

rm -rf ~/.ivy2/cache/