簡體   English   中英

在Scala中讀取鑲木地板作為案例類對象的列表

[英]Read parquet as list of case class objects in Scala

假設你已經將一些case類的集合寫成了鑲木地板,然后想在另一個spark作業中讀取它,回到同一個case類(也就是說,你已經編寫了一些List[MyCaseClass]並希望閱讀它背部)。

MyCaseClass ,假設MyCaseClass包含嵌套的case類。

目前我只能使用此代碼藍圖來實現此功能:

  /** 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代碼庫中注釋為實驗性的)。 顯然,序列化通常是Java / Scala中棘手的問題。 有額外的,安全的方式嗎?

除了spark之外,我發現在stackoverflow上其他地方建議的獨立代碼解決方案不穩定且支持不足。

我正在尋找干凈,聲明性的方法,不需要手工編碼如何轉換每個字段,這依賴於良好支持的實體庫,這些庫不依賴於超慢速反射以擊敗優雅。 可能是一個不可能的渴望組合,但這只是一種語言的驕傲,並將Spark作為其主要成就之一。

相反,關於為什么不使用案例類的評論也是受歡迎的!

正如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視為上面的MyData集合的人肯定會期望非重復計數為3而不是4。

如何安全地轉換為數據集[MyData]?

如果您明確希望僅在數據集中保留案例類列,則有一個非常簡單的解決方案(但具有次優性能):將其提取為RDD並將其重新轉換為數據集[U]。

val ds = df.as[MyData].rdd.toDS()

ds.distinct.count
res5: Long = 3

它基本上完成了你的flatMap以相同的成本做的事情:Spark需要從其內部行格式反序列化數據以創建案例類實例並將其重新序列化為內部行。 它會產生不必要的垃圾,增加內存壓力,並可能破壞WholeStage codegen優化。

在我看來,更好的方法是在將數據集強制轉換為指定的case類時,從源DataFrame中選擇必要的列。 這將防止as[U]大部分不必要的副作用,但沒有反序列化/序列化成本。

一種優雅的方法是利用Scala功能使用隱式類擴展現有類和實例的行為:

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]
    }
  }
}

有了上面的對象,我現在可以修改我的初始代碼:

import SparkExtensions._

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

ds.distinct.count
res11: Long = 3

ds.printSchema
root
 |-- value: integer (nullable = false)

我已經做了一些非常復雜和嵌套的case class類型,並且從來沒有必要使用你擁有的.flatMap()

一般來說,我只是確保我在范圍內有一個隱式Encoder ,並且只需使用.as[MyCaseClass]DataFrame轉換為Dataset ,spark就足夠了。

我有一個非常常見的模式:

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

當然,您還必須為每個嵌套類型都有一個單獨的編碼器。 只要它們都擴展Product (就像case class那樣),那么Encoders.product[T]可以了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM