scalaplayframeworkslickpac4j

How to get play datasource instance when using slick


I am using play-slick with slick evolutions to create my db.

I have the following config

slick.dbs.default.profile="slick.jdbc.H2Profile$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:mem:play;DB_CLOSE_DELAY=-1"

This works fine when I want to use slick itself to operate on the db. However, I want to access the underlying javax.sql.Datasource instance (which I need to pass in to another library, namely pac4js DbProfileService).

I tried to add a config for the db name as dbName="play", and then add

  @Provides
  def dbProfileService(dbApi: DBApi): DbProfileService = {
    new DbProfileService(dbApi.database(configuration.get[String]("dbName")).dataSource)
  }

however, it seems that the injection happens before the evolution script has a chance to run, which causes an exception saying that the play database isn't able to be found. Is there a better approach for this problem?

Here is the stack trace

aused by: java.lang.IllegalArgumentException: Could not find database for play
    at play.api.db.slick.evolutions.internal.DBApiAdapter.$anonfun$database$1(DBApiAdapter.scala:32)
    at scala.collection.immutable.Map$Map1.getOrElse(Map.scala:248)
    at play.api.db.slick.evolutions.internal.DBApiAdapter.database(DBApiAdapter.scala:32)
    at modules.SecurityModule.dbProfileService(SecurityModule.scala:53)
    at modules.SecurityModule$$FastClassByGuice$$153731.GUICE$TRAMPOLINE(<generated>)
    at modules.SecurityModule$$FastClassByGuice$$153731.apply(<generated>)
    at com.google.inject.internal.ProviderMethod$FastClassProviderMethod.doProvision(ProviderMethod.java:260)
    at com.google.inject.internal.ProviderMethod.doProvision(ProviderMethod.java:171)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.provision(InternalProviderInstanceBindingImpl.java:185)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.get(InternalProviderInstanceBindingImpl.java:162)
    at com.google.inject.internal.SingleParameterInjector.inject(SingleParameterInjector.java:40)
    at com.google.inject.internal.SingleParameterInjector.getAll(SingleParameterInjector.java:60)
    at com.google.inject.internal.ProviderMethod.doProvision(ProviderMethod.java:171)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.provision(InternalProviderInstanceBindingImpl.java:185)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.get(InternalProviderInstanceBindingImpl.java:162)
    at com.google.inject.internal.SingleParameterInjector.inject(SingleParameterInjector.java:40)
    at com.google.inject.internal.SingleParameterInjector.getAll(SingleParameterInjector.java:60)
    at com.google.inject.internal.ProviderMethod.doProvision(ProviderMethod.java:171)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.provision(InternalProviderInstanceBindingImpl.java:185)
    at com.google.inject.internal.InternalProviderInstanceBindingImpl$CyclicFactory.get(InternalProviderInstanceBindingImpl.java:162)
    at com.google.inject.internal.SingleFieldInjector.inject(SingleFieldInjector.java:50)
    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:146)
    at com.google.inject.internal.MembersInjectorImpl.injectAndNotify(MembersInjectorImpl.java:101)
    at com.google.inject.internal.Initializer$InjectableReference.get(Initializer.java:245)
    at com.google.inject.internal.Initializer.injectAll(Initializer.java:140)
    at com.google.inject.internal.InternalInjectorCreator.injectDynamically(InternalInjectorCreator.java:180)

I also added a breakpoint, and sure enough, there was only the default db there, not the play db as I would expect.


Solution

  • The problem is with the value of the key dbName. When you are doing

    configuration.get[String]("dbName")
    

    You are looking up for the value of the key named dbName that is in your config files. In the provided example, the value is play. Now, looking at the complete line you have this

    @Provides
    def dbProfileService(dbApi: DBApi): DbProfileService = {
      new DbProfileService(
        dbApi                                   // trait play.api.db.DBApi
          .database(                            // method from `DBApi`
            configuration.get[String]("dbName") // getting the value of the key `dbName` from config file
          )
          .dataSource 
        )
    }
    

    You are calling the database method from DBApi. There are two classes that are mixing this trait. One is DBApiAdapter from play-slick-evolutions and the other one is DefaultDBApi from play-jdbc. I think in your case is using the one that belongs to the evolution module. If you look at the implementaiton of the method database that you are calling, both classes have the same logic (so, it doesn't matter which implementation is being used).

    def database(name: String): Database = {
      databaseByName.getOrElse(name, throw new IllegalArgumentException(s"Could not find database for $name"))
    }
    

    This means that it will look for a key with the prefix slick.dbs.<db name>. Based on the error message you got

    caused by: java.lang.IllegalArgumentException: Could not find database for play
        at play.api.db.slick.evolutions.internal.DBApiAdapter.$anonfun$database$1(DBApiAdapter.scala:32)
    

    you tried to look for a db config with the prefix slick.dbs.play and the stracktrace shows that in this case the implementation from evolutino module is being used.

    To solve the issue you have two options: