我们将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系统中的常见问题,但我无法找到解决方案。任何帮助表示赞赏。
答案 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]
。