Apache Flink 具有两个关系 API(表 API 和 SQL)用于统一流和批处理。Table API 是 Scala 和 Java 的语言集成查询API,查询允许组合关系运算符,例如 select查询,过滤filter和连接 join。 Flink 的 SQL 支持基于实现 SQL 标准的 Apache Calcite。

Table API 和 SQL 接口彼此集成,以及 Flink 的 DataStream 和 DataSet API 也是。可以在基于 API 构建的所有 API 和库之间切换。例如,可以使用 CEP 库从 DataStream 中提取模式,然后 使用 Table API 分析模式,或者可以在预处理上运行 Gelly 图算法之前使用 SQL 查询扫描,过滤和聚合批处理表数据。

Maven 依赖:

    <properties>
        <flink.version>1.7.2</flink.version>
        <hadoop.version>2.7.6</hadoop.version>
        <scala.version>2.11.8</scala.version>
    </properties>
    <dependencies>
        <!-- flink核心API -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-scala_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>
    </dependencies>

student.txt

95002,刘晨,女,19,IS
95017,王风娟,女,18,IS
95018,王一,女,19,IS
95014,王小丽,女,19,CS
95019,邢小丽,女,19,IS
95020,赵钱,男,21,IS
95003,王敏,女,22,MA
95004,张立,男,19,IS
95012,孙花,女,20,CS
95010,孔小涛,男,19,CS
95005,刘刚,男,18,MA
95006,孙庆,男,23,CS
95007,易思玲,女,19,MA
95008,李娜,女,18,CS
95021,周二,男,17,MA
95022,郑明,男,20,MA
95001,李勇,男,20,CS
95011,包小柏,男,18,MA
95009,梦圆圆,女,18,MA
95015,王君,男,18,MA

1.读取数据源

  • 创建一个 TableEnvironment

    TableEnvironment 是 Table API 和 SQL 集成的核心概念。

    • 在内部目录中注册表
    • 注册外部目录
    • 执行 SQL 查询
    • 注册用户定义的(标量,表或聚合)函数
    • 将 DataStream 或 DataSet 转换为表 Table 对象
    • 可引用 ExecutionEnvironment 或 StreamExecutionEnvironment
  • 注册表

    TableEnvironment 维护按名称注册的表的目录。有两种类型的表,输入表和输出表。输入表 可以在表 API 和 SQL 查询中引用,并提供输入数据。输出表可用于将 Table API 或 SQL 查询 的结果发送到外部系统。

    可以从各种来源注册输入表:

    • 现有的 Table 对象,通常是 Table API 或 SQL 查询的结果。
    • TableSource,用于访问外部数据,例如文件,数据库或消息传递系统。
    • 一个 DataStream 或则 DataSet 也可以成为表的数据源。
    • 可以使用 TableSink 注册输出表

    注册表的处理方式与关系数据库系统中已知的 view 类似,即定义表的查询未优化, 但在另一个查询引用注册表时将内联。如果多个查询引用相同的注册表,则将为每个引用查询内联并多次执行,即不会共享注册表的结果。

    • 注册 Table:Table 在 TableEnvironment 中注册

    • 注册 TableSource:TableSource 在 TableExecutionEnvironment 中注册

      TableSource 提供对外部数据的访问,外部数据存储在存储系统中,例如数据库(MySQL, HBase,…),具有特定编码的文件(CSV,Apache [Parquet,Avro,ORC],…)或消息传递系 统(Apache Kafka,RabbitMQ,…)。

    • DataSet 和 DataStream 转换为 Table

    • 注册 TableSink

      注册的 TableSink 可用于将 Table API 或 SQL 查询的结果发送到外部存储系统(数据库,文件 系统)(例如,JDBC,Apache HBase,Apache Cassandra,Elasticsearch)或消息传递系统(例 如,Apache Kafka,RabbitMQ 的)。TableSink 支持各种文件格式(例如 CSV,Apache Parquet, Apache Avro)。

      注意:

    1. 批处理表只能写入 BatchTableSink

    2. 流表需要 AppendStreamTableSink,RetractStreamTableSink 或 UpsertStreamTableSink

    3. 自定义 TableSink(继承 TableSinkBase) Table.insertInto(String tableName)方法将 Table 提交到注册的 TableSink。该方法通过名称从目录中查找 TableSink,并验证 Table 的 schema 是否与 TableSink 的 schema 相同。

    4. 注册外部目录 外部目录可以提供有关外部数据库和表的信息,例如其名称,架构,统计信息以及有关如何 访问存储在外部数据库,表或文件中的数据的信息。

ReadTableSource.scala

package blog.table

import org.apache.flink.api.common.typeinfo.{TypeInformation, Types}
import org.apache.flink.api.scala.{DataSet, ExecutionEnvironment, _}
import org.apache.flink.core.fs.FileSystem.WriteMode
import org.apache.flink.table.api.scala.BatchTableEnvironment
import org.apache.flink.table.api.{Table, TableEnvironment}
import org.apache.flink.table.sinks.CsvTableSink
import org.apache.flink.table.sources.CsvTableSource

/**
* @Author Daniel
* @Description Flink Table API——使用CSVTableSource方式读取数据
*
**/
object ReadTableSource {

def main(args: Array[String]): Unit = {
  //创建table编程入口
  val datasetEnv = ExecutionEnvironment.getExecutionEnvironment
  val tableEnv = TableEnvironment.getTableEnvironment(datasetEnv)
  val csvTableSourcePath: String = "student.txt"

  val source1: CsvTableSource = new CsvTableSource(
    csvTableSourcePath,
    Array[String]("id", "name", "sex", "age", "department"),
    Array[TypeInformation[_]](Types.INT, Types.STRING, Types.STRING, Types.INT, Types.STRING)
    , fieldDelim = ","
    , ignoreFirstLine = false
  )

  val source2: CsvTableSource = CsvTableSource
    .builder()
    .path(csvTableSourcePath)
    .field("id", Types.INT)
    .field("name", Types.STRING)
    .field("sex", Types.STRING)
    .field("age", Types.INT)
    .field("department", Types.STRING)
    .fieldDelimiter(",")
    .lineDelimiter("\n")
    .ignoreFirstLine
    .ignoreParseErrors
    .build()

  //第一种方式
  val resultTable: Table = selectAll(tableEnv, source1, "student1")
  //第二种方式
  selectAll(tableEnv, source2, "student2")

  //将数据从一个数据源导出到另一个数据源
  val fieldNames: Array[String] = Array("id", "name", "sex", "age", "department")
  val fieldTypes: Array[TypeInformation[_]] = Array(Types.INT, Types.STRING, Types.STRING, Types.INT, Types.STRING)
  //创建一个TableSink
  val csvSink: CsvTableSink = new CsvTableSink("flinkout/tablesource/student.txt", ",", 1, WriteMode.OVERWRITE)
  tableEnv.registerTableSink("student3", fieldNames, fieldTypes, csvSink)
  //插入数据
  resultTable.insertInto("student3")
  datasetEnv.execute()
}

private def selectAll(tableEnv: BatchTableEnvironment, studentCsvSource: CsvTableSource, tableName: String) = {
  //注册数据源
  tableEnv.registerTableSource(tableName, studentCsvSource)
  //DSL风格查询数据
  val resultTable1: Table = tableEnv.scan(tableName)
  //SQL风格查询数据
  val resultTable2: Table = tableEnv.sqlQuery("select id,name,sex,age,department from " + tableName)
  println("--------------打印表的结构----------------------")
  resultTable1.printSchema()
  // 打印输出表中的数据
  val dataset1: DataSet[Student] = tableEnv.toDataSet[Student](resultTable1)
  println("--------------DSL输出----------------------")
  dataset1.print()
  val dataset2: DataSet[Student] = tableEnv.toDataSet[Student](resultTable2)
  println("--------------SQL输出----------------------")
  dataset2.print()
  resultTable1
}
}

case class Student(id: Int, name: String, sex: String, age: Int, department: String)

2.表查询

Table API 是 Scala 和 Java 的语言集成查询 API。

API 基于 Table 类代表流或批处理,提供应用关系操作的方法。这些方法返回一个 新的 Table 对象,该对象表示在输入表上应用关系运算的结果。一些关系操作由多个方法调 用组成,例如 table.groupBy(…)。select(),其中 groupBy(…)指定表的分组,并选择(…)分组上 的投影表。 Table API 文档描述了流和批处理表支持的所有 Table API 操作。

TableQuery.scala

package blog.table

import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.api.scala.{ExecutionEnvironment, _}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.api.{TableEnvironment, Types, _}
import org.apache.flink.table.sources.CsvTableSource
import org.apache.flink.types.Row

/**
  * @Author Daniel
  * @Description Flink查询操作
  *
  **/
object TableQuery {

  def main(args: Array[String]): Unit = {

    //创建table编程入口
    val datasetEnv = ExecutionEnvironment.getExecutionEnvironment
    val tableEnv = TableEnvironment.getTableEnvironment(datasetEnv)
    val csvTableSourcePath: String = "student.txt"

    val source: CsvTableSource = new CsvTableSource(
      csvTableSourcePath,
      Array[String]("id", "name", "sex", "age", "department"),
      Array[TypeInformation[_]](Types.INT, Types.STRING, Types.STRING, Types.INT, Types.STRING)
      , fieldDelim = ","
      , ignoreFirstLine = false
    )

    tableEnv.registerTableSource("student", source)


    println("-----------------DSL:统计每个部门大于18岁的人数--------------------------")
    val result1: Table = tableEnv.scan("student")
      .as("id, name, sex, age, department")
      .filter("age>18")
      .groupBy("department")
      .select("department, id.count as total")
    tableEnv.toDataSet[Row](result1).print()


    println("-----------------DSL:统计每个部门大于18岁的人数(tick(')写法)--------------------------")
    val result2: Table = tableEnv.scan("student")
      .as("id, name, sex, age, department")
      .filter('age > 18)
      .groupBy('department)
      .select('department, 'id.count as 'total)
    tableEnv.toDataSet[Row](result2).print()


    println("-----------------DSL:找出人数大于6的部门--------------------------")
    val result3: Table = tableEnv.scan("student")
      .as("id, name, sex, age, department")
      .groupBy("department")
      .select("department, id.count as total")
      .where("total > 6")
    tableEnv.toDataSet[Row](result3).print()

    println("-----------------SQL:统计男性与女性中大于18岁的人数-------------------------")
    val result4: Table=tableEnv.sqlQuery(
      """
        select sex, count(*) as total
        from student
        where age > 18
        group by sex
      """.stripMargin)
   tableEnv.toDataSet[SexCount](result4).print()
  }
}

case class SexCount(age:String, count:Long)

3. DataSet 或DataStream与Table 相互转化

Flink TableAPI与SQL_Flink

3.1DataSet

注册 DataStream 或则 DataSet 为 Table DataStream 或 DataSet 可以在 TableEnvironment 中注册为表。结果表的 schema 取决于注册的DataStream 或 DataSet 的数据类型。转换 DataStream 或则 DataSet 为表 它不在 TableEnvironment 中注册 DataStream 或 DataSet,也可以直接转换为 Table。如果要在 Table API 查询中使用 Table,这很方便。

当将一个 Table 转换为 DataStream 或者 DataSet 时,需要指定生成的 DataStream 或者 DataSet 的数据类型,即需要转换表的行的数据类型,通常最方便的转换类型是 Row,下面列表概述了不同选项的功能:

  • Row:字段通过位置映射、可以是任意数量字段,支持空值,非类型安全访问

  • POJO:字段通过名称(POJO 字段作为 Table 字段时,必须命名)映射,可以是任意数量 字段,支持空值,类型安全访问

  • Case Class:字段通过位置映射,不支持空值,类型安全访问

  • Tuple:字段通过位置映射,不得多于 22(Scala)或者 25(Java)个字段,不支持空值,类

型安全访问

  • Atomic Type:Table 必须有一个字段,不支持空值,类型安全访问。

Table 转换为 DataSet

Table 转换为 DataStream 作为流式查询的结果的表被动态更新,即,当新记录到达查询的输入流时,它正在改变。将 表转换为 DataStream 有两种模式:

  • 附加模式:只有在动态表仅通过 INSERT 修改时才能使用此模式,即它仅附加并且以前 提交的结果永远不会更新。

  • 缩进模式:始终可以使用此模式。它使用布尔标志对 INSERT 和 DELETE 更改进行编程。 关于动态表,详细可参考官网,案例代码见课堂。

TableDataSet.scala

package blog.table

import org.apache.flink.api.common.typeinfo.{TypeInformation, Types}
import org.apache.flink.api.scala.{DataSet, ExecutionEnvironment, _}
import org.apache.flink.table.api.scala.{BatchTableEnvironment, _}
import org.apache.flink.table.api.{Table, TableEnvironment}
import org.apache.flink.table.sources.CsvTableSource

/**
  * @Author Daniel
  * @Description Table与DataSet的相互转化
  *
  **/
object TableDataSet {

  def main(args: Array[String]): Unit = {

    // 获取编程入口
    val batchEnvironment: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
    val tableEnv: BatchTableEnvironment = TableEnvironment.getTableEnvironment(batchEnvironment)
    val csvTableSourcePath: String = "student.txt"
    val source: CsvTableSource = new CsvTableSource(
      csvTableSourcePath,
      Array[String]("id", "name", "sex", "age", "department"),
      Array[TypeInformation[_]](Types.INT, Types.STRING, Types.STRING, Types.INT, Types.STRING)
      , fieldDelim = ","
      , ignoreFirstLine = false
    )
    //将数据源注册为table
    tableEnv.registerTableSource("student", source)
    val resultTable: Table = tableEnv.sqlQuery("select id,name,sex,age,department from student")
    //将table转为DataSet
    val studentDataSet: DataSet[(Int, String, String, Int, String)] = tableEnv.toDataSet[(Int, String, String, Int, String)](resultTable)
    //将DataSet转为table
    println("-------------按默认名称将DataSet转换为Table-------------")
    val studentTable1: Table = tableEnv.fromDataSet(studentDataSet)
    studentTable1.printSchema()

    println("------------将DataSet中的id与name转换为Table--------------")
    val studentTable2: Table = tableEnv.fromDataSet(studentDataSet, 'id, 'name)
    studentTable2.printSchema()

    println("---------------按指定顺序将DataSet转换为Table--------------")
    val studentTable3: Table = tableEnv.fromDataSet(studentDataSet, '_4, '_3, '_5, '_2, '_1)
    studentTable3.printSchema()

  }
}

3.2DataStream

package blog.template.table

import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.table.api.scala._
import org.apache.flink.table.api.{Table, TableEnvironment}

import scala.collection.mutable

/**
  * @Author Daniel
  * @Description Table与DataStream的相互转化
  *
  **/
object TableToDataStream {

  def main(args: Array[String]): Unit = {
    //模拟输入数据
    val data = Seq("hello", "hadoop", "flink", "hello", "spark", "hello", "storm", "hello", "flink", "flink")
    val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment
    val tableEnv = TableEnvironment.getTableEnvironment(streamEnv)
    //将DataStream转换为table
    val source: Table = streamEnv.fromCollection(data).toTable(tableEnv, 'word)
    //业务逻辑
    val tableResult: Table = source.groupBy('word) //分组
      .select('word, 'word.count) //统计
    //使用缩进模式将table转换为DataStream,它用true或false来标记数据的插入和撤回,true(数据不存在)代表插入,false(数据已存在)代表撤回
    val result: DataStream[(Boolean, (String, Long))] = tableEnv.toRetractStream[(String, Long)](tableResult)
    //自定义Sink进行输出
    result.addSink(new MySink())
    streamEnv.execute()

  }
}

//自定义Sink,输出插入进来的数据
class MySink extends RichSinkFunction[(Boolean, (String, Long))] {
  private var resultSet: mutable.Set[(String, Long)] = _

  //初始执行一次
  override def open(parameters: Configuration): Unit = {
    //初始化内存存储结构
    resultSet = new mutable.HashSet[(String, Long)]
  }

  //每个元素执行一次
  override def invoke(v: (Boolean, (String, Long)), context: SinkFunction.Context[_]): Unit = {
    //主要逻辑
    if (v._1) {
      resultSet.add(v._2)
    }
  }

  //最后执行一次
  override def close(): Unit = {
    //打印
    resultSet.foreach(println)
  }
}

4.动态表和连续查询

动态表是Flink的Table API和SQL支持流数据的核心概念。与表示批处理数据的静态表相比, 动态表随时间而变化。可以像静态批处理表一样查询它们。查询动态表会产生连续查询。连续查询永远不会终止并生成动态表作为结果。查询不断更新其(动态)结果表以反映其(动 态)输入表的更改。实质上,动态表上的连续查询与定义物化视图的查询非常相似。

值得注意的是,连续查询的结果始终在语义上等同于在输入表的快照上以批处理模式执行的相同查询的结果。简单来说就是连续查询也是由状态的,一次查询跟批处理查询相比,执行方式和结果是相同的。

下图显示了流,动态表和连续查询的关系:

Flink TableAPI与SQL_flink_02

  1. 流转换为动态表。

  2. 在动态表上连续查询,生成新的动态表。

  3. 生成的动态表将转换回流。

动态表首先是一个逻辑概念。在查询执行期间,动态表不一定(完全)物化