Scala反射更新一个case class val

5

我在这里使用scala和slick,有一个baserepository负责执行我的类的基本crud操作。 出于设计考虑,我们确实有updatedTime和createdTime列由应用程序处理,而不是由数据库触发器处理。这两个字段都是joda DataTime实例。 这些字段在表中分别由名为HasUpdatedAt和HasCreatedAt的两个traits定义。

trait HasCreatedAt {
    val createdAt: Option[DateTime]
}

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

我想知道如何使用反射调用用户复制方法,在数据库插入方法期间更新createdAt值。

@vptron和@kevin-wright评论后的编辑

我有一个像这样的代码库

trait BaseRepo[ID, R] {

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

我希望只实现一次插入,同时更新createdAt,这就是我不使用复制方法的原因,否则我需要在每个使用createdAt列的地方都实现它。

1
为什么需要反射?copy方法不是私有的。 - vptheron
5
不要这样做。你不能保证scalac不会基于不可变对象确实是不可变的做出优化。使用copy,那就是它的用途! - Kevin Wright
感谢@vptheron 的评论,我刚刚更新了问题,添加了更多的细节来解释为什么我不想在这里使用复制方法。 - dirceusemighini
2个回答

6

这个问题已经在这里解答了,以帮助其他遇到类似问题的人。 最终我使用了以下代码,通过Scala反射来执行我的案例类的复制方法。

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
}

有了这个功能,我可以执行我的案例类的复制方法,并替换确定的字段值。


1

正如评论所说,不要使用反射来更改val的值。如果您使用Java final变量也这样做,它会使您的代码产生非常意外的结果。如果您需要更改val的值,请不要使用val,而应该使用var。

trait HasCreatedAt {
    var createdAt: Option[DateTime] = None
}

case class User(name:String) extends HasCreatedAt

尽管在 case class 中使用 var 可能会带来一些意外的行为,例如复制时不会按预期工作。这可能导致不倾向于使用 case class。

另一种方法是使插入方法返回一个更新后的 case class 副本,例如:

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*/))
    }
}

编辑:

由于我没有回答你的原始问题,而且你可能知道自己在做什么,我添加了一种方法来实现这个目标。

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*/)

但是,请不要这样做。阅读此StackOverflow答案,以了解它可能引起的问题(vals映射到最终字段)。


2
嗨,马丁,感谢您的回答。这个问题的意图是想知道是否有一种使用反射调用R的复制方法的方式,而不是重新分配val的值。我没有找到复制定义,也不知道如何仅使用一个参数来调用它,在这种情况下是createdAt。 - dirceusemighini
啊,抱歉我误解了问题。这样做似乎更困难,因为它涉及到调用带有默认参数的方法。如何实现可以参考 https://dev59.com/UmYr5IYBdhLWcg3wAFg0#14034802. - Martin Kolinek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接