“Error injecting constructor” when mapping Enumera

2019-08-13 07:45发布

问题:

I have the following classes. please note the commented-out Role lines.

user model:

case class User(
  uid: Option[Long] = None,
//  role: Option[Role.Role] = None,
  firstName: String,
  lastName: String,
  middleName: Option[String] = None,
  email: String,
  avatarUrl: Option[String],
  created: DateTime = DateTime.now,
  modified: DateTime = DateTime.now
)

UserComponent and UserDAO:

import javax.inject._
import scala.concurrent.Future
import org.krazykat.whatsupdoc.models.User
import play.api.db.slick.{HasDatabaseConfigProvider, DatabaseConfigProvider}
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import slick.driver.{JdbcProfile, JdbcDriver}
import org.joda.time.DateTime
import com.github.tototoshi.slick.GenericJodaSupport
import java.sql.Time

trait UsersComponent { self: HasDatabaseConfigProvider[JdbcProfile]  =>
  import driver.api._

  object PortableJodaSupport extends GenericJodaSupport(driver.asInstanceOf[JdbcDriver])
  import PortableJodaSupport._


//  implicit val roleMapper = MappedColumnType.base[Role.Role, String](
//    e => e.toString,
//    s => Role.withName(s)
//  )


  class Users(tag: Tag) extends Table[User](tag, "USERS") {

    def uid = column[Long]("USER_ID", O.PrimaryKey, O.AutoInc)
//    def role = column[Role.Role]("ROLE") 
    def firstName = column[String]("FIRST_NAME")
    def lastName = column[String]("LAST_NAME")
    def middleName = column[String]("MIDDLE_NAME")
    def email = column[String]("EMAIL")
    def avatarUrl = column[String]("AVATAR_URL")
    def created =  column[DateTime]("CREATED")
    def modified = column[DateTime]("MODIFIED")

    def * = (uid.?, firstName, lastName, middleName.?, email, avatarUrl.?, created, modified) <> (User.tupled, User.unapply _)
  }
}


/**
 * @author ehud
 */
@Singleton
class UsersDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends UsersComponent with HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._

  val users = TableQuery[Users]

  def count = db.run(users.length.result)

  def getById(uid: Long) = 
    db.run(users.filter(_.uid === uid).result.headOption)

  def insert(user: User) =
    db.run((users returning users.map(_.uid)) += user).map(id => id)

  def delete(user: User) =
    db.run(users.filter(_.uid === user.uid).delete)

  def update(uid: Long, user: User) = {
    val userToUpdate: User = user.copy(Some(uid))
    db.run(users.filter(_.uid === uid).update(userToUpdate))
  }
}

and a test spec:

class UsersSpec extends Specification {


  def dateIs(date: java.util.Date, str: String) = new java.text.SimpleDateFormat("yyyy-MM-dd").format(date) == str

  trait WithDatabaseConfig {
    lazy val (driver, db) = {
      val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)
      (dbConfig.driver, dbConfig.db)
    }
  }

  "User model" should {   
    def usersDao(implicit app: Application) = {
      val app2UsersDAO = Application.instanceCache[UsersDAO]
      app2UsersDAO(app)
    }

    "be inserted to db correctly" in new WithApplication with WithDatabaseConfig {
      import driver.api._

      val userInsertresult = Await.result(usersDao.insert(
        User(None, "firstname", "lastname", Some("middlename"), "me@me.com", Some("avatar"), DateTime.now, DateTime.now)), 
        Duration.Inf
      )
      val count = Await.result(usersDao.count, Duration.Inf)
      count mustEqual 1
    }

when i run this spec, it succeeds. great.

now i want to add a role to my user. so i created the following Role class:

object Role extends Enumeration{

  type Role = Value

  val None = Value("NONE")
  val Admin = Value("ADMIN")
  val Root = Value("ROOT")
} 

and uncomment the relevant lines (and add it to any instanciation call), and update the evolutions file (the Role part is stored as a VARCHAR). now, when the spec runs, i get the following exception:

[error]   ! be inserted to db correctly
[error]    Unable to provision, see the following errors:
[error]    
[error]    1) Error injecting constructor, java.lang.NullPointerException
[error]      at UsersDAO.<init>(UsersDAO.scala:59)
[error]      at UsersDAO.class(UsersDAO.scala:59)
[error]      while locating UsersDAO
[error]    
[error]    1 error (InjectorImpl.java:1025)
[error] com.google.inject.internal.InjectorImpl$2.get(InjectorImpl.java:1025)
[error] com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1051)
[error] play.api.inject.guice.GuiceInjector.instanceOf(GuiceInjectorBuilder.scala:321)
[error] play.api.inject.guice.GuiceInjector.instanceOf(GuiceInjectorBuilder.scala:316)
[error] play.api.Application$$anonfun$instanceCache$1.apply(Application.scala:234)
[error] play.api.Application$$anonfun$instanceCache$1.apply(Application.scala:234)
[error] play.utils.InlineCache.fresh(InlineCache.scala:69)
[error] play.utils.InlineCache.apply(InlineCache.scala:55)

I tried researching online, but all the Slick samples I found on how to do Enumerations seem like mine, and the injection error does not show up in the Slick context.

any idea?

回答1:

The problem here is that you are trying to use driver.api.MappedColumnType from the HasDatabaseConfigProvider trait in the UsersComponent trait. But at the moment of the UsersComponent initialization driver is not injected yet, that causes NPE. You can either make your roleMapper val lazy or change it from val to def:

implicit lazy val roleMapper = ...

OR

implicit def roleMapper = ...


回答2:

Any particular reason you go for Enumeration? I can't help you if so, but I can suggest using sealed trait + case objects + sealerate.

Create a companion for case class User and describe Role

object User {
  sealed trait Role
  case object None extends Role
  case object Root extends Role
  case object Admin extends Role

  // A bit messy stuff to convert Role to String and back
  def s = sealerate.values[Role].foldLeft(Map[String, Role]()) { case (m, f) ⇒ m.updated(f.toString, f) }
  implicit val roleColumnType: JdbcType[Role] with BaseTypedType[Role] = MappedColumnType.base[Role, String](_.toString, s)
}

Add role to User case class

case class User(
  uid: Option[Long] = None,
  role: Option[User.Role] = User.None,
  firstName: String,
  lastName: String,
  middleName: Option[String] = None,
  email: String,
  avatarUrl: Option[String],
  created: DateTime = DateTime.now,
  modified: DateTime = DateTime.now
)

Add role to Users table

class Users(tag: Tag) extends Table[User](tag, "USERS") {
  ...
  def role = column[User.Role]("ROLE") 
  ...
}

That's all :)

Brief explanation on Scala enums here.