5

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.

dirceusemighini
  • 1,344
  • 2
  • 16
  • 35
  • 1
    Why do you need reflection? The `copy` method is not private. – vptheron Jan 29 '14 at 13:18
  • 5
    DON'T DO THIS. You have no guarantee that scalac won't make optimisations on the assumption that immutable objects are, in fact, immutable. Use `copy`, that's what it's for! – Kevin Wright Jan 29 '14 at 13:28
  • Thanks for the comments @vptheron , I just updated the question to add more details explaining why I don't want to use copy method in here. – dirceusemighini Jan 29 '14 at 14:19

2 Answers2

6

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.

dirceusemighini
  • 1,344
  • 2
  • 16
  • 35
1

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

Community
  • 1
  • 1
Martin Kolinek
  • 2,010
  • 14
  • 16
  • 2
    Hi Martin, thanks for the answer, the intention of this question is to know if there is a way to call the copy method of R using reflection, and not reassign the value of a val. I didn't found the copy definition, and how to call it using just an argument, in this case the createdAt – dirceusemighini Jan 30 '14 at 02:32
  • Ah, sorry I misunderstood the question. Doing that seems to be more difficult, because it involves calling a method with default parameters. How this is done can be seen at http://stackoverflow.com/questions/14034142/how-do-i-access-default-parameter-values-via-scala-reflection/14034802#14034802. – Martin Kolinek Jan 30 '14 at 14:08