在Scala中将实木复合地板读取为案例类对象的列表

时间:2019-08-11 12:54:36

标签: scala apache-spark parquet

假设您已经将某些案例类的集合写到了实木复合地板上,然后又想在另一个Spark作业中读取它,回到同一案例类(也就是说,您已经写了一些List[MyCaseClass]并且喜欢读回来)。

出于一般性考虑,假设MyCaseClass中嵌套了案例类。

目前,只有使用以下代码蓝图,我才能使它工作:

  /** applies the secret sauce for coercing to a case class that is implemented by spark's flatMap */
  private def toCaseClass(spark : SparkSession, inputDF : DataFrame) : Dataset[MyCaseClass] = {
    import spark.implicits._
    inputDF.as[MyCaseClass].flatMap(record => {
      Iterator[MyCaseClass](record)
    })
  }

似乎在Spark 2.x中,flatMap将导致进行转换/强制转换的实验性Spark代码(当使用调试器查看该代码时,在Spark代码库中将其注释为实验性)。显然,在Java / Scala中,序列化通常是一个棘手的问题。还有其他安全的方法吗?

除了火花之外,我还发现了其他关于stackoverflow的独立代码解决方案,它们摇摇欲坠且支持不佳。

我正在寻找不需要手动编码的干净,声明性方式,即如何转换每个字段,这些方法依赖于受支持的可靠库,而这些库不依赖于以慢速进行的超慢反射优雅。这可能是绝望的混合,但这是一种以其案例类为傲并且将Spark作为其主要成就之一的语言。

也欢迎对为什么不使用案例类发表评论!

2 个答案:

答案 0 :(得分:5)

正如Luis Miguel所评论的那样,大多数Dataset API都标记为实验性的,但是已经稳定并且已经在生产中使用了数年。

Dataset.as [U]

的问题

您是正确的,简单地使用.as[MyCaseClass]与显式实例化case类有一些细微的区别:最重要的是Dataset.as[U]不保证您的数据集仅包含按类型定义的列U,它可能会保留其他数据,这些数据以后可能会中断计算。

这是一个例子:

import org.apache.spark.sql._
import org.apache.spark.sql.functions._

case class MyData(value: Int)

val df: DataFrame = spark.createDataset(Seq(1,1,2,3)).withColumn("hidden",rand)

val ds: Dataset[MyData] = df.as[MyData]

ds.distinct.count
res3: Long = 4

数据集ds保留hidden列值,即使未在MyData类型中定义它也可能生成意外结果:有人将数据集ds视为{{上面的1}}肯定希望不重复计数为3,而不是4。

如何安全地转换为数据集[MyData]?

如果您明确希望只将案例类列保留在数据集中,则有一个非常简单的解决方案(但性能欠佳):将其提取为RDD并将其重新转换为Dataset [U]。

MyData

基本上,它以相同的成本完成了val ds = df.as[MyData].rdd.toDS() ds.distinct.count res5: Long = 3 的工作:Spark需要将其内部行格式的数据反序列化以创建case类实例并将其重新序列化为内部行。 它会产生不必要的垃圾,增加内存压力,并可能破坏WholeStage代码生成的优化。

我认为,更好的方法是,在将数据集转换为指定案例类时,从源DataFrame中选择必要的列。这样可以避免flatMap的大部分不良副作用,但不会带来反序列化/序列化的费用。

一种完美的实现方式是利用Scala功能通过implicit classes扩展现有类和实例的行为:

as[U]

有了上述对象,我现在可以修改我的初始代码:

import scala.reflect.runtime.universe.TypeTag
import org.apache.spark.sql._

object SparkExtensions {
  implicit class ExtendedDataFrame(df: DataFrame) {
    def to[T <: Product: TypeTag]: Dataset[T] = {
      import df.sparkSession.implicits._
      import org.apache.spark.sql.functions.col
      df.select(Encoders.product[T].schema.map(f => col(f.name)): _*).as[T]
    }
  }
}

答案 1 :(得分:0)

我已经完成了一些非常复杂和嵌套的case class类型,并且从未做过您拥有的身份.flatMap()

通常,我只是确保范围内有一个隐式Encoder,仅使用.as[MyCaseClass]DataFrame转换为Dataset,spark看起来就足够开心了。 / p>

我有一个很常见的模式:

implicit def enc: Encoder[MyCaseClass] = Encoders.product[MyCaseClass]

自然,您还必须为每种嵌套类型都使用一个单独的编码器。只要它们都扩展Product(就像case class一样),那么Encoders.product[T]就可以工作。