Flink本身是批流统一的处理框架,所以Table API和SQL,就是批流统一的上层处理API。
目前功能尚未完善,处于活跃的开发阶段。
Table API是一套内嵌在Java和Scala语言中的查询API,它允许我们以非常直观的方式,组合来自一些关系运算符的查询(比如select、filter和join)。而对于Flink SQL,就是直接可以在代码中写SQL,来实现一些查询(Query)操作。Flink的SQL支持,基于实现了SQL标准的Apache Calcite(Apache开源SQL解析工具)。
无论输入是批输入还是流式输入,在这两套API中,指定的查询都具有相同的语义,得到相同的结果。
Table API和SQL需要引入的依赖有两个:planner和bridge。
// flink自己版本的依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner_2.11artifactId>
<version>1.10.0version>
dependency>
// blink版本的依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner-blink_2.11artifactId>
<version>1.10.0version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-api-scala-bridge_2.11artifactId>
<version>1.10.0version>
dependency>
flink-table-planner:
planner计划器,是table API最主要的部分,提供了运行时环境和生成程序执行计划的planner;
flink-table-api-scala-bridge:
bridge桥接器,主要负责table API和 DataStream/DataSet API的连接支持,按照语言分java和scala。
这里的两个依赖,是IDE环境下运行需要添加的;如果是生产环境,lib目录下默认已经有了planner,就只需要有bridge就可以了。
当然,如果想使用用户自定义函数,或是跟kafka做连接,需要有一个SQL client,这个包含在flink-table-common里。
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.Table
import org.apache.flink.table.api.scala._
object TableAPITest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("hadoop", 7777)
val dataStream = inputStream.map {
data =>
val splitData = data.split(",")
SensorReading10(splitData(0), splitData(1).toLong, splitData(2).toDouble)
}
// 创建表执行环境
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env)
// 基于数据流,转换成一张表,然后进行计算
val dataTable: Table = tableEnv.fromDataStream(dataStream)
// 1.直接写sql得到转换结果
val resultSqlTable: Table = tableEnv
.sqlQuery("select id, temperature from " + dataTable + " where id = 'sensor_1'")
// 装换成数据流,打印输出
val sqlResultStream: DataStream[(String, Double)] = resultSqlTable.toAppendStream[(String, Double)]
// 打印结果
sqlResultStream.print("sql Result")
// 2.调用TableAPI 得到转换结果
val resultTable: Table = dataTable
.select("id,temperature")
.filter("id == 'sensor_1'")
// 装换成数据流,打印输出
val resultStream: DataStream[(String, Double)] = resultTable.toAppendStream[(String, Double)]
resultStream.print("API Result")
// 打印表结构
resultTable.printSchema()
env.execute("xxxx")
}
}
case class SensorReading10(id: String, timestamp: Long, temperature: Double)
Table API 和 SQL 的程序结构,与流式处理的程序结构类似;也可以近似地认为有这么几步:首先创建执行环境,然后定义source、transform和sink。
具体操作流程如下:
val tableEnv = ... // 创建表的执行环境
// 创建一张表,用于读取数据
tableEnv.connect(...).createTemporaryTable("inputTable")
// 注册一张表,用于把计算结果输出
tableEnv.connect(...).createTemporaryTable("outputTable")
// 通过 Table API 查询算子,得到一张结果表
val result = tableEnv.from("inputTable").select(...)
// 通过 SQL查询语句,得到一张结果表
val sqlResult = tableEnv.sqlQuery("SELECT ... FROM inputTable ...")
// 将结果表写入输出表中
result.insertInto("outputTable")
表环境(TableEnvironment)是flink中集成Table API & SQL的核心概念。它负责:
注册catalog
在内部 catalog 中注册表
执行 SQL 查询
注册用户自定义函数
将 DataStream 或 DataSet 转换为表
保存对 ExecutionEnvironment 或 StreamExecutionEnvironment 的引用
在创建TableEnv的时候,可以多传入一个EnvironmentSettings或者TableConfig参数,可以用来配置 TableEnvironment的一些特性。
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.{EnvironmentSettings, TableEnvironment}
import org.apache.flink.table.api.scala.{BatchTableEnvironment, StreamTableEnvironment}
object TableAPILearning {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 1.创建表环境
// 1.1创建老版本的流查询环境
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
// 1.2 创建老版本的批处理查询环境
val batchEnv: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
val batchTableEnv: BatchTableEnvironment = BatchTableEnvironment.create(batchEnv)
// 1.3 创建Blink 版本的流查询环境
val bsSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()
.inStreamingMode()
.build()
val bsTableEnv = StreamTableEnvironment.create(env, bsSettings)
// 1.4 创建blink版本的批查询环境
val bbSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()
.inBatchMode()
.build()
val bbTableEnv = TableEnvironment.create(bbSettings)
}
}
TableEnvironment可以注册目录Catalog,并可以基于Catalog注册表。它会维护一个Catalog-Table表之间的map。
表(Table)是由一个“标识符”来指定的,由3部分组成:Catalog名、数据库(database)名和对象名(表名)。如果没有指定目录或数据库,就使用当前的默认值。
表可以是常规的(Table,表),或者虚拟的(View,视图)。常规表(Table)一般可以用来描述外部数据,比如文件、数据库表或消息队列的数据,也可以直接从 DataStream转换而来。视图可以从现有的表中创建,通常是table API或者SQL查询的一个结果。
连接外部系统在Catalog中注册表,直接调用tableEnv.connect()就可以,里面参数要传入一个ConnectorDescriptor,也就是connector描述器。对于文件系统的connector而言,flink内部已经提供了,就叫做FileSystem()。
代码如下:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{FileSystem, OldCsv, Schema}
object TableAPILearning {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 创建表环境
// 创建老版本的流查询环境
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
// 连接到文件系统(CSV)以逗号分隔的字符串
val filePath = "F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\word.txt"
tableEnv.connect(new FileSystem().path(filePath))
.withFormat(new OldCsv()) // 定义读取数据后的格式化方法
.withSchema(new Schema() // 定义表结构
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("inputTable") // 注册一张表
// 转换成流输出
val sensorTable: Table = tableEnv.from("inputTable")
sensorTable.toAppendStream[(String, Long, Double)].print()
env.execute("CCCC")
}
}
这是旧版本的csv格式描述器。由于它是非标的,跟外部系统对接并不通用,所以将被弃用,以后会被一个符合RFC-4180标准的新format描述器取代。新的描述器就叫Csv(),但flink没有直接提供,需要引入依赖flink-csv:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-csvartifactId>
<version>1.10.0version>
dependency>
代码非常类似,只需要把withFormat里的OldCsv改成Csv就可以了。
kafka的连接器flink-kafka-connector中,1.10版本的已经提供了Table API的支持。我们可以在 connect方法中直接传入一个叫做Kafka的类,这就是kafka连接器的描述器ConnectorDescriptor。
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{FileSystem, Kafka, OldCsv, Schema}
object TableAPILearning {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 创建表环境
// 创建老版本的流查询环境
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
// 连接到Kafka
tableEnv.connect(new Kafka()
.version("0.11") // 定义kafka版本
.topic("sensor")
.property("bootstrap.servers", "hadoop:9092")
.property("zookeeper.connect", "hadoop:2181")
)
.withFormat(new OldCsv)
.withSchema(new Schema() // 定义表结构
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("kafkaInputTable")
// 转换成流打印输出
val sensorTable2: Table = tableEnv.from("kafkaInputTabele")
sensorTable2.toAppendStream[(String, Long, Double)]
env.execute("CCCC")
}
}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{FileSystem, OldCsv, Schema}
object TableQueryTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
val filePath = "F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\word.txt"
// 1.连接到文件系统(CSV)以逗号分隔的字符串
// 定义表数来源、定义表结构
tableEnv.connect(new FileSystem().path(filePath))
.withFormat(new OldCsv()) // 定义读取数据后的格式化方法
.withSchema(new Schema() // 定义表结构
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("inputTable") // 注册一张表
// 2.获取表对象
val sourceTable: Table = tableEnv.from("inputTable")
// 3.从表对象中提取所需表数据
// 3.1 Table API运用
val resultTabeAPI: Table = sourceTable
.select("id, temperature")
.filter("id == 'sensor_1'") // 注意单引号不能少
// 3.2 SQL 注意是直接在tableEnv上运用
// 3.2.1 单行sql字符串
val resultSqlTable: Table = tableEnv.sqlQuery("select id, temperature from inputTable where id = 'sensor_1'")
// 3.2.2 多行sql字符串
val resultSqlTable2: Table = tableEnv.sqlQuery(
"""
|select
| id
| ,temperature
|from inputTable
|where id = 'sensor_1'
|""".stripMargin)
// 3.3 特殊方式
val resultTableAPI2 = sourceTable
.select('id, 'temperature) // 单引号后面跟字段名称
.filter('id === "sensor_1") // 三个等号 用于判断是非
//4.将表转换成流
val resultTabeAPIResult: DataStream[(String, Double)] = resultTabeAPI.toAppendStream[(String, Double)]
val resultSqlTableResult: DataStream[(String, Double)] = resultSqlTable.toAppendStream[(String, Double)]
val resultTableAPI2Result: DataStream[(String, Double)] = resultTableAPI2.toAppendStream[(String, Double)]
val resultSqlTable2Result: DataStream[(String, Double)] = resultSqlTable2.toAppendStream[(String, Double)]
// 5.打印数据
resultTabeAPIResult.print()
resultSqlTableResult.print()
resultTableAPI2Result.print()
env.execute("XXXX")
}
}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{FileSystem, OldCsv, Schema}
object TableAggregateTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
val filePath = "F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\word.txt"
// 1.连接到文件系统(CSV)以逗号分隔的字符串
// 定义表数来源、定义表结构
tableEnv.connect(new FileSystem().path(filePath))
.withFormat(new OldCsv()) // 定义读取数据后的格式化方法
.withSchema(new Schema() // 定义表结构
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("inputTable") // 注册一张表
// 2.获取表对象
val sourceTable: Table = tableEnv.from("inputTable")
// 3.聚合
// 3.1 简单聚合 统计每个传感器的温度个数
val aggResultTable = sourceTable
.groupBy('id)
.select('id, 'id.count as 'cnt)
// 3.2 sql 实现聚合
// tableEnv.sqlQuery(
// """
// |select ......
// |""".stripMargin)
// 4. 转换成流 注意由于是聚合 需要toRetractStream方法,遇到之前有过的数据会输出两条数据
val aggResultTableStream: DataStream[(Boolean, (String, Long))] = aggResultTable.toRetractStream[(String, Long)]
aggResultTableStream.print("result")
env.execute()
}
}
Flink允许我们把Table和DataStream做转换:我们可以基于一个DataStream,先流式地读取数据源,然后map成样例类,再把它转成Table。Table的列字段(column fields),就是样例类里的字段,这样就不用再麻烦地定义schema了。
代码中实现非常简单,直接用tableEnv.fromDataStream()就可以了。默认转换后的 Table schema 和 DataStream 中的字段定义一一对应,也可以单独指定出来。
这就允许我们更换字段的顺序、重命名,或者只选取某些字段出来,相当于做了一次map操作(或者Table API的 select操作)。
代码具体如下:
val inputStream: DataStream[String] = env.readTextFile("sensor.txt")
val dataStream: DataStream[SensorReading] = inputStream
.map(data => {
val dataArray = data.split(",")
SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
})
val sensorTable: Table = tableEnv.fromDataStream(dataStream)
val sensorTable2 = tableEnv.fromDataStream(dataStream, 'id, 'timestamp as 'ts)
DataStream 中的数据类型,与表的 Schema 之间的对应关系,是按照样例类中的字段名来对应的(name-based mapping),所以还可以用as做重命名。
另外一种对应方式是,直接按照字段的位置来对应(position-based mapping),对应的过程中,就可以直接指定新的字段名了。
基于名称的对应:
val sensorTable = tableEnv.fromDataStream(dataStream, 'timestamp as 'ts, 'id as 'myId, 'temperature)
基于位置的对应:
val sensorTable = tableEnv.fromDataStream(dataStream, 'myId, 'ts)
Flink的DataStream和 DataSet API支持多种类型。
组合类型,比如元组(内置Scala和Java元组)、POJO、Scala case类和Flink的Row类型等,允许具有多个字段的嵌套数据结构,这些字段可以在Table的表达式中访问。其他类型,则被视为原子类型。
元组类型和原子类型,一般用位置对应会好一些;如果非要用名称对应,也是可以的:
元组类型,默认的名称是 “_1”, “_2”;而原子类型,默认名称是 ”f0”。
创建临时视图的第一种方式,就是直接从DataStream转换而来。同样,可以直接对应字段转换;也可以在转换的时候,指定相应的字段。
代码如下:
tableEnv.createTemporaryView("sensorView", dataStream)
tableEnv.createTemporaryView("sensorView", dataStream, 'id, 'temperature, 'timestamp as 'ts)
另外,当然还可以基于Table创建视图:
tableEnv.createTemporaryView("sensorView", sensorTable)
View和Table的Schema完全相同。事实上,在Table API中,可以认为View和Table是等价的。
表的输出,是通过将数据写入 TableSink 来实现的。TableSink 是一个通用接口,可以支持不同的文件格式、存储数据库和消息队列。
具体实现,输出表最直接的方法,就是通过 Table.insertInto() 方法将一个 Table 写入注册过的 TableSink 中。
代码如下:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings}
import org.apache.flink.table.api.scala.StreamTableEnvironment
import org.apache.flink.table.descriptors.{FileSystem, OldCsv, Schema}
object TableOutputTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val settings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tabelEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
// source table
tabelEnv.connect(new FileSystem().path("F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\word.txt"))
.withFormat(new OldCsv)
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("tableSource")
// sink table
tabelEnv.connect(new FileSystem().path("F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\outputTable.txt"))
.withFormat(new OldCsv)
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("tableOutput")
tabelEnv.connect(new FileSystem().path("F:\\SparkWorkSpace\\flink-learning\\src\\main\\resources\\aggOutputTable.txt"))
.withFormat(new OldCsv)
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("temperature", DataTypes.DOUBLE())
.field("cnt", DataTypes.BIGINT())
)
.createTemporaryTable("aggOutputTable")
// 对应sinK table的表对象
val resultTable = tabelEnv.sqlQuery(
"""
|select id,temperature as t
|from tableSource
|""".stripMargin)
val aggTableResult = tabelEnv.sqlQuery("select id, temperature, count(1) as cnt from tableSource group by id,temperature")
// 结果表中的数据向sinK table插入
resultTable.insertInto("tableOutput")
//由于聚合是不断变化的,会报这样的错:TableException: AppendStreamTableSink requires that Table has only insert changes.
aggTableResult.insertInto("aggOutputTable")
env.execute("xxxx")
}
}
在流处理过程中,表的处理并不像传统定义的那样简单。
对于流式查询(Streaming Queries),需要声明如何在(动态)表和外部连接器之间执行转换。与外部系统交换的消息类型,由更新模式(update mode)指定。
Flink Table API中的更新模式有以下三种:
1)追加模式(Append Mode)
在追加模式下,表(动态表)和外部连接器只交换插入(Insert)消息。
2)撤回模式(Retract Mode)
在撤回模式下,表和外部连接器交换的是:添加(Add)和撤回(Retract)消息。
插入(Insert)会被编码为添加消息;
删除(Delete)则编码为撤回消息;
更新(Update)则会编码为,已更新行(上一行)的撤回消息,和更新行(新行)的添加消息。
在此模式下,不能定义key,这一点跟upsert模式完全不同。
3)Upsert(更新插入)模式
在Upsert模式下,动态表和外部连接器交换Upsert和Delete消息。
这个模式需要一个唯一的key,通过这个key可以传递更新消息。为了正确应用消息,外部连接器需要知道这个唯一key的属性。
插入(Insert)和更新(Update)都被编码为Upsert消息;
删除(Delete)编码为Delete信息。
这种模式和Retract模式的主要区别在于,Update操作是用单个消息编码的,所以效率会更高。
除了输出到文件,也可以输出到Kafka。我们可以结合前面Kafka作为输入数据,构建数据管道,kafka进,kafka出。
代码如下:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{Kafka, OldCsv, Schema}
object OutputToKafkaTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val settings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
tableEnv.connect(new Kafka()
.version("0.11")
.topic("sensor")
.property("bootstrap.servers", "hadoop:9092")
.property("zookeeper.connect", "hadoop:2181")
)
.withFormat(new OldCsv())
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("kafkaSourceTable")
val kafkaTable: Table = tableEnv.from("kafkaSourceTable")
val resultTable = kafkaTable
.select('id, 'temperature)
.filter('id === "sensor_1")
tableEnv.connect(new Kafka()
.version("0.11")
.topic("sinkTest")
.property("bootstrap.servers", "hadoop:9092")
.property("zookeeper.connect", "hadoop:2181")
)
.withFormat(new OldCsv())
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("temp", DataTypes.DOUBLE())
)
.createTemporaryTable("kafkaOutputTable")
resultTable.insertInto("kafkaOutputTable")
env.execute("XXXX")
}
}
报这样的错
Exception in thread "main" org.apache.flink.table.api.NoMatchingTableFactoryException: Could not find a suitable table factory for 'org.apache.flink.table.factories.SerializationSchemaFactory' in
the classpath.
Reason: No factory implements 'org.apache.flink.table.factories.SerializationSchemaFactory'.
解决问题:
使用新Csv格式化jar包
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-csvartifactId>
<version>1.10.2version>
dependency>
ElasticSearch的connector可以在upsert(update+insert,更新插入)模式下操作,这样就可以使用Query定义的键(key)与外部系统交换UPSERT/DELETE消息。
另外,对于“仅追加”(append-only)的查询,connector还可以在append 模式下操作,这样就可以与外部系统只交换insert消息。
es目前支持的数据格式,只有Json,而flink本身并没有对应的支持,所以还需要引入依赖:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-jsonartifactId>
<version>1.10.0version>
dependency>
代码实现如下:
// 输出到es
tableEnv.connect(
new Elasticsearch()
.version("6")
.host("localhost", 9200, "http")
.index("sensor")
.documentType("temp")
)
.inUpsertMode() // 指定是 Upsert 模式
.withFormat(new Json())
.withSchema( new Schema()
.field("id", DataTypes.STRING())
.field("count", DataTypes.BIGINT())
)
.createTemporaryTable("esOutputTable")
aggResultTable.insertInto("esOutputTable")
Flink专门为Table API的jdbc连接提供了flink-jdbc连接器,我们需要先引入依赖:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-jdbc_2.11artifactId>
<version>1.10.0version>
dependency>
jdbc连接的代码实现比较特殊,因为没有对应的java/scala类实现ConnectorDescriptor,所以不能直接tableEnv.connect()。不过Flink SQL留下了执行DDL的接口:tableEnv.sqlUpdate()。
对于jdbc的创建表操作,天生就适合直接写DDL来实现,所以我们的代码可以这样写:
// 输出到 Mysql
val sinkDDL: String =
"""
|create table jdbcOutputTable (
| id varchar(20) not null,
| cnt bigint not null
|) with (
| 'connector.type' = 'jdbc',
| 'connector.url' = 'jdbc:mysql://localhost:3306/test',
| 'connector.table' = 'sensor_count',
| 'connector.driver' = 'com.mysql.jdbc.Driver',
| 'connector.username' = 'root',
| 'connector.password' = '123456'
|)
""".stripMargin
tableEnv.sqlUpdate(sinkDDL)
aggResultSqlTable.insertInto("jdbcOutputTable")
表可以转换为DataStream或DataSet。这样,自定义流处理或批处理程序就可以继续在 Table API或SQL查询的结果上运行了。
将表转换为DataStream或DataSet时,需要指定生成的数据类型,即要将表的每一行转换成数据类型。通常,最方便的转换类型就是Row。当然,因为结果的所有字段类型都是明确的,我们也经常会用元组类型来表示。
表作为流式查询的结果,是动态更新的。所以,将这种动态查询转换成的数据流,同样需要对表的更新操作进行编码,进而有不同的转换模式。
Table API中表到DataStream有两种模式:
用于表只会被插入(Insert)操作更改的场景。
用于任何场景。有些类似于更新模式中Retract模式,它只有Insert和Delete两类操作。
得到的数据会增加一个Boolean类型的标识位(返回的第一个字段),用它来表示到底是新增的数据(Insert),还是被删除的数据(老数据, Delete)。
代码实现如下:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings, Table}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.descriptors.{Csv, Kafka, Schema}
import org.apache.flink.types.Row
object OutputModeTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val settings: EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)
// 测试动态表的两种模式,toAppendStream和toRecractDStream
// 1. toAppendStream
// 流来源表
tableEnv.connect(new Kafka()
.version("0.11")
.topic("sensor")
.property("bootstrap.servers", "hadoop:9092")
.property("zookeeper.connect", "hadoop:2181")
)
.withFormat(new Csv())
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
)
.createTemporaryTable("kafkaSourceData")
// val table: Table = tableEnv.from("kafkaSourceData")
val resultTable: Table = tableEnv.sqlQuery(
"""
|select id,temperature from kafkaSourceData
|""".stripMargin)
// 转换成追加流
val resultStream: DataStream[(String, Double)] = tableEnv.toAppendStream[(String, Double)](resultTable)
val resultStream2: DataStream[Row] = tableEnv.toAppendStream[Row](resultTable)
// 复习下前面的 将Table转换成流
val resultStream3: DataStream[(String, Double)] = resultTable.toAppendStream[(String, Double)]
val resultStream4: DataStream[Row] = resultTable.toAppendStream[Row]
// resultStream.print("resultStream")
// resultStream.print("resultStream2")
// resultStream.print("resultStream3")
// resultStream.print("resultStream4")
// 2.toRecractDStream
val resultTable2: Table = tableEnv.sqlQuery("select id, avg(temperature) from kafkaSourceData group by id")
val aggResultStream: DataStream[(Boolean, Row)] = tableEnv.toRetractStream[Row](resultTable2)
aggResultStream.print("aggResultStream")
/**
*输入
sensor1,188,39.9
sensor1,188,33.9
sensor3,188,37.9
sensor5,188,34.9
sensor2,188,31.9
sensor1,188,30.9
输出
aggResultStream> (true,sensor1,39.9)
aggResultStream> (false,sensor1,39.9)
aggResultStream> (true,sensor1,36.9)
aggResultStream> (true,sensor3,37.9)
aggResultStream> (true,sensor5,34.9)
aggResultStream> (true,sensor2,31.9)
aggResultStream> (false,sensor1,36.9)
aggResultStream> (true,sensor1,34.9)
*/
env.execute("OutputModeTest")
}
}
所以,没有经过groupby之类聚合操作,可以直接用 toAppendStream 来转换;而如果经过了聚合,有更新操作,一般就必须用 toRetractDstream。
Table API提供了一种机制来解释(Explain)计算表的逻辑和优化查询计划。这是通过TableEnvironment.explain(table)方法或TableEnvironment.explain()方法完成的。
explain方法会返回一个字符串,描述三个计划:
未优化的逻辑查询计划
优化后的逻辑查询计划
实际执行计划
我们可以在代码中查看执行计划:
val explaination: String = tableEnv.explain(resultTable)
println(explaination)
Query的解释和执行过程,老planner和blink planner大体是一致的,又有所不同。整体来讲,Query都会表示成一个逻辑查询计划,然后分两步解释: