更新资料
这个答案仍然是有效的和翔实的,但事已至此更好的是,因为2.2 / 2.3,它加入了内置编码器的支持Set
,Seq
,Map
,Date
,Timestamp
,和BigDecimal
。如果您坚持只使用case类和常用的Scala类型来创建类型,那么只使用隐式in就可以了SQLImplicits
。
不幸的是,几乎没有任何东西可以帮助您。@since 2.0.0
在in中搜索Encoders.scala
或SQLImplicits.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!
或差不多。问题在于,使用kryo
Lead导致Spark仅将数据集中的每一行存储为平面二进制对象。对于map
,filter
,foreach
那就足够了,但对于像操作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
,其具有的类型的字段Int
,java.util.UUID
以及Set[String]
。首先照顾自己。第二,尽管我可以序列化使用,kryo
如果将其存储为a则将更加有用String
(因为UUID
s通常是我想加入的对象)。第三个实际上只是属于一个二进制列。
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)
ExpressionEncoder
使用JSON序列化创建自定义类?在我的情况,我不能逃脱元组,并KRYO给了我一个二进制列...