在 Scala Slick 中实现类实例成员修改的最佳方法?

Posted

技术标签:

【中文标题】在 Scala Slick 中实现类实例成员修改的最佳方法?【英文标题】:Best way to implement member modification of class instances in Scala Slick? 【发布时间】:2013-11-21 16:30:59 【问题描述】:

我正在尝试在一个真实案例中使用 Slick 的提升嵌入方法(体育俱乐部成员个人数据的自我管理)。我已经设法从数据库中检索信息并更新记录(将成员实现为案例类并使用案例类复制方法,如下所示),但我很难找到实现成员修改的最佳方法自然的方式。

我考虑了两种可能性: 1) 维护类实例的不变性并实现通用设置器(见代码) 2)使构造函数参数为“var”(什么会抑制不变性,因此不理想)

坚持选项 1,我想出了以下代码(摘录,不是全部源代码):

    case class Member(id: Int, name: String, firstname: Option[String] = None,
      birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None,
      healthNotes: Option[String]) 

      // Just a try so far
      def set(n: String = name, fn: Option[String] = firstname, bd : Option[Date] = birthDate)
        (implicit session: Session) =  
        val m = this.copy(id,n,fn,bd)
        Members.update(m)
        m
      
    

    object Members extends Table[Member]("clubmembers") 
        with CayenneAutoPKSupport 

      def id = column[Int]("Member_ID", O.PrimaryKey) // This is the primary key column
      def name = column[String]("Name")
      def firstname = column[Option[String]]("Firstname")
      def birthDate = column[Option[Date]]("BirthDate")
      def gender = column[Option[String]]("Gender")
      def country = column[Option[String]]("Country")
      def healthNotes = column[Option[String]]("HealthNotes")

      // Every table needs a * projection with the same type as the table's type parameter
      def * = id ~ name ~ firstname ~ birthDate ~ gender ~ country ~ healthNotes <> (Member.apply _, Member.unapply _)

这按预期工作,但我希望 set 方法的命名参数具有相同的名称(什么会使调用更“自然”)。我尝试了以下(无济于事)

def set( name: String = this.name, …

这不能编译,我可以想象为什么编译器不高兴(OTOH 当前的实现似乎可以工作),但我也可以想象它可以工作。无论如何:有人看到实现这一目标的方法吗?

或者,作为实现 Slick-persisted 对象的可修改类实例的最佳实践,人们会推荐什么?

提前感谢您的任何提示。

问候

【问题讨论】:

2) 使构造函数参数“var”不是是惯用的 Slick 代码。 【参考方案1】:

实际上,如果对参数和默认值使用相同的名称,原始代码就可以工作。只有 Scala IDE 中的语法高亮显示似乎无法理解:虽然如果名称不同,默认值会(正确)突出显示为类成员,但它们只是在名称相同的情况下显示为参数本身。

这里是当前版本(按预期工作,但未正确突出显示语法):

 def set(name: String = name, firstname: String = firstname, birthDate : Option[Date] = birthDate,
        gender: String = gender, country: String = country, 
        addressBlockId: Option[Int] = addressBlockId, healthNotes: String = healthNotes)
        (implicit session: Session) =  
    val m = this.copy(id,name,Option(firstname),birthDate,
        Option(gender),Option(country),addressBlockId,Option(healthNotes))
    m
  

注意:字符串参数最好也作为 Option[String] 传递。

欢迎提出意见和建议。

问候

【讨论】:

这是一种有效的方法,我认为这是正确的答案。在案例类中是否拥有数据库访问方法是您的设计和架构的问题。如果您不希望它们像@krivachy.akos 一样,您可以将 set 方法移动到您的 dao 调用中 setMember 并将成员对象作为第一个参数传递。 这个答案与复制方法中内置的 Scala 案例类没有什么不同,它做的事情完全相同。如果您打算使用该会话变量做某事,请阅读我更新的答案。 好的。感谢您的积极回答和进一步参考材料的指针 @krivachy.akos:“做同样的事情”:是的,但从调用者的角度来看,意图是不同的(而且更清晰)。就个人而言,我发现“设置”比“复制”更正确。 @koalabi set 意味着可变性。来电者可能会将其误解为:val m = Member(...); m.set(name = "Name")。这显然是不正确的——正确的调用应该是:val updatedM = m.set(name = "Name")。所以这个动作本质上是copy,而不是set【参考方案2】:

如果这是您想要达到的目标,这将为我编译:

  def set(name: String = name, firstname: Option[String] = firstname, birthDate : Option[Date] = birthDate)(implicit session: Session) = 
    val m = Member(id, name, firstname, birthDate, gender, country, healthNotes)
    Members.update(m)
    m
  
...
member.set(firstname = Some("name"))   

我不建议在您的案例类中执行数据库功能。我们一般用case 类作为简单的数据存储对象,并且只定义有助于管理该数据的函数。

成员的更新应该发生在您的成员 DAO 类中,可能最简单的解决方案是这样的:

object MemberDao 
  def updateMemberDetails(member: Member, name: String, firstname: Option[String], birthdate : Option[Date]) = 
    val updatedMember = member.copy(name = name, firstname = firstname, birthDate = birthdate)
    Members.update(updatedMember)
  

处理可修改类的另一种解决方案是使用如下模式:

case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String]) 
  def updateName(n: String) = this.copy(name = n)
  def updateFirstName(fn: Option[String]) = this.copy(firstname = fn)
  def updateBirthDate(bd: Option[Date]) = this.copy(birthDate = bd)

最后,如果你想要一个保存函数,你可以通过隐式语法注入流畅的样式语法,它总是返回一个定义了保存函数的新成员。

case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String])

object Updatable 
  implicit class UpdatableMember(member: Member) 
    def updateName(n: String) = member.copy(name = n)
    def updateFirstName(fn: Option[String]) = member.copy(firstname = fn)
    def updateBirthDate(bd: Option[Date]) = member.copy(birthDate = bd)
    def save(implicit session: Session) = 
      ???
    
  


object MemberDao 
  def updateMember(member: Member) = 
    import Updatable._
    DB withSession  implicit session =>
      member.updateFirstName(Some("First Name")).updateBirthDate(Some(new Date(123456789))).save
    
  

希望这些选项对您有所帮助。如果我误解了您的要求,请发表评论!

更新

您不一定必须为每个成员设置一个更新方法,但您可以在那里执行其他逻辑。如果您想要一个简单的解决方案,请像以前一样使用 Scala 的内置复制功能。

val member = MemberDao.getMember(123)
val updatedMember = member.copy(birthDate = Some(new Date(123456789)), name = "Updated name")
MemberDao.persist(updatedMember)

如果这对您不起作用,请解释原因。

我在本质上存储数据的类中使用 save 方法的问题是它没有考虑到Seperation of Concerns。此外,您的 set 函数具有副作用,可能会使其他开发人员感到困惑(并且功能不太强大)。如果您只想设置名字然后将其传递给另一个类来设置生日怎么办?你想做两个数据库事务吗?我猜不会。

这就是Data Access Object and its advantages 发挥作用的地方。您会将案例类的所有数据库操作放入一个类中,从而分离关注点:您的案例类保存数据并具有仅对数据进行操作的函数,并且您的 DAO 具有查询/持久化/更新/删除您的案例类。这也使测试您的应用程序变得更容易,因为模拟 DAO 比案例类的保存函数更简单。

很多人have differing opinions on DAO's,这些陈述只是我的观点。

不使用 DAO

我建议为可更新的案例类定义一个特征:

trait DatabaseAccess[T] 
  def create(implicit session: Session): T
  def update(implicit session: Session): T
  def delete(implicit session: Session): Boolean


case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String]) extends DatabaseAccess[Member] 
  def create(implicit session: Session): Member = ???
  def delete(implicit session: Session): Boolean = ???
  def update(implicit session: Session): Member = ???

这些只是想法,我认为没有正确或错误的解决方案。选择一种对您有意义并为您提供必要的可用性/灵活性比的产品。

最佳做法

您询问了 Scala 最佳实践。您的问题更多是软件设计问题,因此阅读一般 OO 软件设计可以帮助您。 Scala 的案例类只是编写 POJO 的一种更简单的方法。因此,在将任何新函数添加到案例类之前,请先问自己一个问题:“我会把它放在 POJO 中吗?”。如果是,那就继续吧:)

【讨论】:

你好 Krivachy.Achos, 首先,感谢您提供如此详细的答案以及花时间记录它。这些肯定是相关且有趣的 cmets,我将更详细地研究它们。关于代码,是的,它可以编译,但恐怕 - 仅基于语法突出显示:-(,我承认 - “名称”与命名参数相同。这就是我试图限定它的原因。如果我的看法是true,默认值无效。 现在,关于“流利的”方法和 DAO,这正是我想要避免的:使用案例类有什么意义(这避免了对 setter 和 getter 的需求“à la JavaBean”)如果必须为每个成员实施 1 个更新方法?其次——这是一个真正的问题——在哪里可以找到关于案例类和数据库访问的最佳实践?我的意思是:我拥有并定期重读“Scala 编程,第 2 版”。 (Odersky、Spoon 和 Venners)而且我没有看到不鼓励使用 case 类中的方法。他们的例子似乎恰恰相反。 您能否提供一些指示或详细说明?提前致谢。

以上是关于在 Scala Slick 中实现类实例成员修改的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何在头文件C++中实现类对象

如何在派生类中实现类的基本函数

在 pyspark 中实现类不平衡算法

如何在 Scala Slick 中运行补丁/部分数据库更新?

即使在 XCTest 文件中实现类扩展中的方法后,仍会调用协议的默认实现

Scala快速入门--继承与重写的使用