文章目录
- Build-In数据源
- third-party packages
- 自定义数据源的构建
- 常见的trait
- 通过JDBCRelation的源码了解外部数据源的执行
- 自己实现一个外部数据源(核心重要)
Build-In数据源
前面学习的数据源都是Build-In类型的,Spark内置了Parquet、ORC、JSON、Hive Tables、JDBC To Other DataBases 、Avro等外部数据源读取和保存操作。Spark默认的是parquet格式的文件。这些前面已经讲过,这里不再述说。
小知识点:
举个例子:电商行业有些数据源,基本配置存放在RDBMS关系型数据库里,用户行为日志存放在Hive表或者Spark SQL表里。
用前面的方法,每次需要设置连接的属性,连接到相关数据库,加载数据:
val dbsjdbcDF = spark.read
.format("jdbc")
.option("url","jdbc:mysql://hadoop001:3306/hive")
.option("dbtable","dbs")
.option("user","root")
.option("password","111")
.load()
那么,Spark启动时,有没有办法直接把我们所想要的东西全部注册进来呢?
(可以去实现一下)
如果可以的话,那就可以直接使用了。
通过WebUI把很多东西都配置好,在Spark启动的时候,去读取配置信息,然后注册到Spark中,后面使用的时候就可以直接进行操作了。
third-party packages
A community index of third-party packages for Apache Spark : https://spark-packages.org/
这个是Spark整合第三方的parckages,比如外部数据源 集中在这个网站上。可以使用这里面的。但是不一定好。很多时候也需要自己去自定义外部数据源。
上面内置的或者第三方的数据源,都是可以直接拿来使用,下面来学习一下,自定义数据源如何实现,如何构建自己定义的数据源。
自定义数据源的构建
(面试加分项)
常见的trait
源码:
下面是interfaces.scala中常见的一些接口:
下面各种类、方法,在源码里面都有详细的注释。
//Spark提供的一个标准的接口
//如果要实现自己的外部数据源,必须要实现它里面的一些方法
//这个里面是含有schema的元组集合(字段:字段类型)
//继承了BaseRelation的类,必须以StructType这个形式产生数据的schema
//继承了`Scan`类之后,要实现它里面的相应的方法
@InterfaceStability.Stable
abstract class BaseRelation {
def sqlContext: SQLContext
def schema: StructType
.....
}
//A BaseRelation that can produce all of its tuples as an RDD of Row objects.
//读取数据,构建RDD[ROW]
//可以理解为select * from xxx 把所有数据读取出来变成RDD[Row]
trait TableScan {
def buildScan(): RDD[Row]
}
//A BaseRelation that can eliminate unneeded columns before producing an RDD
//containing all of its tuples as Row objects.
//可以理解为select a,b from xxx 读取需要的列变成RDD[Row]
trait PrunedScan {
def buildScan(requiredColumns: Array[String]): RDD[Row]
}
//可以理解为select a,b from xxx where a>10 读取需要的列,再进行过滤,变成RDD[Row]
trait PrunedFilteredScan {
def buildScan(requiredColumns: Array[String], filters: Array[Filter]): RDD[Row]
}
//写数据,插入数据,无返回
trait InsertableRelation {
def insert(data: DataFrame, overwrite: Boolean): Unit
}
trait CatalystScan {
def buildScan(requiredColumns: Seq[Attribute], filters: Seq[Expression]): RDD[Row]
}
//用来创建上面的BaseRelation
//传进来指定数据源的参数:比如url、dbtable、user、password等(这个就是你要连接的那个数据源)
//最后返回BaseRelation(已经带有了传进来参数的属性了)
trait RelationProvider {
def createRelation(sqlContext: SQLContext, parameters: Map[String, String]): BaseRelation
}
// Saves a DataFrame to a destination (using data source-specific parameters)
//mode: SaveMode,当目标已经存在,是用什么方式保存
//parameters: Map[String, String] :指定的数据源参数
//要保存的DataFrame,比如执行查询之后的rows
//返回BaseRelation
trait CreatableRelationProvider {
def createRelation(
sqlContext: SQLContext,
mode: SaveMode,
parameters: Map[String, String],
data: DataFrame): BaseRelation
}
//把你的数据源起一个简短的别名
trait DataSourceRegister {
//override def shortName(): String = "parquet"(举例)
def shortName(): String
}
//比CreatableRelationProvider多了个schema参数
trait SchemaRelationProvider {
def createRelation(
sqlContext: SQLContext,
parameters: Map[String, String],
schema: StructType): BaseRelation
}
通过JDBCRelation的源码了解外部数据源的执行
(这一节不太懂,后面需要再研究一下)
打开IDEA,搜索一下JdbcRelationProvider。
(需要好好研究一下里面的源码,可以打个debug跑一下)
可以看到jdbc这个数据包下面有很多类
点击JdbcRelationProvider ,可以看到它是如何实现的
class JdbcRelationProvider extends CreatableRelationProvider
with RelationProvider with DataSourceRegister {
override def shortName(): String = "jdbc"
override def createRelation(
sqlContext: SQLContext,
parameters: Map[String, String]): BaseRelation = {
//这个option就是去连接JDBC的那些信息,比如url、dbtable、user等等
//具体看JDBCOptions的源码
val jdbcOptions = new JDBCOptions(parameters)
val resolver = sqlContext.conf.resolver
val timeZoneId = sqlContext.conf.sessionLocalTimeZone
//这个schema如何拿到的???
//通过JDBC metastore获取得到的
//具体可以getSchema的源码
val schema = JDBCRelation.getSchema(resolver, jdbcOptions)
val parts = JDBCRelation.columnPartition(schema, resolver, timeZoneId, jdbcOptions)
//创建JDBCRelation,JDBCRelation这个是把上面说的那些scan的东西给实现出来
JDBCRelation(schema, parts, jdbcOptions)(sqlContext.sparkSession)
}
override def createRelation(
.......
下面是JDBCRelation.scala
//可以看一下它里面实现的方法,底层就是拼sql
private[sql] case class JDBCRelation(
override val schema: StructType,
parts: Array[Partition],
jdbcOptions: JDBCOptions)(@transient val sparkSession: SparkSession) //可以点击scanTable具体分析一下,都是拼SQL
extends BaseRelation
with PrunedFilteredScan
with InsertableRelation {
....................
//实现Scan
override def buildScan(requiredColumns: Array[String], filters: Array[Filter]): RDD[Row] = {
// Rely on a type erasure hack to pass RDD[InternalRow] back as RDD[Row]
JDBCRDD.scanTable(
sparkSession.sparkContext,
schema,
requiredColumns,
filters,
parts,
jdbcOptions).asInstanceOf[RDD[Row]]
}
//实现写数据
override def insert(data: DataFrame, overwrite: Boolean): Unit = {
data.write
.mode(if (overwrite) SaveMode.Overwrite else SaveMode.Append)
.jdbc(jdbcOptions.url, jdbcOptions.tableOrQuery, jdbcOptions.asProperties)
}
..........
}
总结:Spark去处理JDBC数据源就是:拼sql,然后交给JDBC API编程,然后产生DataFrame。
上面是JDBC数据源的如何实现的,还有其它数据源比如json、parquet、text等等。
下面自己来实现一下外部数据源。
自己实现一个外部数据源(核心重要)
现在有个文本:
//编号、名字、性别、工资、年终奖
101,zhansan,0,10000,200000
102,lisi,0,150000,250000
103,wangwu,1,3000,5
104,zhaoliu,2,500,6
这个文本是没有schema的,之前有两种方式把它转换成DataFrame。一种是通过case class反射的方式,另一种是通过创建带有Rows的RDD,自定义一个schema,然后再用通过createDataFrame来创建DataFrame。
现在通外部数据源把它来实现。
上面的JDBCRelation是通过JdbcRelationProvider来实现的。
定义一个DefaultSource,继承CreatableRelationProvider,参考上面JDBC的JdbcRelationProvider。
定义一个TextDataSourceRelation,继承BaseRelation和TableScan,并实现TableScan,参考上面JDBC的JdbcRelation。
下面是完整代码:
package com.ruozedata.spark.com.ruozedata.spark.sql.text
import org.apache.spark.sql.SparkSession
object TextApp {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("TextApp")
.master("local[2]")
.getOrCreate()
//只要写到包名就可以了...sql.text,不用这样写...sql.text.DefaultSource
val df = spark.read.format("com.ruozedata.spark.com.ruozedata.spark.sql.text")
.option("path","E://data.txt").load()
df.show()
spark.stop()
}
}
package com.ruozedata.spark.com.ruozedata.spark.sql.text
import org.apache.spark.sql.types.{DataType, LongType, StringType}
object Utils {
def castTo(value:String,dataType:DataType) ={
dataType match {
case _:LongType =>value.toLong
case _:StringType => value
}
}
}
package com.ruozedata.spark.com.ruozedata.spark.sql.text
import org.apache.spark.sql.{DataFrame, SQLContext, SaveMode}
import org.apache.spark.sql.sources.{BaseRelation, CreatableRelationProvider, SchemaRelationProvider}
import org.apache.spark.sql.types.StructType
//DefaultSource这个名字不能乱写
class DefaultSource extends CreatableRelationProvider with SchemaRelationProvider{
def createRelation(
sqlContext: SQLContext,
parameters: Map[String, String],
schema: StructType): BaseRelation ={
val path = parameters.get("path")
path match {
case Some(x) => new TextDataSourceRelation(sqlContext,x,schema)
case _ => throw new IllegalArgumentException("path is required...")
}
}
override def createRelation(sqlContext: SQLContext, mode: SaveMode, parameters: Map[String, String], data: DataFrame): BaseRelation = {
createRelation(sqlContext,parameters,null)
}
}
package com.ruozedata.spark.com.ruozedata.spark.sql.text
import org.apache.spark.internal.Logging
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.sources.{BaseRelation, TableScan}
import org.apache.spark.sql.types.{LongType, StringType, StructField, StructType}
class TextDataSourceRelation(override val sqlContext: SQLContext,path:String,userSchema: StructType) extends BaseRelation with TableScan with Logging{
//如果传进来的schema不为空,就用传进来的schema,否则就用自定义的schema
override def schema: StructType = {
if(userSchema != null){
userSchema
}else{
StructType(
StructField("id",LongType,false) ::
StructField("name",StringType,false) ::
StructField("gender",StringType,false) ::
StructField("salary",LongType,false) ::
StructField("comm",LongType,false) :: Nil
)
}
}
//把数据读进来,读进来之后把它转换成 RDD[Row]
override def buildScan(): RDD[Row] = {
logWarning("this is ruozedata buildScan....")
//读取数据,变成为RDD
//wholeTextFiles会把文件名读进来,可以通过map(_._2)把文件名去掉,第一位是文件名,第二位是内容
val rdd = sqlContext.sparkContext.wholeTextFiles(path).map(_._2)
//拿到schema
val schemaField = schema.fields
//rdd.collect().foreach(println)
//rdd + schemaField 把rdd和schemaField解析出来拼起来
val rows = rdd.map(fileContent => {
//拿到每一行的数据
val lines = fileContent.split("\n")
//每一行数据按照逗号分隔,分隔之后去空格,然后转成一个seq集合
val data = lines.map(_.split(",").map(_.trim)).toSeq
//zipWithIndex
val result = data.map(x => x.zipWithIndex.map {
case (value, index) => {
val columnName = schemaField(index).name
//castTo里面有两个参数,第一个参数需要给个判断,如果是字段是性别,里面再进行判断再转换一下,如果不是性别就直接用这个字段
Utils.castTo(if(columnName.equalsIgnoreCase("gender")){
if(value == "0"){
"man"
}else if(value == "1"){
"woman"
} else{
"unknown"
}
}else{
value
},schemaField(index).dataType)
}
})
result.map(x => Row.fromSeq(x))
})
rows.flatMap(x => x)
}
}
上面只完成了一个select * from xxx的功能。其它功能暂时没有实现:
with PrunedScan 的功能
with PrunedFilteredScan 的功能
with InsertableRelation 的功能