在 Spark 中有 DataFrame 这样的关系型编程接口,因其强大且灵活的表达能力,能够让 用户通过非常丰富的接口对数据进行处理,有效降低了用户的使用成本。Flink 也提供了关 系型编程接口 Table API 以及基于 Table API 的 SQL API,让用户能够通过使用结构化编程 接口高效地构建 Flink 应用。同时 Table API 以及 SQL 能够统一处理批量和实时计算业务, 无须切换修改任何应用代码就能够基于同一套 API 编写流式应用和批量应用,从而达到真正 意义的批流统一。
1.在进行相关开发前,我们首先导入相关的依赖。
org.apache.flink
flink-table-planner_2.11
1.9.1
org.apache.flink
flink-table-api-scala-bridge_2.11
1.9.1
2.创建 TableEnvironment 对象
//使用Flink原生的代码创建TableEnvironment
val fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val fsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings)
在 Flink 中创建一张表有两种方法:
Table API 中已经提供了 TableSource 从外部系统获取数据,例如常见的数据库、文件 系统和 Kafka 消息队列等外部系统。
从文件中创建Table(静态表)
Flink 允许用户从本地或者分布式文件系统中读取和写入数据,在 Table API 中可以通 过 CsvTableSource 类来创建,只需指定相应的参数即可。但是文件格式必须是 CSV 格式的。 其 他 文 件 格 式 也 支 持 ( 在 Flink 还 有 Connector 的 来 支 持 其 他 格 式 或 者 自 定 义 TableSource)。
demo如下:
package com.flink.primary.TableAndSQL
import org.apache.flink.api.scala.typeutils.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.scala.StreamTableEnvironment
import org.apache.flink.table.sources.CsvTableSource
/**
* 从文件中创建 Table(静态表)
* @author xjh 2020.5.30
*/
object CreateTableEnvironmentByFile {
def main(args: Array[String]): Unit = {
//使用Flink原生的代码创建TableEnvironment
val fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val fsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings)
//读取数据
val source = new CsvTableSource("/station.log",
Array[String]("f1", "f2", "f3", "f4", "f5", "f6"),
Array(Types.STRING, Types.STRING, Types.STRING, Types.STRING, Types.LONG, Types.LONG)
)
//注册数据表
fsTableEnv.registerTableSource("t_station_log",source)
//打印表结构,或者使用Table API,需要得到Table对象
val table = fsTableEnv.scan("t_station_log")
table.printSchema() //打印表结构
}
}
值得注意的是,改代码中并没有流计算逻辑,所以没有写fsEnv.execute()。
从DataStream中创建Table(动态表)
前面已经知道 Table API 是构建在 DataStream API 和 DataSet API 之上的一层更高级 的抽象,因此用户可以灵活地使用 Table API 将 Table 转换成 DataStream 或 DataSet 数据集,也可以将 DataSteam 或 DataSet 数据集转换成 Table,这和 Spark 中的 DataFrame 和 RDD 的关系类似。
demo如下:
package com.flink.primary.TableAndSQL
import com.flink.primary.DataSource.StationLog
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.scala.StreamTableEnvironment
/**
* 从文件中创建 Table(静态表)
* @author xjh 2020.5.30
*/
object CreateTableEnvironmentByDataStream {
def main(args: Array[String]): Unit = {
//使用Flink原生的代码创建TableEnvironment
val fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val fsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings)
import org.apache.flink.api.scala._
//读取数据
val stream = fsEnv.socketTextStream("m1", 8888)
.map(line => {
val arr = line.split(",")
new StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
})
//注册数据表
fsTableEnv.registerDataStream("t_table2",stream)
//打印表结构,或者使用Table API,需要得到Table对象
val table = fsTableEnv.scan("t_table2")
table.printSchema() //打印表结构
fsEnv.execute()
}
}
这里还可以使用fromDataStream来代替registerDataStream:
//把DataStream对象变成一个Table
val table: Table = fsTableEvn.fromDataStream(stream) //直接变成table对象 table.printSchema() //打印表结构 streamEnv.execute()
Flink 支持把自定义 POJOs 类的所有 case 类的属性名字变成字段名,也可以通过基于 字段偏移位置和字段名称两种方式重新修改:
//导入table库中的隐式转换
import org.apache.flink.table.api.scala._
// 基于位置重新指定字段名称为"field1", "field2", "field3"
val table = tStreamEnv.fromDataStream(stream, 'field1, 'field2, 'field3)
// 将DataStream转换成Table,并且将字段名称重新成别名
val table: Table = tStreamEnv.fromDataStream(stream, 'rowtime as 'newTime, 'id as 'newId,'variable as 'newVariable)
注意:要导入隐式转换。如果使用as修改字段,需要修改表中所有字段。
在 Table 对象上使用 select 操作符查询需要获取的指定字段,也可以使用 filter 或 where 方法过滤字段和检索条件,将需要的数据检索出来。
object TableAPITest {
def main(args: Array[String]): Unit = {
val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
streamEnv.setParallelism(1)
//初始化Table API的上下文环境
val tableEvn =StreamTableEnvironment.create(streamEnv)
//导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.scala._
val data = streamEnv.socketTextStream("hadoop101",8888)
.map(line=>{
var arr =line.split(",")
new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long)
})
val table: Table = tableEvn.fromDataStream(data)
//查询
tableEvn.toAppendStream[Row]( //toAppendStream 是把Table对象转换成为DataStream对象
table.select('sid,'callType as 'type,'callTime,'callOut))
.print()
//过滤查询
tableEvn.toAppendStream[Row](
table.filter('callType==="success") //filter
.where('callType==="success")) //where
.print()
tableEvn.execute("sql")
}
例如:
val table: Table = tableEvn.fromDataStream(data)
tableEvn.toRetractStream[Row](
table.groupBy('sid).select('sid, 'sid.count as 'logCount))
.filter(_._1==true) //返回的如果是true才是Insert的数据
.print()
在代码中可以看出,使用 toAppendStream 和 toRetractStream 方法将 Table 转换为 DataStream[T]数据集,T 可以是 Flink 自定义的数据格式类型 Row,也可以是用户指定的数 据 格 式 类 型 。 在 使 用 toRetractStream 方 法 时 , 返 回 的 数 据 类 型 结 果 为 DataStream[(Boolean,T)],Boolean 类型代表数据更新类型,True 对应 INSERT 操作更新的 数据,False 对应 DELETE 操作更新的数据。
用户可以在 Table API 中自定义函数类,常见的抽象类和接口是:
demo如下:
package com.flink.primary.TableAndSQL
import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.{EnvironmentSettings, Types}
import org.apache.flink.table.api.scala.StreamTableEnvironment
import org.apache.flink.table.functions.TableFunction
import org.apache.flink.types.Row
/**
* 使用table API实现word count
* @author xjh 2020.5.31
*/
object UDFByWordCount {
def main(args: Array[String]): Unit = {
//使用Flink原生的代码创建TableEnvironment
val fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val fsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings)
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.scala._
//读取数据源
val stream = fsEnv.socketTextStream("m1", 8888)
val table = fsTableEnv.fromDataStream(stream,'line)
//使用Table API切割单词,洗定义一个切割单词的函数
val my_func = new MyFlatMapFunction
val result=table.flatMap(my_func('line)).as('word,'word_c)
.groupBy('word)
.select('word,'word_c.sum as 'c)
fsTableEnv.toRetractStream[Row](result).filter(_._1==true).print()
fsTableEnv.execute("table_api")
}
/**
* 自定义UDF
*/
class MyFlatMapFunction extends TableFunction[Row]{
//定义函数处理之后的返回类型,输出单词和1
override def getResultType: TypeInformation[Row] =Types.ROW(Types.STRING(),Types.INT())
//函数主体
def eval(str:String):Unit={
str.trim.split(" ").foreach(word=>{
var row=new Row(2)
row.setField(0,word)
row.setField(1,1)
collect(row)
})
}
}
}
Flink 支持 ProcessTime、EventTime 和 IngestionTime 三种时间概念,针对每种时间 概念,Flink Table API 中使用 Schema 中单独的字段来表示时间属性,当时间字段被指定 后,就可以在基于时间的操作算子中使用相应的时间属性。
在 Table API 中通过使用.rowtime 来定义 EventTime 字段,在 ProcessTime 时间字段 名后使用.proctime 后缀来指定 ProcessTime 时间属性
demo:统计最近 5 秒钟,每个基站的呼叫数量
object TableAPITest {
def main(args: Array[String]): Unit = {
val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//指定EventTime为时间语义
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
streamEnv.setParallelism(1)
//初始化Table API的上下文环境
val tableEvn =StreamTableEnvironment.create(streamEnv)
//导入隐式转换
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.scala._
val data = streamEnv.socketTextStream("hadoop101",8888)
.map(line=>{
var arr =line.split(",")
new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long)
}) .assignTimestampsAndWatermarks(
//引入Watermark
new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(2)){//延迟2 秒
override def extractTimestamp(element: StationLog) = { element.callTime }
})
//设置时间属性
val table: Table = tableEvn.fromDataStream(data,'sid,'callOut,'callIn,'callType,'callTime.rowtime)
//滚动Window ,第一种写法
val result: Table = table.window(Tumble over 5.second on 'callTime as 'window)
//第二种写法
val result: Table = table.window(Tumble.over("5.second").on("callTime").as("window"))
.groupBy('window, 'sid)
.select('sid, 'window.start, 'window.end, 'window.rowtime, 'sid.count)
//打印结果
tableEvn.toRetractStream[Row](result) .filter(_._1==true) .print()
tableEvn.execute("sql")
}
}