Spark / Scala,数据集和案例类的多态性

时间:2017-06-22 19:52:02

标签: scala apache-spark design-patterns polymorphism

我们将Spark 2.x与Scala一起用于具有13种不同ETL操作的系统。其中7个相对简单,每个都由一个域类驱动,主要区别在于这个类以及负载处理方式的一些细微差别。

加载类的简化版本如下,为了这个例子的目的,说有7个披萨配料被加载,这里是Pepperoni:

object LoadPepperoni {
  def apply(inputFile: Dataset[Row],
            historicalData: Dataset[Pepperoni],
            mergeFun: (Pepperoni, PepperoniRaw) => Pepperoni): Dataset[Pepperoni] = {
    val sparkSession = SparkSession.builder().getOrCreate()
    import sparkSession.implicits._

    val rawData: Dataset[PepperoniRaw] = inputFile.rdd.map{ case row : Row =>
      PepperoniRaw(
          weight = row.getAs[String]("weight"),
          cost = row.getAs[String]("cost")
        )
    }.toDS()

    val validatedData: Dataset[PepperoniRaw] = ??? // validate the data

    val dedupedRawData: Dataset[PepperoniRaw] = ??? // deduplicate the data

    val dedupedData: Dataset[Pepperoni] = dedupedRawData.rdd.map{ case datum : PepperoniRaw =>
        Pepperoni( value = ???, key1 = ???, key2 = ??? )
    }.toDS()

    val joinedData = dedupedData.joinWith(historicalData,
      historicalData.col("key1") === dedupedData.col("key1") && 
        historicalData.col("key2") === dedupedData.col("key2"),
      "right_outer"
    )

    joinedData.map { case (hist, delta) =>
      if( /* some condition */) {
        hist.copy(value = /* some transformation */)
      }
    }.flatMap(list => list).toDS()
  }
}

换句话说,该类对数据执行一系列操作,操作大多相同且始终以相同的顺序,但每个顶部可能略有不同,从“原始”到“域”的映射也是如此。合并功能。

要为7个浇头(即蘑菇,奶酪等)执行此操作,我宁愿不复制/粘贴类并更改所有名称,因为结构和逻辑对所有加载都是通用的。相反,我宁愿定义一个泛型类型的泛型“Load”类,如下所示:

object Load {
  def apply[R,D](inputFile: Dataset[Row],
            historicalData: Dataset[D],
            mergeFun: (D, R) => D): Dataset[D] = {
    val sparkSession = SparkSession.builder().getOrCreate()
    import sparkSession.implicits._

    val rawData: Dataset[R] = inputFile.rdd.map{ case row : Row =>
...

对于每个特定于类的操作,例如从“原始”到“域”的映射或合并,都有一个实现特定的特征或抽象类。这将是典型的依赖注入/多态模式。

但是我遇到了一些问题。从Spark 2.x开始,编码器仅提供给本机类型和案例类,并且无法将类一般地标识为案例类。因此,在使用泛型类型时,推断的toDS()和其他隐式功能不可用。

同样如this related question of mine中所述,使用泛型时,案例类copy方法也不可用。

我已经研究了Scala和Haskell常见的其他设计模式,例如类型类或ad-hoc多态,但障碍是Spark数据集基本上只处理案例类,不能抽象地定义。

这似乎是Spark系统中的常见问题,但我无法找到解决方案。任何帮助表示赞赏。

1 个答案:

答案 0 :(得分:5)

启用.toDS的隐式转换是:

implicit def rddToDatasetHolder[T](rdd: RDD[T])(implicit arg0: Encoder[T]): DatasetHolder[T]

(来自https://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.SQLImplicits

您完全正确,因为Encoder[T]的范围内没有隐含的值,因为您已将apply方法设为通用,因此无法进行此转换。但你可以简单地接受一个作为隐含参数!

object Load {
  def apply[R,D](inputFile: Dataset[Row],
            historicalData: Dataset[D],
            mergeFun: (D, R) => D)(implicit enc: Encoder[D]): Dataset[D] = {
...

然后,在调用负载时,使用特定类型,它应该能够找到该类型的编码器。请注意,您在调用上下文中也必须import sparkSession.implicits._

编辑:类似的方法是通过绑定您的类型(newProductEncoder[T <: Product](implicit arg0: scala.reflect.api.JavaUniverse.TypeTag[T]): Encoder[T])并接受隐式apply[R, D <: Product]作为参数来启用隐式JavaUniverse.TypeTag[D]