Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

schneiderlin/autoMapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

4 Commits

Repository files navigation

为什么需要auto-mapper

在开发的过程中,一个相似的数据类经常会在多个层次中出现。 例如在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)

auto mapper可以做什么

  • 不同顺序,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, ...)
    

relative works

bfil的auto-mapper,这个是用反射获取class的各个field的名称和类型,然后用模板生成转换代码。
nevillelyh的RecordMapper,使用shapeless做generic programming实现的。只有当需要转换的两个类字段的顺序和个数完全一样的时候,才能转换。主要参考了这个项目的实现,增加了对字段顺序不同,个数不同(隐藏部分字段)的支持。

examples

在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_

开启Option[A]转A功能(默认关闭)

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_ 

nested type

如何实现的

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

对HList进行修改

关于怎么写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)
 }
LO的field比LI的field少

通过上面Selector,自动解决了这个问题。因为是递归循环选LO的所有field。

LI中某个key对应的类型是V,但是LO中key对应的类型是W

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)
 }
LI中某个key对应的类型是Option[V],但是LO中key对应的类型是V

提供一个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

递归的对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)
 }

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /