Flink Table API 和 Flink SQL

1. 简单介绍

1.1 什么是 Table API 和 Flink SQL

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中,指定的查询都具有相同的语义,得到相同的结果。

1.2 pom依赖

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里。

1.3 两种planner(old & blink)的区别

  1. 批流统一:Blink将批处理作业,视为流式处理的特殊情况。所以,blink不支持表和DataSet之间的转换,批处理作业将不转换为DataSet应用程序,而是跟流处理一样,转换为DataStream程序来处理。
  2. 因为批流统一,Blink planner也不支持BatchTableSource,而使用有界的StreamTableSource代替。
  3. Blink planner只支持全新的目录,不支持已弃用的ExternalCatalog。
  4. 旧planner和Blink planner的FilterableTableSource实现不兼容。旧的planner会把PlannerExpressions下推到filterableTableSource中,而blink planner则会把Expressions下推。
  5. 基于字符串的键值配置选项仅适用于Blink planner。
  6. PlannerConfig在两个planner中的实现不同。
  7. Blink planner会将多个sink优化在一个DAG中(仅在TableEnvironment上受支持,而在StreamTableEnvironment上不受支持)。而旧planner的优化总是将每一个sink放在一个新的DAG中,其中所有DAG彼此独立。
  8. 旧的planner不支持目录统计,而Blink planner支持。

1.4 一个简单TableAPI使用示例

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)

2. API调用

2.1 基本程序结构

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")

2.2 创建表环境

表环境(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)
    }
}

2.3 在Catalog中注册表

2.3.1 表(Table)的概念

TableEnvironment可以注册目录Catalog,并可以基于Catalog注册表。它会维护一个Catalog-Table表之间的map。
表(Table)是由一个“标识符”来指定的,由3部分组成:Catalog名、数据库(database)名和对象名(表名)。如果没有指定目录或数据库,就使用当前的默认值。
表可以是常规的(Table,表),或者虚拟的(View,视图)。常规表(Table)一般可以用来描述外部数据,比如文件、数据库表或消息队列的数据,也可以直接从 DataStream转换而来。视图可以从现有的表中创建,通常是table API或者SQL查询的一个结果。

2.3.2 连接到文件系统(Csv格式)

连接外部系统在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就可以了。

2.3.3 连接到Kafka

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")
    }
}

2.4 表的查询

2.4.1 简单查询

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")
    }
}

2.4.2 聚合查询

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()
    }
}

2.5 将DataStream 转换成表

Flink允许我们把Table和DataStream做转换:我们可以基于一个DataStream,先流式地读取数据源,然后map成样例类,再把它转成Table。Table的列字段(column fields),就是样例类里的字段,这样就不用再麻烦地定义schema了。

2.5.1 代码表达

代码中实现非常简单,直接用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)

2.5.2 数据类型与 Table schema的对应

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”。

2.6. 创建临时视图(Temporary View)

创建临时视图的第一种方式,就是直接从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是等价的。

2.7. 输出表

表的输出,是通过将数据写入 TableSink 来实现的。TableSink 是一个通用接口,可以支持不同的文件格式、存储数据库和消息队列。
具体实现,输出表最直接的方法,就是通过 Table.insertInto() 方法将一个 Table 写入注册过的 TableSink 中。

2.7.1 输出到文件

代码如下:

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")
    }
}

2.7.2 更新模式(Update Mode)

在流处理过程中,表的处理并不像传统定义的那样简单。
对于流式查询(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操作是用单个消息编码的,所以效率会更高。

2.7.3 输出到Kafka

除了输出到文件,也可以输出到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>

2.7.4 输出到ElasticSearch

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")

2.7.5 输出到MySql

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")

2.8 将表转换成DataStream

表可以转换为DataStream或DataSet。这样,自定义流处理或批处理程序就可以继续在 Table API或SQL查询的结果上运行了。
将表转换为DataStream或DataSet时,需要指定生成的数据类型,即要将表的每一行转换成数据类型。通常,最方便的转换类型就是Row。当然,因为结果的所有字段类型都是明确的,我们也经常会用元组类型来表示。

表作为流式查询的结果,是动态更新的。所以,将这种动态查询转换成的数据流,同样需要对表的更新操作进行编码,进而有不同的转换模式。
Table API中表到DataStream有两种模式:

  1. 追加模式(Append Mode)

用于表只会被插入(Insert)操作更改的场景。

  1. 撤回模式(Retract Mode)

用于任何场景。有些类似于更新模式中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。

2.9 Query的解释和执行

Table API提供了一种机制来解释(Explain)计算表的逻辑和优化查询计划。这是通过TableEnvironment.explain(table)方法或TableEnvironment.explain()方法完成的。
explain方法会返回一个字符串,描述三个计划:

未优化的逻辑查询计划
优化后的逻辑查询计划
实际执行计划

我们可以在代码中查看执行计划:

val explaination: String = tableEnv.explain(resultTable)
println(explaination)

Query的解释和执行过程,老planner和blink planner大体是一致的,又有所不同。整体来讲,Query都会表示成一个逻辑查询计划,然后分两步解释:

  1. 优化查询计划
  2. 解释成 DataStream 或者 DataSet程序
    而Blink版本是批流统一的,所以所有的Query,只会被解释成DataStream程序;另外在批处理环境TableEnvironment下,Blink版本要到tableEnv.execute()执行调用才开始解释。

你可能感兴趣的:(Spark)