【问题标题】:Creating a nested generic Case Class from a flattened Tuple从扁平元组创建嵌套的通用案例类
【发布时间】:2017-01-16 00:56:17
【问题描述】:

背景

我正在开发一个同时使用 Play Framework 和 Slick 的 API。为了避免重复的样板,我想定义我的公共 JSON 模型而不使用其 ID 字段并将它们包装在 WithId 容器中。

import play.api.libs.json._
import play.api.libs.functional.syntax._

case class WithId[T](id: Long, item: T)
case class Wiki(name: String, source: Option[String], text: String)

object WithId {
  implicit def withIdRead[T : Reads] : Reads[WithId[T]] = (
    (JsPath \ "id").read[Long] and
    JsPath.read[T]
  )((id, item) => WithId(id, item))

  implicit def withIdWrite[T : Writes] : Writes[WithId[T]] = (
    (JsPath \ "id").write[Long] and
    JsPath.write[T]
  ).apply(unlift(WithId.unapply[T]))
}

感谢ReadsWrites 定义的魔力,我可以轻松处理带或不带id 的JSON。

scala> val rawIdJson = """{"id": 123, "name": "My First Wiki", "text": "This is my first wiki article"}"""
rawIdJson: String = {"id": 123, "name": "My First Wiki", "text": "This is my first wiki article"}

scala> val withId = Json.parse(rawIdJson).validate[WithId[Wiki]].get
withId: model.util.WithId[model.entity.Wiki] = WithId(123,Wiki(My First Wiki,None,This is my first wiki article))

scala> val withIdJson = Json.toJson(withId)
withIdJson: play.api.libs.json.JsValue = {"id":123,"name":"My First Wiki","text":"This is my first wiki article"}



scala> val rawJson = """{"name": "My First Wiki", "text": "This is my first wiki article"}"""
rawJson: String = {"name": "My First Wiki", "text": "This is my first wiki article"}

scala> val withoutId = Json.parse(rawJson).validate[Wiki].get
withoutId: model.entity.Wiki = Wiki(My First Wiki,None,This is my first wiki article)

scala> val withoutIdJson = Json.toJson(withoutId)
withoutIdJson: play.api.libs.json.JsValue = {"name":"My First Wiki","text":"This is my first wiki article"}

一切顺利。

我现在遇到的问题是 Slick 将以元组或案例类的形式从数据库返回行,具体取决于我使用的查询。显然,我可以编写很多非常直接的辅助方法来将元组/案例类转换为每个公共模型:

object Wiki {
  implicit val wikiFmt = Json.format[Wiki]
  def fromRow(row: WikiRow) : WithId[Wiki] = WithId(row.id, Wiki(row.name, row.source, row.text))
  def fromRow(tup: (Long, String, Option[String], String)) : WithId[Wiki] = WithId(tup._1, Wiki(tup._2, tup._3, tup._4))
}

...但是随着公共模型数量的增长,需要维护的样板文件很多。

问题

  1. 有没有一种干净的方法可以将Tuple4[Long, String, Option[String], String]case class WikiRow(id: Long, name: String, source: Option[String], text: String) 转换为WithId[Wiki](反之亦然)?

  2. 一旦我介绍了另一个公共模型,如 case class Template(name: String, description: String),我们能否将解决方案从 #1 推广到现在处理将 Tuple3[Long, String, Strong] 转换为 WithId[Template](反之亦然)?

    李>
  3. 如果我们将一个未在公共模型中使用的字段放入私有模型中会发生什么?例如,case class WikiRow(id: Long, name: String, source: Option[String], text: String, hidden: Boolean)hidden 字段在转到 WikiRow => WithId[Wiki] 时需要删除,并在转到 WithId[Wiki] => WikiRow 时从另一个来源提供。

【问题讨论】:

  • 如何定义从元组或案例类到WithId[Model]类的隐式转换?
  • @Yaneeve - 这与为每个公共模型定义 fromRow 有何不同?我觉得WithId[T] 可以获取一个元组,将Long 的头部从它身上剪下来,然后将元组的尾部传递给T 的构造函数。我只是不确定那是什么魔法。
  • 没有什么不同,只是您不必显式编写调用。至于魔法,很遗憾我帮不了你。只是提醒一下,在这种情况下,魔术只是运行我们看不到(或理解)的代码。关于元组的魔术师是shapless。我不太了解它,但它可能会帮助您实现您的愿望

标签: scala tuples shapeless case-class


【解决方案1】:

至于问题 1 和 2:是的,正如建议的那样,无形是可能的。这是从元组或行转到WithId[T] 的解决方案。

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class WikiRow(id: Long, name: String, source: Option[String], text: String) 
case class Wiki(name: String, source: Option[String], text: String)
case class WithId[T](id: Long, item: T)

def createWithId[T] = new WithIdCreator[T]

class WithIdCreator[Out] {
  import shapeless._
  import shapeless.ops.hlist.IsHCons
  def apply[In, InGen <: HList, Tail <: HList](in: In)(
    implicit
    genIn: Generic.Aux[In,InGen],
    hcons: IsHCons.Aux[InGen,Long,Tail],
    genOut: Generic.Aux[Out,Tail]
  ): WithId[Out] = {
    val rep = genIn.to(in)
    val id = hcons.head(rep)
    val tail = hcons.tail(rep)
    WithId(id, genOut.from(tail))
  }
}

// Exiting paste mode, now interpreting.

defined class WikiRow
defined class Wiki
defined class WithId
createWithId: [T]=> WithIdCreator[T]
defined class WithIdCreator

scala> createWithId[Wiki](WikiRow(3L, "foo", None, "barbaz"))
res1: WithId[Wiki] = WithId(3,Wiki(foo,None,barbaz))

scala> createWithId[Wiki]((3L, "foo", None: Option[String], "barbaz"))
res2: WithId[Wiki] = WithId(3,Wiki(foo,None,barbaz))

scala> case class Template(name: String, description: String)
defined class Template

scala> createWithId[Template]((3L, "foo", "barbaz"))
res3: WithId[Template] = WithId(3,Template(foo,barbaz))

另一个方向的转换或多或少是类似的。

我看不出为什么 3 也不可能,但是您将不得不再次重写转换以处理删除或手动提供参数。

您可以在shapeless guide 中了解更多信息。里面解释了所有必要的概念。

【讨论】:

  • 这几乎正是我的想法。感谢您提供该指南的链接,它看起来会很有帮助。
猜你喜欢
  • 1970-01-01
  • 2018-08-03
  • 2018-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-10
相关资源
最近更新 更多