我正在使用Play Framework和Slick处理API。为了避免重复的锅炉板,我想在没有ID字段的情况下定义我的公共JSON模型,并将它们包装在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]))
}
感谢Reads
和Writes
定义的神奇之处,我可以轻松处理带有或不带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将以元组或案例类的形式从数据库中返回行,具体取决于我正在使用的查询。显然,我可以编写很多非常简单的帮助方法来将元组/ case类转换为每个公共模型:
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))
}
...但随着公共模型数量的增长,这需要维护很多样板。
是否有一种干净的方式来获取Tuple4[Long, String, Option[String], String]
或case class WikiRow(id: Long, name: String, source: Option[String], text: String)
并将其转换为WithId[Wiki]
(反之亦然)?
在我介绍其他公共模型case class Template(name: String, description: String)
之后,我们是否可以将解决方案从#1推广到现在处理将Tuple3[Long, String, Strong]
转换为WithId[Template]
(反之亦然)?
如果我们将一个字段放入未在公共模型中使用的私有模型,会发生什么?例如,case class WikiRow(id: Long, name: String, source: Option[String], text: String, hidden: Boolean)
。 hidden
字段需要在转到WikiRow => WithId[Wiki]
时删除,并在转到WithId[Wiki] => WikiRow
时从其他来源提供。
答案 0 :(得分:3)
关于问题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中详细了解相关信息。所有必要的概念都在那里解释。