DataFrame转DataSet 实现存储自定义对象case class

8 篇文章 0 订阅
根据介绍Spark数据集:

当我们期待Spark 2.0时,我们计划对数据集进行一些激动人心的改进,特别是:...自定义编码器–虽然我们目前可以自动生成多种类型的编码器,但我们希望为自定义对象打开一个API。

并尝试在Dataset导致以下错误的情况下存储自定义类型:

找不到用于存储在数据集中的类型的编码器。导入sqlContext.implicits。支持基本类型(Int,String等)和产品类型(案例类)。_在将来的版本中将添加对序列化其他类型的支持。

要么:

Java.lang.UnsupportedOperationException:No Encoder found for xxx

是否有任何现有的解决方法?

 

java.lang.UnsupportedOperationException: No Encoder found for java.lang.Object
- field (class: "java.lang.Object", name: "context")
- root class: "dt.sql.alarm.core.AlarmRecord"
 
这个问题在stackoverflow中看到了一个不错的解决办法 就翻译过来了~
 
 

+200

这个答案仍然是有效的和翔实的,但事已至此更好的是,因为2.2 / 2.3,它加入了内置编码器的支持SetSeqMapDateTimestamp,和BigDecimal。如果您坚持只使用case类和普通的Scala类型来创建类型,那么只使用隐式in就可以了SQLImplicits

不幸的是,几乎没有任何东西可以帮助您。@since 2.0.0在in中搜索Encoders.scalaSQLImplicits.scala发现与原始类型(以及对case类的一些调整)有关的事情。因此,首先要说的是:当前没有对自定义类编码器的真正好的支持。鉴于此,考虑到我们目前掌握的所有技巧,接下来将做一些窍门,尽我们所能完成。作为预先的免责声明:这将无法完美运行,并且我会尽力将所有限制都明确并预先解决。

到底是什么问题

当您要创建数据集时,Spark“需要一个编码器(以将T类型的JVM对象与内部Spark SQL表示形式相互转换),该编码器通常是通过的隐式自动创建的SparkSession,或者可以通过调用静态方法来显式创建的在上Encoders(取自上的文档createDataset)。编码器的格式为Encoder[T]where T是您要编码的类型。第一个建议是添加import spark.implicits._(为您提供这些隐式编码器),第二个建议是使用组与编码器相关的功能显式传入隐式编码器。

没有适用于常规课程的编码器,因此

import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))

将给您以下隐式相关的编译时错误:

找不到用于存储在数据集中的类型的编码器。导入sqlContext.implicits。支持基本类型(Int,String等)和产品类型(案例类)。_在将来的版本中将添加对序列化其他类型的支持。

但是,如果将任何用于包装上述错误的类型包装在某个extends类中Product,则该错误会很容易地延迟到运行时,因此

import spark.implicits._
case class Wrap[T](unwrap: T)
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(Wrap(new MyObj(1)),Wrap(new MyObj(2)),Wrap(new MyObj(3))))

编译很好,但是在运行时失败

java.lang.UnsupportedOperationException:找不到MyObj的编码器

这是因为Spark使用隐式创建的编码器实际上仅在运行时(通过scala relfection)制成。在这种情况下,所有Spark在编译时的检查都是最外层的类扩展Product(所有大小写类都这样做),并且仅在运行时才意识到它仍然不知道该怎么做MyObj(如果我尝试执行此操作,将会出现相同的问题a Dataset[(Int,MyObj)]-Spark等待,直到运行时发出声音MyObj。这些是迫切需要解决的核心问题:

  • Product尽管扩展总是在运行时崩溃,但一些扩展了编译的类并
  • 无法传递用于嵌套类型的自定义编码器(我无法为Spark提供编码器,MyObj以使其知道如何编码Wrap[MyObj](Int,MyObj))。

只需使用 kryo

每个人都建议的解决方案是使用kryo编码器。

import spark.implicits._
class MyObj(val i: Int)
implicit val myObjEncoder = org.apache.spark.sql.Encoders.kryo[MyObj]
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))

但是,这变得非常乏味。尤其是如果您的代码正在处理各种数据集,联接,分组等。您最终将获得大量额外的隐式信息。那么,为什么不做一个隐式的自动完成呢?

import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) = 
  org.apache.spark.sql.Encoders.kryo[A](ct)

现在,我几乎可以做任何我想做的事(下面的示例在自动导入的spark-shell位置不起作用spark.implicits._

class MyObj(val i: Int)

val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).alias("d2") // mapping works fine and ..
val d3 = d1.map(d => (d.i,  d)).alias("d3") // .. deals with the new type
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1") // Boom!

或差不多。问题在于,使用kryoLead导致Spark仅将数据集中的每一行存储为平面二进制对象。对于mapfilterforeach那就足够了,但对于像操作join,星火真的需要这些被分隔成列。检查d2或的架构d3,您会看到只有一个二进制列:

d2.printSchema
// root
//  |-- value: binary (nullable = true)

元组的部分解决方案

因此,使用Scala中的隐式魔术(在6.26.3重载分辨率中有更多信息),我可以使自己成为一系列隐式,这些隐式将尽可能地发挥作用,至少对于元组而言,并且可以与现有隐式一起很好地工作:

import org.apache.spark.sql.{Encoder,Encoders}
import scala.reflect.ClassTag
import spark.implicits._  // we can still take advantage of all the old implicits

implicit def single[A](implicit c: ClassTag[A]): Encoder[A] = Encoders.kryo[A](c)

implicit def tuple2[A1, A2](
  implicit e1: Encoder[A1],
           e2: Encoder[A2]
): Encoder[(A1,A2)] = Encoders.tuple[A1,A2](e1, e2)

implicit def tuple3[A1, A2, A3](
  implicit e1: Encoder[A1],
           e2: Encoder[A2],
           e3: Encoder[A3]
): Encoder[(A1,A2,A3)] = Encoders.tuple[A1,A2,A3](e1, e2, e3)

// ... you can keep making these

然后,使用这些隐式函数,尽管可以重命名某些列,但我可以使上面的示例正常工作

class MyObj(val i: Int)

val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d2")
val d3 = d1.map(d => (d.i  ,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d3")
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1")

我还没有弄清楚如何在不重命名的情况下默认获取期望的元组名称(_1,,_2...)-如果其他人想使用它,就是名称"value"的引入位置,就是元组的位置通常会添加名称。但是,关键是我现在拥有一个不错的结构化架构:

d4.printSchema
// root
//  |-- _1: struct (nullable = false)
//  |    |-- _1: integer (nullable = true)
//  |    |-- _2: binary (nullable = true)
//  |-- _2: struct (nullable = false)
//  |    |-- _1: integer (nullable = true)
//  |    |-- _2: binary (nullable = true)

因此,总而言之,此解决方法:

  • 允许我们为元组获取单独的列(因此我们可以再次加入元组,是的!)
  • 我们可以再次依靠隐式函数(因此无需kryo遍历整个地方)
  • 几乎完全向后兼容import spark.implicits._(涉及一些重命名)
  • 没有让我们一起上kyro连载二列,更不用说对这些领域有可能
  • 将某些元组列重命名为“值”具有令人不快的副作用(如果需要,可以通过转换.toDF,指定新列名并转换回数据集来撤消该操作-模式名称似乎通过联接保留,最需要的地方)。

一般类的部分解决方案

这是令人不愉快的,并且没有好的解决方案。但是,既然我们有了上面的元组解决方案,我就预感到了另一个答案的隐式转换解决方案也不会那么痛苦,因为您可以将更复杂的类转换为元组。然后,在创建数据集之后,您可能会使用数据框方法来重命名列。如果一切顺利,这确实是一种进步,因为我现在可以在我的课程领域中执行联接。如果我只使用了一个平面二进制kryo序列化器,那将是不可能的。

这里是做了一切位的例子:我有一个类MyObj,其具有的类型的字段Intjava.util.UUID以及Set[String]。首先照顾自己。第二,尽管我可以序列化使用,kryo如果将其存储为a则将更加有用String(因为UUIDs通常是我想要加入的对象)。第三个实际上只是属于一个二进制列。

class MyObj(val i: Int, val u: java.util.UUID, val s: Set[String])

// alias for the type to convert to and from
type MyObjEncoded = (Int, String, Set[String])

// implicit conversions
implicit def toEncoded(o: MyObj): MyObjEncoded = (o.i, o.u.toString, o.s)
implicit def fromEncoded(e: MyObjEncoded): MyObj =
  new MyObj(e._1, java.util.UUID.fromString(e._2), e._3)

现在,我可以使用以下机制创建具有良好架构的数据集:

val d = spark.createDataset(Seq[MyObjEncoded](
  new MyObj(1, java.util.UUID.randomUUID, Set("foo")),
  new MyObj(2, java.util.UUID.randomUUID, Set("bar"))
)).toDF("i","u","s").as[MyObjEncoded]

该模式向我显示了具有正确名称的I列,以及我可以加入的前两个内容。

d.printSchema
// root
//  |-- i: integer (nullable = false)
//  |-- u: string (nullable = true)
//  |-- s: binary (nullable = true)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值