假设您已经将某些案例类的集合写到了实木复合地板上,然后又想在另一个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作为其主要成就之一的语言。
也欢迎对为什么不使用案例类发表评论!
答案 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]
就可以工作。