dirceusemighini
dirceusemighini

Reputation: 1354

Scala Reflection to update a case class val

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 createdTime columns all handled by the application, and not by triggers in database. Both of this fields are joda DataTime instances. Those fields are defined in two traits called HasUpdatedAt, and HasCreatedAt, for the tables

trait HasCreatedAt {
    val createdAt: Option[DateTime]
}

case class User(name:String,createdAt:Option[DateTime] = None) extends HasCreatedAt

I would like to know how can I use reflection to call the user copy method, to update the createdAt value during the database insertion method.

Edit after @vptron and @kevin-wright comments

I have a repo like this

trait BaseRepo[ID, R] {

    def insert(r: R)(implicit session: Session): ID
  }

I want to implement the insert just once, and there I want to createdAt to be updated, that's why I'm not using the copy method, otherwise I need to implement it everywhere I use the createdAt column.

Upvotes: 5

Views: 3571

Answers (2)

dirceusemighini
dirceusemighini

Reputation: 1354

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.

Upvotes: 6

Martin Kolinek
Martin Kolinek

Reputation: 2010

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).

Upvotes: 1

Related Questions