How to bind Slick dependency with Lagom?

scala

(Yash Bhardwaj) #1

So, I have this dependency which is used to create tables and interact with Postgres. Here is a Sample Class:

class ConfigTable {

  this: DBFactory =>

  import driver.api._

  implicit val configKeyMapper = MappedColumnType.base[ConfigKey, String](e => e.toString, s => ConfigKey.withName(s))

  val configs = TableQuery[ConfigMapping]

  class ConfigMapping(tag: Tag) extends Table[Config](tag, "configs") {

    def key = column[ConfigKey]("key")
    def value = column[String]("value")
    def * = (key, value) <> (Config.tupled, Config.unapply _)
  }

  /**
    * add config
    *
    * @param config
    * @return
    */
  def add(config: Config): Try[Config] = try {
    sync(db.run(configs += config)) match {
      case 1 => Success(config)
      case _ => Failure(new Exception("Unable to add config"))
    }
  } catch {
    case ex: PSQLException =>
      if (ex.getMessage.contains("duplicate key value")) Failure(new Exception("alt id already exists."))
      else Failure(new Exception(ex.getMessage))
  }
 
  def get(key: ConfigKey): Option[Config] = sync(db.run(configs.filter(x => x.key === key).result)).headOption

  def getAll(): Seq[Config] = sync(db.run(configs.result))

}

object ConfigTable extends ConfigTable with PSQLComponent

PSQLComponent is the Abstraction for Database meta configuration:

import slick.jdbc.PostgresProfile

trait PSQLComponent extends DBFactory {

  val driver = PostgresProfile

  import driver.api.Database

  val db: Database = Database.forConfig("db.default")
}

DBFactory is again an abstraction:

import slick.jdbc.JdbcProfile

trait DBFactory {

  val driver: JdbcProfile

  import driver.api._

  val db: Database

}

application.conf:

db.default {
  driver = "org.postgresql.Driver"
  url = "jdbc:postgresql://localhost:5432/db"
  user = "user"
  password = "pass"
  hikaricp {
    minimumIdle = ${db.default.async-executor.minConnections}
    maximumPoolSize = ${db.default.async-executor.maxConnections}
  }
}

jdbc-defaults.slick.profile = "slick.jdbc.PostgresProfile$"
lagom.persistence.jdbc.create-tables.auto=false

All of the Above are part of the dependency. I compile and publish this dependency to nexus and trying to use this in my Lagom Microservice.

Here is the Loader Class:

class SlickExapleAppLoader extends LagomApplicationLoader {

  override def load(context: LagomApplicationContext): LagomApplication = new SlickExampleApp(context) {
    override def serviceLocator: ServiceLocator = NoServiceLocator
  }

  override def loadDevMode(context: LagomApplicationContext): LagomApplication = new SlickExampleApp(context) with LagomDevModeComponents {

  }

  override def describeService = Some(readDescriptor[SlickExampleLMSServiceImpl])
}

abstract class SlickExampleApp(context: LagomApplicationContext)
  extends LagomApplication(context)
    // No Idea which to use and how, nothing clear from doc too.
    //    with ReadSideJdbcPersistenceComponents
    //    with ReadSideSlickPersistenceComponents
    //    with SlickPersistenceComponents
    with AhcWSComponents {


  wire[SlickExampleScheduler]

}

I’m trying to implement it in this scheduler:

class SlickExampleScheduler @Inject()(lmsService: LMSService,
                                      configuration: Configuration)(implicit ec: ExecutionContext) {
  val brofile = `SomeDomainObject`
  val gson = new Gson()
  val concurrency = Runtime.getRuntime.availableProcessors() * 10

  implicit val timeout: Timeout = 3.minute
  implicit val system: ActorSystem = ActorSystem("LMSActorSystem")
  implicit val materializer: ActorMaterializer = ActorMaterializer()

  // Getting Exception Initializer here..... For ConfigTable ===> ExceptionLine
  val schedulerImplDao = new SchedulerImplDao(ConfigTable)  

  def hitLMSAPI = {

    println("=============>1")

    schedulerImplDao.doSomething()
  }

  system.scheduler.schedule(2.seconds, 2.seconds) {
    println("=============>")
    hitLMSAPI
  }

}

Not sure if it’s the correct way, or if it’s not what is the correct way of doing this. It is the project requirement to keep the Data Models separate from the service for the obvious reasons of re-usability.

Exception Stack:

17:50:38.666 [info] akka.cluster.Cluster(akka://lms-impl-application) [sourceThread=ForkJoinPool-1-worker-1, akkaTimestamp=12:20:38.665UTC, akkaSource=akka.cluster.Cluster(akka://lms-impl-application), sourceActorSystem=lms-impl-application] - Cluster Node [akka.tcp://lms-impl-application@127.0.0.1:45805] - Started up successfully
17:50:38.707 [info] akka.cluster.Cluster(akka://lms-impl-application) [sourceThread=lms-impl-application-akka.actor.default-dispatcher-6, akkaTimestamp=12:20:38.707UTC, akkaSource=akka.cluster.Cluster(akka://lms-impl-application), sourceActorSystem=lms-impl-application] - Cluster Node [akka.tcp://lms-impl-application@127.0.0.1:45805] - No seed-nodes configured, manual cluster join required
java.lang.ExceptionInInitializerError
	at com.slick.init.impl.SlickExampleScheduler.<init>(SlickExampleScheduler.scala:29)
	at com.slick.init.impl.SlickExampleApp.<init>(SlickExapleAppLoader.scala:42)
	at com.slick.init.impl.SlickExapleAppLoader$$anon$2.<init>(SlickExapleAppLoader.scala:17)
	at com.slick.init.impl.SlickExapleAppLoader.loadDevMode(SlickExapleAppLoader.scala:17)
	at com.lightbend.lagom.scaladsl.server.LagomApplicationLoader.load(LagomApplicationLoader.scala:76)
	at play.core.server.LagomReloadableDevServerStart$$anon$1.$anonfun$get$5(LagomReloadableDevServerStart.scala:176)
	at play.utils.Threads$.withContextClassLoader(Threads.scala:21)
	at play.core.server.LagomReloadableDevServerStart$$anon$1.$anonfun$get$3(LagomReloadableDevServerStart.scala:173)
	at scala.Option.map(Option.scala:163)
	at play.core.server.LagomReloadableDevServerStart$$anon$1.$anonfun$get$2(LagomReloadableDevServerStart.scala:149)
	at scala.util.Success.flatMap(Try.scala:251)
	at play.core.server.LagomReloadableDevServerStart$$anon$1.$anonfun$get$1(LagomReloadableDevServerStart.scala:147)
	at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:658)
	at scala.util.Success.$anonfun$map$1(Try.scala:255)
	at scala.util.Success.map(Try.scala:213)
	at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
	at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:33)
	at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:33)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
	at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.NullPointerException
	at com.example.db.models.LoginTable.<init>(LoginTable.scala:29)
	at com.example.db.models.LoginTable$.<init>(LoginTable.scala:293)
	at com.example.db.models.LoginTable$.<clinit>(LoginTable.scala)
	... 24 more

(Renato) #2

@yash-bhardwaj, it’s hard to see where is the error, but I can give you some tips on best-practices that will eventually remove the error.

First, don’t create the DB yourself. Just use the one being configured by Lagom.

You get one configured for you when you mixin SlickPersistenceComponents.

Don’t create your own ActorSystem and Materializer. Use those provided by Lagom.

At the new, you should have some similar to:

abstract class SlickExampleApp(context: LagomApplicationContext)
  extends LagomApplication(context) with SlickPersistenceComponents
    with AhcWSComponents {

  // don't use an object and let the Slick DB be inject in constructor
  lazy val configTable = wire[ConfigTable] 
  
  // inject ConfigTable, ActorSystem and Materializer here
  wire[SlickExampleScheduler]
}

It also seems that you are using Lagom, but are not using its persistence API. That’s ok, but seems odd.


(Yash Bhardwaj) #3

@renato it gives compilation error:

Object creation impossible, since 
member connectionPool: ConnectionPool in play.api.db.DBComponents is not defined;
member jsonSerializerRegistry: JsonSerializerRegistry in com.lightbend.lagom.scaladsl.playjson.RequiresJsonSerializerRegistry is not defined

in

class SlickExapleAppLoader extends LagomApplicationLoader {

  override def load(context: LagomApplicationContext): LagomApplication = new SlickExampleApp(context) { // compilaion Error
    override def serviceLocator: ServiceLocator = NoServiceLocator
  }

  override def loadDevMode(context: LagomApplicationContext): LagomApplication = new SlickExampleApp(context) with LagomDevModeComponents { // compilaion Error

  }

  override def describeService = Some(readDescriptor[SlickExampleLMSServiceImpl])
}

when I use SlickPersistenceComponents as above. May be to avoid this I just created db myself. As I don’t want to implement any connection pool and Serializer separately.


(Yash Bhardwaj) #4

@renato thank for the correct direction, this is how it is woking:

abstract class SlickExampleApp(context: LagomApplicationContext) extends LagomApplication(context)
  with SlickPersistenceComponents with AhcWSComponents {

  override implicit lazy val actorSystem: ActorSystem = ActorSystem("LMSActorSystem")

  override lazy val materializer: ActorMaterializer = ActorMaterializer()
  override lazy val lagomServer = serverFor[SlickExampleLMSService](wire[SlickExampleLMSServiceImpl])
  lazy val externalService = serviceClient.implement[LMSService]

  override def connectionPool: ConnectionPool = new HikariCPConnectionPool(environment)

  override def jsonSerializerRegistry: JsonSerializerRegistry = new JsonSerializerRegistry {
    override def serializers: immutable.Seq[JsonSerializer[_]] = Vector.empty
  }

  val loginTable = wire[LoginTable]

  wire[SlickExampleScheduler]

}

> One thing I’d like to report is: Lagom docs about the application.conf configuration of slick is not correct, it misleaded me for two days, the I digged into the Liberary code and this is how it goes:

 private val readSideConfig = system.settings.config.getConfig("lagom.persistence.read-side.jdbc")
  private val jdbcConfig = system.settings.config.getConfig("lagom.persistence.jdbc")
  private val createTables = jdbcConfig.getConfig("create-tables")
  val autoCreateTables: Boolean = createTables.getBoolean("auto")

  // users can disable the usage of jndiDbName for userland read-side operations by
  // setting the jndiDbName to null. In which case we fallback to slick.db.
  // slick.db must be defined otherwise the application will fail to start
  val db = {
    if (readSideConfig.hasPath("slick.jndiDbName")) {
      new InitialContext()
        .lookup(readSideConfig.getString("slick.jndiDbName"))
        .asInstanceOf[Database]
    } else if (readSideConfig.hasPath("slick.db")) {
      Database.forConfig("slick.db", readSideConfig)
    } else {
      throw new RuntimeException("Cannot start because read-side database configuration is missing. " +
        "You must define either 'lagom.persistence.read-side.jdbc.slick.jndiDbName' or 'lagom.persistence.read-side.jdbc.slick.db' in your application.conf.")
    }
  }

  val profile = DatabaseConfig.forConfig[JdbcProfile]("slick", readSideConfig).profile

The configuration it requires is very much different than the suggested one on the Doc.


(Renato) #5

Hi @yash-bhardwaj

Sorry. I didn’t see your message. The config is correct, as explained here: https://github.com/lagom/lagom/pull/1681#issuecomment-448948612

I’m happy to hear that you solved the wiring issue you were having.

Cheers,

Renato