You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Shapeless的Scala Case Class DTO转换问题求助

Scala Case Class 转换:处理字段组合与字段名映射的方案

你提到的基于Shapeless的同名字段转换器仅能处理字段名、类型完全匹配的场景,针对字段组合和字段名映射这两类需求,下面提供几种可行的实现方式:

一、基于Shapeless扩展的类型安全方案

Shapeless的LabelledGeneric可以保留字段的标签信息,我们可以通过扩展它来支持自定义字段映射和组合逻辑,同时保留编译时类型检查。

1. 字段名映射场景实现

通过重写字段的符号标签,将DTO的字段映射到目标Case Class的对应字段:

import shapeless._
import shapeless.labelled.{FieldType, field}

object DtoToDomainMapper {
  // 定义字段映射的类型类
  trait FieldMapping[A, B] {
    def apply(a: A): B
  }

  // 单个字段的映射逻辑
  implicit def mapSingleField[K <: Symbol, V, TargetK <: Symbol](implicit
    targetWitness: Witness.Aux[TargetK]
  ): FieldMapping[FieldType[K, V], FieldType[TargetK, V]] =
    (sourceField: FieldType[K, V]) => field[TargetK](sourceField)

  // 递归处理HList的字段映射
  implicit val hnilMapping: FieldMapping[HNil, HNil] = _ => HNil
  implicit def hconsMapping[K <: Symbol, V, T <: HList, TT <: HList](implicit
    headMap: FieldMapping[FieldType[K, V], TT#Head],
    tailMap: FieldMapping[T, TT#Tail]
  ): FieldMapping[FieldType[K, V] :: T, TT] =
    (hlist: FieldType[K, V] :: T) => headMap(hlist.head) :: tailMap(hlist.tail)

  // 对外暴露的转换方法
  def convert[S, T, SR <: HList, TR <: HList](source: S)(implicit
    genS: LabelledGeneric.Aux[S, SR],
    genT: LabelledGeneric.Aux[T, TR],
    mapping: FieldMapping[SR, TR]
  ): T = genT.from(mapping(genS.to(source)))
}

// 使用示例
case class UserDTO(name: String, surname: String, age: Int)
case class User(firstName: String, surname: String, age: Int)

// 为name到firstName的映射提供隐式证据
implicit val nameToFirstName: DtoToDomainMapper.FieldMapping[
  FieldType[Symbol @@ "name", String],
  FieldType[Symbol @@ "firstName", String]
] = field => field[Symbol @@ "firstName"](field)

val dto = UserDTO("John", "Doe", 30)
val user = DtoToDomainMapper.convert[UserDTO, User](dto)
// 输出:User(John, Doe, 30)

2. 字段组合场景实现

在转换流程中插入自定义逻辑,将多个DTO字段组合成目标字段:

import shapeless._
import shapeless.labelled.{FieldType, field}

object DtoToDomainMapper {
  // 定义字段组合的类型类
  trait FieldCombiner[S, T] {
    def combine(source: S): T
  }

  // 针对UserDTO到User的组合逻辑
  implicit val userCombiner: FieldCombiner[UserDTO, User] = (dto: UserDTO) =>
    User(dto.name + dto.surname, dto.age)

  // 对外暴露的转换方法
  def convert[S, T](source: S)(implicit combiner: FieldCombiner[S, T]): T = combiner.combine(source)
}

// 使用示例
case class UserDTO(name: String, surname: String, age: Int)
case class User(fullName: String, age: Int)

val dto = UserDTO("John", "Doe", 30)
val user = DtoToDomainMapper.convert[UserDTO, User](dto)
// 输出:User(JohnDoe, 30)

如果需要结合自动同名字段转换+部分字段组合,可以扩展Shapeless的Intersection逻辑,在自动映射后补充组合字段:

import shapeless._
import shapeless.labelled.{FieldType, field}

class Converter[T] {
  def apply[S, SR <: HList, TR <: HList, IR <: HList](source: S)(implicit
    genS: LabelledGeneric.Aux[S, SR],
    genT: LabelledGeneric.Aux[T, TR],
    intersection: Intersection.Aux[SR, TR, IR],
    align: Align[IR, TR],
    supplement: Supplement[S, TR]
  ): T = {
    val autoMapped = align(intersection(genS.to(source), HNil))
    val completeHList = supplement(source, autoMapped)
    genT.from(completeHList)
  }
}

// 定义补充字段的类型类
trait Supplement[S, TR <: HList] {
  def apply(source: S, partial: TR): TR
}

// 针对UserDTO到User的补充逻辑
implicit val userSupplement: Supplement[UserDTO, FieldType[Symbol @@ "fullName", String] :: FieldType[Symbol @@ "age", Int] :: HNil] =
  (dto, partial) => field[Symbol @@ "fullName"](dto.name + dto.surname) :: partial.tail

// 使用
val converter = new Converter[User]
val user = converter(UserDTO("John", "Doe", 30))

二、类似Quill Alias的DSL方案

实现轻量级DSL,让你像Quill那样直观声明字段映射和组合规则:

case class MappingRule[S](rules: List[(S => Any, String)])

object MappingDSL {
  // 字段名映射规则
  def alias[S](sourceField: S => Any)(targetField: String): MappingRule[S] =
    MappingRule(List((sourceField, targetField)))

  // 字段组合规则
  def combine[S](sourceFields: (S => Any)*)(targetField: String)(combiner: Seq[Any] => Any): MappingRule[S] =
    MappingRule(List((s => combiner(sourceFields.map(_(s))), targetField)))

  // 执行转换
  def convert[S, T](source: S, rules: MappingRule[S]*)(implicit ct: scala.reflect.ClassTag[T]): T = {
    val constructor = ct.runtimeClass.getDeclaredConstructors.head
    val ruleMap = rules.flatMap(_.rules).toMap
    val targetFieldNames = ct.runtimeClass.getDeclaredFields.map(_.getName)

    val args = targetFieldNames.map { fieldName =>
      ruleMap.get(fieldName) match {
        case Some(rule) => rule(source)
        case None => // 自动匹配同名字段
          source.getClass.getMethod(fieldName).invoke(source)
      }
    }
    constructor.newInstance(args: _*).asInstanceOf[T]
  }
}

// 使用示例
// 字段名映射场景
val dto = UserDTO("John", "Doe", 30)
val user1 = MappingDSL.convert(dto, 
  MappingDSL.alias[UserDTO](_.name)("firstName")
)
// 输出:User(John, Doe, 30)

// 字段组合场景
case class User(fullName: String, age: Int)
val user2 = MappingDSL.convert(dto,
  MappingDSL.combine[UserDTO](_.name, _.surname)("fullName")(_.mkString)
)
// 输出:User(JohnDoe, 30)

三、Scala反射方案

反射方案实现简单,但缺少编译时类型检查,适合快速原型或规则简单的场景:

import scala.reflect.runtime.universe._

object ReflectConverter {
  def convert[S: TypeTag, T: TypeTag](source: S): T = {
    val mirror = runtimeMirror(source.getClass.getClassLoader)
    val sourceInstance = mirror.reflect(source)
    val targetClass = typeOf[T].typeSymbol.asClass
    val targetMirror = mirror.reflectClass(targetClass)
    val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
    val paramNames = constructor.paramLists.head.map(_.name.toString)

    val args = paramNames.map { paramName =>
      paramName match {
        case "firstName" => sourceInstance.reflectField(typeOf[S].decl(TermName("name")).asTerm).get
        case "fullName" => 
          val name = sourceInstance.reflectField(typeOf[S].decl(TermName("name")).asTerm).get.asInstanceOf[String]
          val surname = sourceInstance.reflectField(typeOf[S].decl(TermName("surname")).asTerm).get.asInstanceOf[String]
          name + surname
        case _ => // 自动匹配同名字段
          sourceInstance.reflectField(typeOf[S].decl(TermName(paramName)).asTerm).get
      }.asInstanceOf[AnyRef]
    }
    targetMirror.reflectConstructor(constructor)(args: _*).asInstanceOf[T]
  }
}

// 使用示例
val dto = UserDTO("John", "Doe", 30)
val user1 = ReflectConverter.convert[UserDTO, User](dto) // 字段名映射场景
val user2 = ReflectConverter.convert[UserDTO, User](dto) // 字段组合场景

方案选型建议

  • 类型安全优先:选择基于Shapeless的扩展方案,编译时即可检查字段匹配和类型正确性。
  • 简洁易用优先:选择类似Quill Alias的DSL方案,语法直观,学习成本低。
  • 快速实现:反射方案适合小项目或临时需求,但需注意运行时错误风险。

内容的提问来源于stack exchange,提问作者Lindenes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:07:35