Scala Reflection to update a case class val

后端 未结 2 1682
不知归路
不知归路 2021-01-13 16:45

I\'m using scala and slick here, and I have a baserepository which is responsible for doing the basic crud of my classes. For a design decision, we do have updatedTime and c

相关标签:
2条回答
  • 2021-01-13 17:00

    This question was answered here to help other with this kind of problem. I end up using this code to execute the copy method of my case classes using scala reflection.

    import reflect._
    import scala.reflect.runtime.universe._
    import scala.reflect.runtime._
    
    class Empty
    
    val mirror = universe.runtimeMirror(getClass.getClassLoader)
    // paramName is the parameter that I want to replacte the value
    // paramValue is the new parameter value
    def updateParam[R : ClassTag](r: R, paramName: String, paramValue: Any): R = {
    
      val instanceMirror = mirror.reflect(r)
      val decl = instanceMirror.symbol.asType.toType
      val members = decl.members.map(method => transformMethod(method, paramName, paramValue, instanceMirror)).filter {
        case _: Empty => false
        case _ => true
      }.toArray.reverse
    
      val copyMethod = decl.declaration(newTermName("copy")).asMethod
      val copyMethodInstance = instanceMirror.reflectMethod(copyMethod)
    
      copyMethodInstance(members: _*).asInstanceOf[R]
    }
    
    def transformMethod(method: Symbol, paramName: String, paramValue: Any, instanceMirror: InstanceMirror) = {
      val term = method.asTerm
      if (term.isAccessor) {
        if (term.name.toString == paramName) {
          paramValue
        } else instanceMirror.reflectField(term).get
      } else new Empty
    }
    

    With this I can execute the copy method of my case classes, replacing a determined field value.

    0 讨论(0)
  • 2021-01-13 17:09

    As comments have said, don't change a val using reflection. Would you that with a java final variable? It makes your code do really unexpected things. If you need to change the value of a val, don't use a val, use a var.

    trait HasCreatedAt {
        var createdAt: Option[DateTime] = None
    }
    
    case class User(name:String) extends HasCreatedAt
    

    Although having a var in a case class may bring some unexpected behavior e.g. copy would not work as expected. This may lead to preferring not using a case class for this.

    Another approach would be to make the insert method return an updated copy of the case class, e.g.:

    trait HasCreatedAt {
        val createdAt: Option[DateTime]
        def withCreatedAt(dt:DateTime):this.type
    }
    
    case class User(name:String,createdAt:Option[DateTime] = None) extends HasCreatedAt {
        def withCreatedAt(dt:DateTime) = this.copy(createdAt = Some(dt))
    }
    
    trait BaseRepo[ID, R <: HasCreatedAt] {
        def insert(r: R)(implicit session: Session): (ID, R) = {
            val id = ???//insert into db
            (id, r.withCreatedAt(??? /*now*/))
        }
    }
    

    EDIT:

    Since I didn't answer your original question and you may know what you are doing I am adding a way to do this.

    import scala.reflect.runtime.universe._
    
    val user = User("aaa", None)
    val m = runtimeMirror(getClass.getClassLoader)
    val im = m.reflect(user)
    val decl = im.symbol.asType.toType.declaration("createdAt":TermName).asTerm
    val fm = im.reflectField(decl)
    fm.set(??? /*now*/)
    

    But again, please don't do this. Read this stackoveflow answer to get some insight into what it can cause (vals map to final fields).

    0 讨论(0)
提交回复
热议问题