在开发的过程中,一个相似的数据类经常会在多个层次中出现。 例如在DAO层有一个ProductRow类,用来存放product table的字段。
case class ProductRow(id: Long, seller: Long, name: String)
在业务逻辑层,有一个Product类,表示的也是一个product,但是seller的类型不是一个用户id,而是一个User类的object。
case class Product(id: Long, seller: User, name: String)
例如在DAO层,有一个UserRow类,里面包括了用户的各种credential
case class UserRow(id: Long, password: String, ...)
但是在别的抽象层次,并不需要用户的密码等信息,例如一个专门做序列化然后返回json给前端的User类。
case class User(id: Long, ...)
要在这些case class之间做转换,需要写大量的代码,特别是在写ontology特别多的应用的时候。user, product, address, payment, ...,每一个业务实体都要写一整套的转换代码
def productRowToProduct(row: ProductRow): Product: {
Product(
id = row.id
seller = userRowToUser(dao.byId(row.seller)),
name = row.name
)
}
auto-mapper就是解决这个问题的,不需要每一个类写转换的代码,就可以用一行代码完成转换
// 召唤一个能把ProductRow映射成Product的instance
val p = Projection[ProductRow, Product]
val product = p.to(row)
- 不同顺序,A(i: Int, l: Long, //...其他基础类型) -> B(l: Long, i: Int, //...)
- Option可以开启/关闭,A(i: Option[Int]) -> B(i: Int)
- 类型不同,用户提供自定义转换方法
implicit def vToW(v: V): W = ??? A(fieldName: V) -> B(fieldName: W)
- A的字段比B多(隐藏部分字段), A(i: Int, hide: Long, ...) -> B(i: Int, ...)
- nested type自动转换,如果A可以转换成B,那么C可以转换成D
case class C(a: A, ...) case class D(b: B, ...)
bfil的auto-mapper,这个是用反射获取class的各个field的名称和类型,然后用模板生成转换代码。
nevillelyh的RecordMapper,使用shapeless做generic programming实现的。只有当需要转换的两个类字段的顺序和个数完全一样的时候,才能转换。主要参考了这个项目的实现,增加了对字段顺序不同,个数不同(隐藏部分字段)的支持。
在build.sbt中添加
case class A(i: Int, l: Long)
case class B(l: Long, i: Int)
val p = Projection[A, B]
val a = A(1, 1L)
val b = B(1L, 1)
val b_ = p.to(a)
b shouldEqual b_
case class A(i: Int, l: Long, hide: String)
case class B(l: Long, i: Int)
val p = Projection[A, B]
val a = A(1, 1L, "some secret")
val b = B(1L, 1)
val b_ = p.to(a)
b shouldEqual b_
case class A(i: Int, l: Option[Long])
case class B(l: Long, i: Int)
import Projection.UnsafeOptionExtractorImplicits._
val p = Projection[A, B]
val a = A(1, Some(1L))
val b = B(1L, 1)
val b_ = p.to(a)
b shouldEqual b_
// 在数据库里面,images是用逗号分隔的方式存储的
case class A(images: String)
// 在业务层中,images是list of url
case class B(images: List[String])
// 用户自定义一个从String转换成List[String]的方法,注意控制implicit的scope
implicit def imagesSplit(s: String): List[String] =
images.split(",").toList
val a = A("1.png,2.png,3.jpg")
val b = B(List("1.png", "2.png", "3.jpg"))
val p = Projection[A, B]
val b_ = p.to(a)
b shouldEqual b_
case class可以转换成他的generic representation, HList。
基本思路是利用shapeless的Generic,把一个case class转成HList,再从HList转成另一个case class
case class A(i: Int, s: String)
case class B(i: Int, s: String)
val a = A(1, "s")
val b = B(2, "s")
val genA = Generic[A]
val genB = Generic[B]
val hlistA = genA.to(a) // 1 :: "s" :: HNil
val b_ = genB.from(hlistA) // B(2, "s")
现在可以写一个最基本的Projection[A, B],当A和B的字段完全一样的时候可以转换
// 限制A和B的generic representation是一样的(都是L)
class Projection[A, B] {
def to[L <: HList](a: A)(implicit
genA: LabelledGeneric.Aux[A, L],
genB: LabelledGeneric.Aux[B, L])
: B = genB.from(genA.to(a))
}
如果要支持上面说的字段乱序和隐藏,需要额外定义一个MapRecord,MapRecord提供一个把A的HList转换成 B的HList的方法
trait MapRecord[LI <: HList, LO <: HList] {
def apply(l: LI): LO
}
现在可以给Projection提供多一个MapRecord。表示A和B的generic representation不必完全一样,只需要 LA能够转换到LB就足够了。
class Projection[A, B] extends Serializable {
def to[LA <: HList, LB <: HList](a: A)(implicit
genA: LabelledGeneric.Aux[A, LA],
genB: LabelledGeneric.Aux[B, LB],
mr: MapRecord[LA, LB])
: B = genB.from(mr(genA.to(a)))
}
关于怎么写MapRecord,其实就是对输入的type LI进行一些type level的操作,使之变成type LO。
shapeless提供了一个Selector,可以在HList里面获取对应的key value pair。
val a = A(1, "s")
val genA = LabelledGeneric[A]
val hlistA = genA.to(a)
val i = Witness('i)
val s = Witness('s)
val iSelector = Selector[genA.Repr, i.T]
val sSelector = Selector[genA.Repr, s.T]
val iValue = iSelector(hlistA) // 1,key[i]对应的值
val sValue = sSelector(hlistA) // "s", key[s]对应的值
可以利用Selector,循环LO中所有的field Key,在LI中select对应的key value pair
// 假设B有两个field,keys分别是[s, i]
// 两个keys各有一个对应的Selector
val iSelector = Selector[genA.Repr, i.T]
val sSelector = Selector[genA.Repr, s.T]
// 按照B中field的顺序对LI进行select
val hlistB = sSelector(hlistA) :: iSelector(hlistA) :: HNil
在这个例子中,成功把一个Int :: String :: HNil转换成了String :: Int :: HNil。现在可以利用 递归,让编译器自动推导所有的乱序转换
// 递归终点,SourceHList要转换成HNil,HNil里面什么field都没有,所以根本不需要select,直接
// 返回HNil就可以
implicit def hnilMapRecord[SourceHList <: HList]: MapRecord[SourceHList, HNil] = new MapRecord[SourceHList, HNil] {
override def apply(l: SourceHList): HNil = HNil
}
// 递归case,假设TargetHList的Tail可以转换(mrT),并且可以在SourceHList中选出field K(select)
// 那么就可以推导出存在一个MapRecord[SourceHList, FieldType[K, V] :: TargetHListTail]
implicit def hconsMapRecordBase[K, V, SourceHList <: HList, TargetHListTail <: HList]
(implicit
select: Selector.Aux[SourceHList, K, V],
mrT: Lazy[MapRecord[SourceHList, TargetHListTail]])
: MapRecord[SourceHList, FieldType[K, V] :: TargetHListTail] = new MapRecord[SourceHList, FieldType[K, V] :: TargetHListTail] {
override def apply(l: SourceHList): FieldType[K, V] :: TargetHListTail =
field[K](select(l)) :: mrT.value(l)
}
通过上面Selector,自动解决了这个问题。因为是递归循环选LO的所有field。
V和W虽然不是一样的类型,但是只需要有一个从V到W的函数,就能完成转换。
只需要在自动推导的时候增加一个implicit V => W
// 只是一个type alias,方便自己不用重复写很长的type
type MV[SourceHList <: HList, K, V, TargetHListTail <: HList] =
MapRecord[SourceHList, FieldType[K, V] :: TargetHListTail]
implicit def hconsMapRecord1[K, V, W, SourceHList <: HList, TargetHListTail <: HList]
(implicit
select: Selector.Aux[SourceHList, K, V],
f: V => W,
mrT: Lazy[MapRecord[SourceHList, TargetHListTail]])
: MV[SourceHList, K, W, TargetHListTail] = new MV[SourceHList, K, W, TargetHListTail] {
override def apply(l: SourceHList): FieldType[K, W] :: TargetHListTail =
field[K](f(select(l))) :: mrT.value(l)
}
提供一个UnsafeOptionExtractorImplicits,当这个implicit在scope里的时候,可以把Option[V]变成V
object UnsafeOptionExtractorImplicits {
implicit def apply[T]: UnsafeOptionExtractor[T] = new UnsafeOptionExtractor[T]
}
如果在LI中V,在LO中是Option[V],只需要在select了V之后加上Option functor的pure方法即可。
递归的对nested type进行转换
implicit def hconsMapRecord0[K, V, W, VRepr <: HList, WRepr <: HList, SourceHList <: HList, TargetHListTail <: HList]
(implicit
select: Selector.Aux[SourceHList, K, V],
genV: LabelledGeneric.Aux[V, VRepr],
genW: LabelledGeneric.Aux[W, WRepr],
mrH: Lazy[MapRecord[VRepr, WRepr]],
mrT: Lazy[MapRecord[SourceHList, TargetHListTail]])
: MV[SourceHList, K, W, TargetHListTail] = new MV[SourceHList, K, W, TargetHListTail] {
override def apply(l: SourceHList): FieldType[K, W] :: TargetHListTail =
field[K](genW.from(mrH.value(genV.to(select(l))))) :: mrT.value(l)
}