Cập nhật
Câu trả lời này vẫn còn hiệu lực và thông tin, mặc dù những điều bây giờ tốt hơn là từ 2.2 / 2.3, trong đó cho biết thêm built-in hỗ trợ mã hóa cho Set
, Seq
, Map
, Date
, Timestamp
, và BigDecimal
. Nếu bạn dính vào việc tạo các kiểu chỉ với các lớp trường hợp và các kiểu Scala thông thường, bạn sẽ ổn với chỉ ẩn SQLImplicits
.
Thật không may, hầu như không có gì được thêm vào để giúp với điều này. Tìm kiếm @since 2.0.0
trong Encoders.scala
hoặc SQLImplicits.scala
tìm thấy những thứ chủ yếu để làm với các kiểu nguyên thủy (và một số điều chỉnh các lớp trường hợp). Vì vậy, điều đầu tiên cần nói: hiện tại không có hỗ trợ tốt thực sự cho các bộ mã hóa lớp tùy chỉnh . Theo cách đó, một số thủ thuật sau đây là một công việc tốt như chúng ta có thể hy vọng, đưa ra những gì chúng ta hiện đang có. Như một tuyên bố từ chối trách nhiệm trước: điều này sẽ không hoạt động hoàn hảo và tôi sẽ cố gắng hết sức để làm cho mọi giới hạn rõ ràng và thẳng thắn.
Chính xác thì vấn đề là gì
Khi bạn muốn tạo tập dữ liệu, Spark "yêu cầu bộ mã hóa (để chuyển đổi một đối tượng JVM loại T sang và từ biểu diễn Spark SQL bên trong) thường được tạo tự động thông qua ẩn SparkSession
hoặc có thể được tạo một cách rõ ràng bằng cách gọi các phương thức tĩnh trên Encoders
"(lấy từ các tài liệu trêncreateDataset
). Một bộ mã hóa sẽ mang hình thức Encoder[T]
mà T
là loại bạn được mã hóa. Các gợi ý đầu tiên là thêm import spark.implicits._
(mang đến cho bạn những bộ mã hóa tuyệt đối) và gợi ý thứ hai là phải vượt qua một cách rõ ràng trong bộ mã hóa ngầm sử dụng này tập hợp các hàm mã hóa liên quan.
Không có bộ mã hóa có sẵn cho các lớp thông thường, vì vậy
import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
sẽ cung cấp cho bạn lỗi thời gian biên dịch ngầm liên quan sau đây:
Không thể tìm thấy bộ mã hóa cho loại được lưu trữ trong Bộ dữ liệu. Các kiểu nguyên thủy (Int, String, v.v.) và các loại Sản phẩm (các lớp trường hợp) được hỗ trợ bằng cách nhập sqlContext.implicits._ Hỗ trợ để tuần tự hóa các loại khác sẽ được thêm vào trong các bản phát hành trong tương lai
Tuy nhiên, nếu bạn bọc bất kỳ loại nào bạn vừa sử dụng để nhận lỗi ở trên trong một số lớp kéo dài Product
, thì lỗi sẽ bị chậm trễ trong thời gian chạy, vì vậy
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))))
Biên dịch tốt, nhưng thất bại trong thời gian chạy với
java.lang.UnsupportedOperationException: Không tìm thấy Bộ mã hóa cho MyObj
Lý do cho điều này là các bộ mã hóa mà Spark tạo ra với các ẩn ý thực sự chỉ được thực hiện khi chạy (thông qua việc chuyển đổi scala). Trong trường hợp này, tất cả các kiểm tra Spark tại thời gian biên dịch là lớp ngoài cùng mở rộng Product
(mà tất cả các lớp trường hợp làm) và chỉ nhận ra khi chạy mà nó vẫn không biết phải làm gì MyObj
(vấn đề tương tự xảy ra nếu tôi cố gắng thực hiện a Dataset[(Int,MyObj)]
- Spark chờ cho đến khi thời gian chạy để bật barf MyObj
). Đây là những vấn đề trung tâm đang rất cần được khắc phục:
- một số lớp mở rộng
Product
biên dịch mặc dù luôn bị lỗi khi chạy và
- không có cách nào chuyển qua các bộ mã hóa tùy chỉnh cho các loại lồng nhau (Tôi không có cách nào cho Spark một bộ mã hóa chỉ
MyObj
để nó biết cách mã hóa Wrap[MyObj]
hoặc (Int,MyObj)
).
Chỉ dùng kryo
Giải pháp mà mọi người gợi ý là sử dụng kryo
bộ mã hóa.
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)))
Điều này khá nhanh chóng mặc dù. Đặc biệt là nếu mã của bạn đang thao túng tất cả các loại bộ dữ liệu, tham gia, nhóm, v.v ... Bạn sẽ kết thúc với một loạt các ẩn ý bổ sung. Vì vậy, tại sao không làm cho một ẩn mà thực hiện tất cả điều này tự động?
import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) =
org.apache.spark.sql.Encoders.kryo[A](ct)
Và bây giờ, có vẻ như tôi có thể làm hầu hết mọi thứ tôi muốn (ví dụ dưới đây sẽ không hoạt động ở spark-shell
nơi spark.implicits._
được nhập tự động)
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!
Hoặc gần như. Vấn đề là việc sử dụng kryo
khách hàng tiềm năng để Spark chỉ lưu trữ mọi hàng trong tập dữ liệu dưới dạng đối tượng nhị phân phẳng. Đối với map
, filter
, foreach
đó là đủ, nhưng đối với các hoạt động như join
, Spark thực sự cần những để được tách ra thành các cột. Kiểm tra lược đồ cho d2
hoặc d3
, bạn thấy chỉ có một cột nhị phân:
d2.printSchema
// root
// |-- value: binary (nullable = true)
Giải pháp một phần cho bộ dữ liệu
Vì vậy, bằng cách sử dụng phép thuật ẩn ý trong Scala (nhiều hơn trong 6.26.3 Độ phân giải quá tải ), tôi có thể tạo cho mình một loạt các hàm ý sẽ làm tốt nhất có thể, ít nhất là cho các bộ dữ liệu và sẽ hoạt động tốt với các ẩn ý hiện có:
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
Sau đó, được trang bị những ẩn ý này, tôi có thể làm cho ví dụ của mình ở trên hoạt động, mặc dù với một số đổi tên cột
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")
Tôi vẫn chưa tìm ra làm thế nào để có được các tên tuple dự kiến ( _1
,, _2
...) mà không đổi tên chúng - nếu ai đó muốn chơi xung quanh nó, đây là nơi tên "value"
được giới thiệu và đây là nơi đặt tên tên thường được thêm vào. Tuy nhiên, điểm quan trọng là bây giờ tôi có một lược đồ có cấu trúc đẹp:
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)
Vì vậy, tóm lại, cách giải quyết này:
- cho phép chúng tôi nhận các cột riêng biệt cho các bộ dữ liệu (vì vậy chúng tôi có thể tham gia trên các bộ dữ liệu một lần nữa, yay!)
- một lần nữa chúng ta có thể chỉ dựa vào những ẩn ý (vì vậy không cần phải đi
kryo
khắp nơi)
- gần như hoàn toàn tương thích ngược với
import spark.implicits._
(với một số đổi tên liên quan)
- không không chúng ta hãy tham gia vào các
kyro
cột nhị phân tuần tự, hãy để một mình trên cánh đồng những người có thể có
- có tác dụng phụ khó chịu khi đổi tên một số cột tuple thành "value" (nếu cần, điều này có thể được hoàn tác bằng cách chuyển đổi
.toDF
, chỉ định tên cột mới và chuyển đổi lại thành tập dữ liệu - và tên lược đồ dường như được giữ nguyên thông qua các phép nối , nơi họ cần nhất).
Giải pháp một phần cho các lớp học nói chung
Điều này là ít dễ chịu và không có giải pháp tốt. Tuy nhiên, bây giờ chúng ta có giải pháp tuple ở trên, tôi có linh cảm giải pháp chuyển đổi ngầm từ một câu trả lời khác sẽ bớt đau hơn một chút vì bạn có thể chuyển đổi các lớp phức tạp hơn của mình thành tuple. Sau đó, sau khi tạo tập dữ liệu, có thể bạn sẽ đổi tên các cột bằng cách sử dụng phương pháp khung dữ liệu. Nếu mọi việc suôn sẻ, đây thực sự là một sự cải tiến vì bây giờ tôi có thể thực hiện các phép nối trên các lĩnh vực của lớp mình. Nếu tôi chỉ sử dụng một kryo
serializer nhị phân phẳng thì điều đó sẽ không thể xảy ra.
Dưới đây là một ví dụ mà không một chút tất cả mọi thứ: Tôi có một lớp học MyObj
trong đó có lĩnh vực loại Int
, java.util.UUID
và Set[String]
. Việc đầu tiên tự chăm sóc bản thân. Thứ hai, mặc dù tôi có thể tuần tự hóa bằng cách sử dụng kryo
sẽ hữu ích hơn nếu được lưu trữ dưới dạng String
(vì UUID
thường là thứ tôi sẽ muốn tham gia). Thứ ba thực sự chỉ thuộc về một cột nhị phân.
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)
Bây giờ, tôi có thể tạo một tập dữ liệu với một lược đồ đẹp bằng cách sử dụng máy móc này:
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]
Và lược đồ hiển thị cho tôi các cột tôi với tên đúng và với cả hai điều đầu tiên tôi có thể tham gia.
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
ExpressionEncoder
bằng cách sử dụng tuần tự hóa JSON không? Trong trường hợp của tôi, tôi không thể thoát khỏi các bộ dữ liệu và kryo cho tôi một cột nhị phân ..