21、Flink 的table API与DataStream API 集成(3)- changelog流处理、管道示例、类型转换和老版本转换示例

Flink 系列文章

1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接

13、Flink 的table api与sql的基本概念、通用api介绍及入门示例
14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性
15、Flink 的table api与sql之流式概念-详解的介绍了动态表、时间属性配置(如何处理更新结果)、时态表、流上的join、流上的确定性以及查询配置
16、Flink 的table api与sql之连接外部系统: 读写外部系统的连接器和格式以及FileSystem示例(1)
16、Flink 的table api与sql之连接外部系统: 读写外部系统的连接器和格式以及Elasticsearch示例(2)
16、Flink 的table api与sql之连接外部系统: 读写外部系统的连接器和格式以及Apache Kafka示例(3)
16、Flink 的table api与sql之连接外部系统: 读写外部系统的连接器和格式以及JDBC示例(4)
16、Flink 的table api与sql之连接外部系统: 读写外部系统的连接器和格式以及Apache Hive示例(6)
17、Flink 之Table API: Table API 支持的操作(1)
17、Flink 之Table API: Table API 支持的操作(2)
18、Flink的SQL 支持的操作和语法
19、Flink 的Table API 和 SQL 中的内置函数及示例(1)
19、Flink 的Table API 和 SQL 中的自定义函数及示例(2)
19、Flink 的Table API 和 SQL 中的自定义函数及示例(3)
19、Flink 的Table API 和 SQL 中的自定义函数及示例(4)
20、Flink SQL之SQL Client: 不用编写代码就可以尝试 Flink SQL,可以直接提交 SQL 任务到集群上
21、Flink 的table API与DataStream API 集成(1)- 介绍及入门示例、集成说明
21、Flink 的table API与DataStream API 集成(2)- 批处理模式和inser-only流处理
21、Flink 的table API与DataStream API 集成(3)- changelog流处理、管道示例、类型转换和老版本转换示例
21、Flink 的table API与DataStream API 集成(完整版)
22、Flink 的table api与sql之创建表的DDL
24、Flink 的table api与sql之Catalogs(介绍、类型、java api和sql实现ddl、java api和sql操作catalog)-1
24、Flink 的table api与sql之Catalogs(java api操作数据库、表)-2
24、Flink 的table api与sql之Catalogs(java api操作视图)-3
24、Flink 的table api与sql之Catalogs(java api操作分区与函数)-4
25、Flink 的table api与sql之函数(自定义函数示例)
26、Flink 的SQL之概览与入门示例
27、Flink 的SQL之SELECT (select、where、distinct、order by、limit、集合操作和去重)介绍及详细示例(1)
27、Flink 的SQL之SELECT (SQL Hints 和 Joins)介绍及详细示例(2)
27、Flink 的SQL之SELECT (窗口函数)介绍及详细示例(3)
27、Flink 的SQL之SELECT (窗口聚合)介绍及详细示例(4)
27、Flink 的SQL之SELECT (Group Aggregation分组聚合、Over Aggregation Over聚合 和 Window Join 窗口关联)介绍及详细示例(5)
27、Flink 的SQL之SELECT (Top-N、Window Top-N 窗口 Top-N 和 Window Deduplication 窗口去重)介绍及详细示例(6)
27、Flink 的SQL之SELECT (Pattern Recognition 模式检测)介绍及详细示例(7)
28、Flink 的SQL之DROP 、ALTER 、INSERT 、ANALYZE 语句
29、Flink SQL之DESCRIBE、EXPLAIN、USE、SHOW、LOAD、UNLOAD、SET、RESET、JAR、JOB Statements、UPDATE、DELETE(1)
29、Flink SQL之DESCRIBE、EXPLAIN、USE、SHOW、LOAD、UNLOAD、SET、RESET、JAR、JOB Statements、UPDATE、DELETE(2)
30、Flink SQL之SQL 客户端(通过kafka和filesystem的例子介绍了配置文件使用-表、视图等)
32、Flink table api和SQL 之用户自定义 Sources & Sinks实现及详细示例
33、Flink 的Table API 和 SQL 中的时区
41、Flink之Hive 方言介绍及详细示例
42、Flink 的table api与sql之Hive Catalog
43、Flink之Hive 读写及详细验证示例
44、Flink之module模块介绍及使用示例和Flink SQL使用hive内置函数及自定义函数详细示例–网上有些说法好像是错误的


文章目录

  • Flink 系列文章
  • 一、Table API 与 DataStream API集成
    • 6、Handling of Changelog Streams处理变化流
      • 1)、fromChangelogStream示例
      • 2)、toChangelogStream示例
    • 7、Adding Table API Pipelines to DataStream API 示例
    • 8、 TypeInformation 和 DataType 转换
      • 1)、TypeInformation to DataType
      • 2)、DataType to TypeInformation
    • 9、Legacy Conversion旧版转换
      • 1)、将 DataStream 转换成表
      • 2)、将表转换成 DataStream
      • 3)、数据类型到 Table Schema 的映射
        • 1、原子类型映射介绍及示例
        • 2、Tuple类型和 Case Class类型映射介绍及示例
        • 3、POJO 类型映射介绍及示例
        • 4、Row类型映射介绍及示例


本文是Flink table api 与 datastream api的集成的第三篇,主要 changelog流处理、管道示例、TypeInformation 和 DataType 转换和老版本table和datastream转换,并以具体的示例进行说明。
本文依赖flink、kafka集群能正常使用。
本文分为4个部分,即changelog流处理、管道示例、TypeInformation 和 DataType 转换和老版本table和datastream转换。
本文的示例是在Flink 1.17版本中运行。

一、Table API 与 DataStream API集成

6、Handling of Changelog Streams处理变化流

在内部,Flink的表运行时是一个changelog处理器。

StreamTableEnvironment提供了以下方法来暴露change data capture(CDC)功能:

  • fromChangelogStream(DataStream):将变更日志条目流(stream of changelog entries)解释为表。流记录类型必须为org.apache.flink.types.Row,因为其RowKind标志在运行时评估(evaluated )。默认情况下,不会传播事件时间和水印。该方法期望将包含所有类型更改的changelog(在org.apache.flink.types.RowKind中枚举)作为默认的ChangelogMode。

  • fromChangelogStream(DataStream, Schema):允许为DataStream定义类似于fromDataStream(DataStream ,schema )的schema 。否则,语义等于fromChangelogStream(DataStream)。

  • fromChangelogStream(DataStream, Schema, ChangelogMode):提供关于如何将stream 解释为changelog的完全控制。传递的ChangelogMode有助于planner 区分insert-only, upsert, or retract行为。

  • toChangelogStream(Table):fromChangelogStream(DataStream)的反向操作。它生成一个包含org.apache.flink.types.Row实例的流,并在运行时为每个记录设置RowKind标志。该方法支持各种更新表。如果输入表包含单个rowtime 列(single rowtime column),则它将传播到流记录的时间戳中(stream record’s timestamp)。水印也将被传播。

  • toChangelogStream(Table, Schema):fromChangelogStream(DataStream,Schema)的反向操作。该方法可以丰富生成的列数据类型。如果需要,planner 可以插入隐式转换。可以将rowtime写出为元数据列。

  • toChangelogStream(Table, Schema, ChangelogMode):提供关于如何将表转换为变更日志流(convert a table to a changelog stream)的完全控制。传递的ChangelogMode有助于planner 区分insert-only, upsert, or retract 行为。

从Table API的角度来看,和DataStream API的转换类似于读取或写入在SQL中使用CREATE Table DDL定义的虚拟表连接器。

由于fromChangelogStream的行为类似于fromDataStream。

此虚拟连接器还支持读取和写入流记录的rowtime 元数据。

虚拟表源实现SupportsSourceWatermark。

1)、fromChangelogStream示例

下面的代码展示了如何将fromChangelogStream用于不同的场景。

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.connector.ChangelogMode;
import org.apache.flink.types.Row;
import org.apache.flink.types.RowKind;

/**
 * @author alanchan
 *
 */
public class TestFromChangelogStreamDemo {

	//the stream as a retract stream
	//默认ChangelogMode应该足以满足大多数用例,因为它接受所有类型的更改。
	public static void test1() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、创建数据源
		DataStream<Row> dataStream =
			    env.fromElements(
			        Row.ofKind(RowKind.INSERT, "alan", 12),
			        Row.ofKind(RowKind.INSERT, "alanchan", 5),
			        Row.ofKind(RowKind.UPDATE_BEFORE, "alan", 12),
			        Row.ofKind(RowKind.UPDATE_AFTER, "alan", 100));
		
		// 3、changlogstream转为table
		Table table = tenv.fromChangelogStream(dataStream);

		// 4、创建视图
		tenv.createTemporaryView("InputTable", table);
		
		//5、聚合查询
		tenv.executeSql("SELECT f0 AS name, SUM(f1) AS score FROM InputTable GROUP BY f0")
		    .print();
//		+----+--------------------------------+-------------+
//		| op |                           name |       score |
//		+----+--------------------------------+-------------+
//		| +I |                       alanchan |           5 |
//		| +I |                               alan |          12 |
//		| -D |                              alan |          12 |
//		| +I |                               alan |         100 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		
		env.execute();
	}

	//the stream as an upsert stream (without a need for UPDATE_BEFORE)
	//展示了如何通过使用upsert模式将更新消息的数量减少50%来限制传入更改的类型以提高效率。
	//通过为toChangelogStream定义主键和upsert changelog模式,可以减少结果消息的数量。
	public static void test2() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		//2、创建数据源
		DataStream<Row> dataStream =
			    env.fromElements(
			        Row.ofKind(RowKind.INSERT, "alan", 12),
			        Row.ofKind(RowKind.INSERT, "alanchan", 5),
			        Row.ofKind(RowKind.UPDATE_AFTER, "alan", 100));
		
		// 3、转为table
		Table table =
				tenv.fromChangelogStream(
			        dataStream,
			        Schema.newBuilder().primaryKey("f0").build(),
			        ChangelogMode.upsert());
		
		// 4、创建视图
		tenv.createTemporaryView("InputTable", table);
		
		// 5、聚合查询
		tenv.executeSql("SELECT f0 AS name, SUM(f1) AS score FROM InputTable GROUP BY f0")
		    .print();
//		+----+--------------------------------+-------------+		
//		| op |                           name |       score |
//		+----+--------------------------------+-------------+
//		| +I |                       alanchan |           5 |
//		| +I |                               alan |          12 |
//		| -U |                               alan |          12 |
//		| +U |                              alan |         100 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		env.execute();
	}
	
	public static void main(String[] args) throws Exception {
//		test1();
		test2();
	}

}

2)、toChangelogStream示例

下面的代码展示了如何将toChangelogStream用于不同的场景。


import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.row;

import java.time.Instant;

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.data.StringData;
import org.apache.flink.types.Row;
import org.apache.flink.util.Collector;

/**
 * @author alanchan
 *
 */
public class TestToChangelogStreamDemo {

	static final String SQL =  "CREATE TABLE GeneratedTable "
		    + "("
		    + "  name STRING,"
		    + "  score INT,"
		    + "  event_time TIMESTAMP_LTZ(3),"
		    + "  WATERMARK FOR event_time AS event_time - INTERVAL '10' SECOND"
		    + ")"
		    + "WITH ('connector'='datagen')";
	
	//以最简单和最通用的方式转换为DataStream(无事件时间)
	public static void test1() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、构建数据源并聚合查询
		Table simpleTable = tenv
			    .fromValues(row("alan", 12), row("alan", 2), row("alanchan", 12))
			    .as("name", "score")
			    .groupBy($("name"))
			    .select($("name"), $("score").sum());

		// 3、将table转成datastream,并输出
		tenv
			    .toChangelogStream(simpleTable)
			    .executeAndCollect()
			    .forEachRemaining(System.out::println);
//		+I[alanchan, 12]
//		+I[alan, 12]
//		-U[alan, 12]
//		+U[alan, 14]
		
		env.execute();
	}

	//以最简单和最通用的方式转换为DataStream(使用事件时间)
	//由于`event_time`是schema的单个时间属性,因此它默认设置为流记录的时间戳;同时,它仍然是Row的一部分
	public static void test2() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、建表并填入数据
		tenv.executeSql(SQL);
		Table table = tenv.from("GeneratedTable");
		
		DataStream<Row> dataStream = tenv.toChangelogStream(table);
		
		dataStream.process(
			    new ProcessFunction<Row, Void>() {
			        @Override
			        public void processElement(Row row, Context ctx, Collector<Void> out) {

			             System.out.println(row.getFieldNames(true));
			             // [name, score, event_time]
			             
			             // timestamp exists twice
			             assert ctx.timestamp() == row.<Instant>getFieldAs("event_time").toEpochMilli();
			        }
			    });
		
		env.execute();
	}
	
	//转换为DataStream,但将time属性写出为元数据列,这意味着它不再是physical schema的一部分
	public static void test3() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、建表并填入数据
		tenv.executeSql(SQL);
		Table table = tenv.from("GeneratedTable");
		
		DataStream<Row> dataStream = tenv.toChangelogStream(
			    table,
			    Schema.newBuilder()
			        .column("name", "STRING")
			        .column("score", "INT")
			        .columnByMetadata("rowtime", "TIMESTAMP_LTZ(3)")
			        .build());

			// the stream record's timestamp is defined by the metadata; it is not part of the Row

			dataStream.process(
			    new ProcessFunction<Row, Void>() {
			        @Override
			        public void processElement(Row row, Context ctx, Collector<Void> out) {

			            // prints: [name, score]
			            System.out.println(row.getFieldNames(true));

			            // timestamp exists once
			            System.out.println(ctx.timestamp());
			        }
			    });
			
		env.execute();
	}
	
	//可以使用更多的内部数据结构以提高效率
	//这里提到这只是为了完整性,因为使用内部数据结构增加了复杂性和额外的类型处理
	//将TIMESTAMP_LTZ列转换为`Long`或将STRING转换为`byte[]`可能很方便,如果需要,结构化类型也可以表示为`Row`
	public static void test4() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、建表并填入数据
		tenv.executeSql(SQL);
		Table table = tenv.from("GeneratedTable");
		
		DataStream<Row> dataStream = tenv.toChangelogStream(
			    table,
			    Schema.newBuilder()
			        .column(  "name", DataTypes.STRING().bridgedTo(StringData.class))
			        .column(  "score", DataTypes.INT())
			        .column( "event_time", DataTypes.TIMESTAMP_LTZ(3).bridgedTo(Long.class))
			        .build());
		dataStream.print();
//		12> +I[1b6717eb5d93058ac3b40458a8a549a5e2fbb3b0fa146b36b7c58b5ebc1606cfc26ff9e4ebc3277832b9a8a0bfa1451d6608, 836085755, 1699941384531]
//		9> +I[6169d2f3a4766f5fce51cba66ccd33772ab72a690381563426417c75766f99de8b1fd5c3c7fc5ec48954df9299456f433fa9, -766105729, 1699941384531]
//		10> +I[e5a815e53d8fdf91b9382d7b15b6c076c5449e27b7ce505520c4334aba227d9a2fefd3333b2609704334b6fb866c244cf03d, 1552621997, 1699941384531]
		
		env.execute();
	}
	
	public static void main(String[] args) throws Exception {
//		test1();
//		test2();
//		test3();
		test4();
	}

}

示例test4()中数据类型支持哪些转换的更多信息,请参阅table API的数据类型页面。
toChangelogStream(Table).executeAndCollect()的行为等于调用Table.execute().collect()。然而,toChangelogStream(表)对于测试可能更有用,因为它允许访问DataStream API中后续ProcessFunction中生成的水印。

7、Adding Table API Pipelines to DataStream API 示例

单个Flink作业可以由多个相邻运行的断开连接的管道组成。

Table API中定义的Source-to-sink管道可以作为一个整体附加到StreamExecutionEnvironment,并在调用DataStream API中的某个执行方法时提交。

源不一定是table source,也可以是以前转换为Table API的另一个DataStream管道。因此,可以将 table sinks用于DataStream API程序。

通过使用StreamTableEnvironment.createStatementSet()创建的专用StreamStatementSet实例可以使用该功能。通过使用语句集,planner 可以一起优化所有添加的语句,并在调用StreamStatement set.attachAsDataStream()时提供一个或多个添加到StreamExecutionEnvironment的端到端管道( end-to-end pipelines)。

下面的示例演示如何将表程序添加到一个作业中的DataStream API程序。


import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.DiscardingSink;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableDescriptor;
import org.apache.flink.table.api.bridge.java.StreamStatementSet;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

/**
 * @author alanchan
 *
 */
public class TestTablePipelinesToDataStreamDemo {

	/**
	 * @param args
	 * @throws Exception 
	 */
	public static void main(String[] args) throws Exception {
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);

		StreamStatementSet statementSet = tenv.createStatementSet();
		
		// 建立数据源
		TableDescriptor sourceDescriptor =
			    TableDescriptor.forConnector("datagen")
			        .option("number-of-rows", "3")
			        .schema(
			            Schema.newBuilder()
			                .column("myCol", DataTypes.INT())
			                .column("myOtherCol", DataTypes.BOOLEAN())
			                .build())
			        .build();
		
		// 建立sink
		TableDescriptor sinkDescriptor = TableDescriptor.forConnector("print").build();
		
		// add a pure Table API pipeline
		Table tableFromSource = tenv.from(sourceDescriptor);
		statementSet.add(tableFromSource.insertInto(sinkDescriptor));
		
		// use table sinks for the DataStream API pipeline
		DataStream<Integer> dataStream = env.fromElements(1, 2, 3);
		Table tableFromStream = tenv.fromDataStream(dataStream);
		statementSet.add(tableFromStream.insertInto(sinkDescriptor));
		
		// attach both pipelines to StreamExecutionEnvironment (the statement set will be cleared after calling this method)
		statementSet.attachAsDataStream();

		// define other DataStream API parts
		env.fromElements(4, 5, 6).addSink(new DiscardingSink<>());

		// use DataStream API to submit the pipelines
		env.execute();
		
//		1> +I[287849559, true]
//		+I[1]
//		+I[2]
//		+I[3]
//		3> +I[-1058230612, false]
//		2> +I[-995481497, false]
		
	}

}

8、 TypeInformation 和 DataType 转换

DataStream API使用org.apache.flink.api.common.typeinfo.TypeInformation的实例来描述在流中传输的记录类型。特别是,它定义了如何将记录从一个DataStream操作符序列化和反序列化到另一个。它还可以帮助将状态序列化为savepoints and checkpoints。

Table API使用自定义数据结构在内部表示记录,并向用户暴露org.apache.flink.table.types.DataType,以声明数据结构转换为的外部格式,以便在 sources, sinks, UDFs, or DataStream API中更容易使用。

DataType比TypeInformation更丰富,因为它还包括有关逻辑SQL类型的详细信息。因此,在转换期间将隐式添加一些细节。

表的列名和类型自动从DataStream的TypeInformation派生。使用DataStream.getType()检查是否已通过DataStream API的反射类型提取工具正确检测到类型信息。如果最外层记录的TypeInformation是CompositeType,则在派生 table’s schema时,它将在第一级被展平(flattened )。

DataStream API并不总是能够基于反射提取更特定的TypeInformation。这通常是默默进行的,并转换成由通用Kryo序列化器支持的GenericTypeInfo。

例如,不能反射地分析Row类,并且始终需要显式类型信息声明。如果在DataStream API中没有声明适当的类型信息,则该行将显示为原始数据类型,并且table API无法访问其字段。在Java中使用.map(…).returns(TypeInformation)来显式声明类型信息。

1)、TypeInformation to DataType

将TypeInformation转换为DataType时适用以下规则:

  • TypeInformation的所有子类都映射到逻辑类型,包括与Flink的内置序列化器对齐(aligned)的为空性(nullability )。

  • TupleTypeInfoBase的子类被转换为行(用于row)或结构化类型(用于tuples、POJO和case类)。

  • 默认情况下,BigDecimal转换为DECIMAL(38,18)。

  • PojoTypeInfo字段的顺序由构造函数确定,所有字段都作为其参数。如果在转换过程中未找到,则字段顺序将按字母顺序排列。

  • 不能表示为列出的org.apache.flink.table.api.DataTypes之一的GenericTypeInfo和其他TypeInformation将被视为黑盒原始类型。当前会话配置用于具体化原始类型的序列化程序(materialize the serializer of the raw type)。然后将无法访问复合嵌套字段。

  • 有关完整的转换逻辑,请参阅TypeInfoDataTypeConverter.java 源码。

使用DataTypes.of(TypeInformation)在自定义schema 声明或UDF中调用上述逻辑。

2)、DataType to TypeInformation

表运行时将确保正确地将输出记录序列化到DataStream API的第一个运算符。

需要考虑DataStream API的类型信息语义。

9、Legacy Conversion旧版转换

以下部分介绍了API中将在未来版本中删除的过时部分。
特别是,这些部分可能没有很好地集成到最近的许多新功能和重构中。

1)、将 DataStream 转换成表

DataStream 可以直接转换为 StreamTableEnvironment 中的 Table。 结果视图的架构取决于注册集合的数据类型。

import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

....

	public static void testDataStreamToTable() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		DataStream<Row> dataStream = env.fromElements(Row.of("alan", 18), Row.of("alanchan", 19), Row.of("alanchanchn", 20), Row.of("alan", 20));
		Table table = tenv.fromDataStream(dataStream, $("name"), $("age"));

//		table.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                           name |         age |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		DataStream<Tuple2<String,Integer>> dataStream2 = env.fromElements(
				Tuple2.of("alan", 18),
				Tuple2.of("alanchan", 19),
				Tuple2.of("alanchanchn", 20),
				Tuple2.of("alan", 20)
				);
		Table table2 = tenv.fromDataStream(dataStream2,$("name"),$("age"));
		table2.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                           name |         age |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		env.execute();
	}
	

2)、将表转换成 DataStream

Table 可以被转换成 DataStream。 通过这种方式,定制的 DataStream 程序就可以在 Table API 或者 SQL 的查询结果上运行了。

将 Table 转换为 DataStream 时,你需要指定生成的 DataStream 的数据类型,即,Table 的每行数据要转换成的数据类型。 通常最方便的选择是转换成 Row 。 以下列表概述了不同选项的功能:

  • Row: 字段按位置映射,字段数量任意,支持 null 值,无类型安全(type-safe)检查。
  • POJO: 字段按名称映射(POJO 必须按Table 中字段名称命名),字段数量任意,支持 null 值,无类型安全检查。
  • Case Class: 字段按位置映射,不支持 null 值,有类型安全检查。
  • Tuple: 字段按位置映射,字段数量少于 22(Scala)或者 25(Java),不支持 null 值,无类型安全检查。
  • Atomic Type: Table 必须有一个字段,不支持 null 值,有类型安全检查。

流式查询(streaming query)的结果表会动态更新,即,当新纪录到达查询的输入流时,查询结果会改变。因此,像这样将动态查询结果转换成 DataStream 需要对表的更新方式进行编码。

将 Table 转换为 DataStream 有两种模式:

  • Append Mode: 仅当动态 Table 仅通过INSERT更改进行修改时,才可以使用此模式,即,它仅是追加操作,并且之前输出的结果永远不会更新。
  • Retract Mode: 任何情形都可以使用此模式。它使用 boolean 值对 INSERT 和 DELETE 操作的数据进行标记。
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.row;

import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.typeutils.TupleTypeInfo;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
//import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

import lombok.Data;

/**
 * @author alanchan
 *
 */
public class TestLegacyConversionDataStreamAndTableDemo {
	
	@Data
	public static class User{
		private String name;
		private int age;
	}
	
	public static void testTableToDataStream() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
//		Table table = tenv.fromValues(
//			    DataTypes.Row(
//			        DataTypes.FIELD("name", DataTypes.STRING()),
//			        DataTypes.FIELD("age", DataTypes.INT()),
//			    row("john", 35),
//			    row("sarah", 32)));
			    
		Table table =  tenv.fromValues(
	    	          DataTypes.ROW(
	    	             DataTypes.FIELD("name", DataTypes.STRING()),
	    	             DataTypes.FIELD("age", DataTypes.INT())
	    	         ),
	    	        row("alan", 18),
	    	        row("alanchan", 19),
	    	        row("alanchanchn", 20)
	    	     );
		
		// Convert the Table into an append DataStream of Row by specifying the class
		DataStream<Row> dsRow = tenv.toAppendStream(table, Row.class);
//		dsRow.print();
//		1> +I[alanchanchn, 20]
//		15> +I[alan, 18]
//		16> +I[alanchan, 19]
		
		// Convert the Table into an append DataStream of Tuple2 with TypeInformation
		TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(Types.STRING, Types.INT);
		DataStream<Tuple2<String, Integer>> dsTuple = tenv.toAppendStream(table, tupleType);
//		dsTuple.print();
//		3> (alanchan,19)
//		2> (alan,18)
//		4> (alanchanchn,20)
		
		// Convert the Table into a retract DataStream of Row.
		// A retract stream of type X is a DataStream>. 
		// The boolean field indicates the type of the change. 
		// True is INSERT, false is DELETE.
		DataStream<Tuple2<Boolean, Row>> retractStream = tenv.toRetractStream(table, Row.class);
//		retractStream.print();
//		10> (true,+I[alan, 18])
//		8> (true,+I[alanchan, 19])
//		9> (true,+I[alanchanchn, 20])
		
		DataStream<User> users = tenv.toAppendStream(table, User.class);
		users.print();
//		7> TestLegacyConversionDataStreamAndTableDemo.User(name=alan, age=18)
//		8> TestLegacyConversionDataStreamAndTableDemo.User(name=alanchan, age=19)
//		9> TestLegacyConversionDataStreamAndTableDemo.User(name=alanchanchn, age=20)
		
		env.execute();
	}

	public static void main(String[] args) throws Exception {
		testTableToDataStream();
	}

}

一旦 Table 被转化为 DataStream,必须使用 StreamExecutionEnvironment 的 execute 方法执行该 DataStream 作业。

3)、数据类型到 Table Schema 的映射

Flink 的 DataStream API 支持多样的数据类型。 例如 Tuple(Scala 内置,Flink Java tuple 和 Python tuples)、POJO 类型、Scala case class 类型以及 Flink 的 Row 类型等允许嵌套且有多个可在表的表达式中访问的字段的复合数据类型。其他类型被视为原子类型。下面,我们讨论 Table API 如何将这些数据类型类型转换为内部 row 表示形式,并提供将 DataStream 转换成 Table 的样例。

数据类型到 table schema 的映射有两种方式:基于字段位置或基于字段名称。

  • 基于位置映射介绍及示例

基于位置的映射可在保持字段顺序的同时为字段提供更有意义的名称。这种映射方式可用于具有特定的字段顺序的复合数据类型以及原子类型。如 tuple、row 以及 case class 这些复合数据类型都有这样的字段顺序。然而,POJO 类型的字段则必须通过名称映射。可以将字段投影出来,但不能使用as(Java 和 Scala) 或者 alias(Python)重命名。

定义基于位置的映射时,输入数据类型中一定不能存在指定的名称,否则 API 会假定应该基于字段名称进行映射。如果未指定任何字段名称,则使用默认的字段名称和复合数据类型的字段顺序,或者使用 f0 表示原子类型。

import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

......

	public static void testDataStreamToTableByPosition() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		DataStream<Tuple2<String, Integer>> dataStream2 = env.fromElements(Tuple2.of("alan", 18), Tuple2.of("alanchan", 19), Tuple2.of("alanchanchn", 20), Tuple2.of("alan", 20));
		Table table = tenv.fromDataStream(dataStream2, $("name"));
		table.execute().print();
//		+----+--------------------------------+
//		| op |                           name |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		| +I |                           alan |
//		+----+--------------------------------+
//		4 rows in set
		
		Table table2 = tenv.fromDataStream(dataStream2, $("name"), $("age"));
		table2.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                           name |         age |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		env.execute();
	}
  • 基于字段名称介绍及示例

基于名称的映射适用于任何数据类型包括 POJO 类型。这是定义 table schema 映射最灵活的方式。映射中的所有字段均按名称引用,并且可以通过 as 重命名。字段可以被重新排序和映射。

若果没有指定任何字段名称,则使用默认的字段名称和复合数据类型的字段顺序,或者使用 f0 表示原子类型。

import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

..............

	public static void testDataStreamToTableByName() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		DataStream<Tuple2<String, Integer>> dataStream = env.fromElements(Tuple2.of("alan", 18), Tuple2.of("alanchan", 19), Tuple2.of("alanchanchn", 20), Tuple2.of("alan", 20));
		
		// convert DataStream into Table with field "f1" only
		Table table = tenv.fromDataStream(dataStream, $("f1"));
		table.execute().print();
//		+----+-------------+
//		| op |          f1 |
//		+----+-------------+
//		| +I |          18 |
//		| +I |          19 |
//		| +I |          20 |
//		| +I |          20 |
//		+----+-------------+
//		4 rows in set
		
		// convert DataStream into Table with swapped fields
		Table table2 = tenv.fromDataStream(dataStream, $("f1"), $("f0"));
		table2.execute().print();
//		+----+-------------+--------------------------------+
//		| op |          f1 |                             f0 |
//		+----+-------------+--------------------------------+
//		| +I |          18 |                           alan |
//		| +I |          19 |                       alanchan |
//		| +I |          20 |                    alanchanchn |
//		| +I |          20 |                           alan |
//		+----+-------------+--------------------------------+
//		4 rows in set
		
		// convert DataStream into Table with swapped fields and field names "name" and "age"
		Table table3 = tenv.fromDataStream(dataStream, $("f1").as("name"), $("f0").as("age"));
		table3.execute().print();
//		+----+-------------+--------------------------------+
//		| op |        name |                            age |
//		+----+-------------+--------------------------------+
//		| +I |          18 |                           alan |
//		| +I |          19 |                       alanchan |
//		| +I |          20 |                    alanchanchn |
//		| +I |          20 |                           alan |
//		+----+-------------+--------------------------------+
//		4 rows in set
		
		env.execute();
	}
1、原子类型映射介绍及示例

Flink 将基础数据类型(Integer、Double、String)或者通用数据类型(不可再拆分的数据类型)视为原子类型。 原子类型的 DataStream 会被转换成只有一条属性的 Table。 属性的数据类型可以由原子类型推断出,还可以重新命名属性。

import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

..................

	public static void test1() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);

		DataStream<String> dataStream = env.fromElements("alan", "alanchan", "alanchanchn");
		
		// Convert DataStream into Table with field name "myName"
		Table table = tenv.fromDataStream(dataStream, $("myName"));
		table.execute().print();
//		+----+--------------------------------+
//		| op |                         myName |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		+----+--------------------------------+
//		3 rows in set
		
		env.execute();
	}
2、Tuple类型和 Case Class类型映射介绍及示例

Flink 支持 Scala 的内置 tuple 类型并给 Java 提供自己的 tuple 类型。 两种 tuple 的 DataStream 都能被转换成表。 可以通过提供所有字段名称来重命名字段(基于位置映射)。 如果没有指明任何字段名称,则会使用默认的字段名称。 如果引用了原始字段名称(对于 Flink tuple 为f0、f1 … …,对于 Scala tuple 为_1、_2 … …),则 API 会假定映射是基于名称的而不是基于位置的。 基于名称的映射可以通过 as 对字段和投影进行重新排序。


import static org.apache.flink.table.api.Expressions.$;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

/**
 * @author alanchan
 *
 */
public class TestLegacyConversionDataStreamAndTableDemo2 {

	public static void testDataStreamToTableByPosition() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		DataStream<Tuple2<String, Integer>> dataStream2 = env.fromElements(Tuple2.of("alan", 18), Tuple2.of("alanchan", 19), Tuple2.of("alanchanchn", 20), Tuple2.of("alan", 20));
		Table table = tenv.fromDataStream(dataStream2, $("name"));
//		table.execute().print();
//		+----+--------------------------------+
//		| op |                           name |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		| +I |                           alan |
//		+----+--------------------------------+
//		4 rows in set

		Table table2 = tenv.fromDataStream(dataStream2, $("name"), $("age"));
		table2.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                           name |         age |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set

		env.execute();
	}

	public static void testDataStreamToTableByName() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		DataStream<Tuple2<String, Integer>> dataStream = env.fromElements(Tuple2.of("alan", 18), Tuple2.of("alanchan", 19), Tuple2.of("alanchanchn", 20), Tuple2.of("alan", 20));

		// convert DataStream into Table with field "f1" only
		Table table = tenv.fromDataStream(dataStream, $("f1"));
		table.execute().print();
//		+----+-------------+
//		| op |          f1 |
//		+----+-------------+
//		| +I |          18 |
//		| +I |          19 |
//		| +I |          20 |
//		| +I |          20 |
//		+----+-------------+
//		4 rows in set

		// convert DataStream into Table with swapped fields
		Table table2 = tenv.fromDataStream(dataStream, $("f1"), $("f0"));
		table2.execute().print();
//		+----+-------------+--------------------------------+
//		| op |          f1 |                             f0 |
//		+----+-------------+--------------------------------+
//		| +I |          18 |                           alan |
//		| +I |          19 |                       alanchan |
//		| +I |          20 |                    alanchanchn |
//		| +I |          20 |                           alan |
//		+----+-------------+--------------------------------+
//		4 rows in set

		// convert DataStream into Table with swapped fields and field names "name" and
		// "age"
		Table table3 = tenv.fromDataStream(dataStream, $("f1").as("name"), $("f0").as("age"));
		table3.execute().print();
//		+----+-------------+--------------------------------+
//		| op |        name |                            age |
//		+----+-------------+--------------------------------+
//		| +I |          18 |                           alan |
//		| +I |          19 |                       alanchan |
//		| +I |          20 |                    alanchanchn |
//		| +I |          20 |                           alan |
//		+----+-------------+--------------------------------+
//		4 rows in set

		env.execute();
	}

	public static void main(String[] args) throws Exception {
		testDataStreamToTableByPosition();
		testDataStreamToTableByName();
	}

}
3、POJO 类型映射介绍及示例

Flink 支持 POJO 类型作为复合类型。

在不指定字段名称的情况下将 POJO 类型的 DataStream 转换成 Table 时,将使用原始 POJO 类型字段的名称。名称映射需要原始名称,并且不能按位置进行。字段可以使用别名(带有 as 关键字)来重命名,重新排序和投影。

import static org.apache.flink.table.api.Expressions.$;

import java.time.Instant;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

...............

	@NoArgsConstructor
	@AllArgsConstructor
	@Data
	public static class User {
		public String name;
		public Integer age;
		public Instant event_time;
	}
	
	public static void test2() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);
		
		// 2、创建数据源
		DataStream<User> dataStream =
			    env.fromElements(
			        new User("alan", 4, Instant.ofEpochMilli(1000)),
			        new User("alanchan", 6, Instant.ofEpochMilli(1001)),
			        new User("alanchanchn", 10, Instant.ofEpochMilli(1002)));
		
		// convert DataStream into Table with renamed fields "myAge", "myName" (name-based)
		Table table = tenv.fromDataStream(dataStream, $("age").as("myAge"), $("name").as("myName"),$("event_time").as("eventTime"));
//		table.execute().print();
//		+----+-------------+--------------------------------+-------------------------+
//		| op |       myAge |                         myName |               eventTime |
//		+----+-------------+--------------------------------+-------------------------+
//		| +I |           4 |                           alan | 1970-01-01 08:00:01.000 |
//		| +I |           6 |                       alanchan | 1970-01-01 08:00:01.001 |
//		| +I |          10 |                    alanchanchn | 1970-01-01 08:00:01.002 |
//		+----+-------------+--------------------------------+-------------------------+
//		3 rows in set
		
		// convert DataStream into Table with projected field "name" (name-based)
		Table table2 = tenv.fromDataStream(dataStream, $("name"));
		table2.execute().print();
//		+----+--------------------------------+
//		| op |                           name |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		+----+--------------------------------+
//		3 rows in set
		
		// convert DataStream into Table with projected and renamed field "myName" (name-based)
		Table table3 = tenv.fromDataStream(dataStream, $("name").as("myName"));
		table3.execute().print();
//		+----+--------------------------------+
//		| op |                         myName |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		+----+--------------------------------+
//		3 rows in set
		
		env.execute();
	}
4、Row类型映射介绍及示例

Row 类型支持任意数量的字段以及具有 null 值的字段。字段名称可以通过 RowTypeInfo 指定,也可以在将 Row 的 DataStream 转换为 Table 时指定。 Row 类型的字段映射支持基于名称和基于位置两种方式。 字段可以通过提供所有字段的名称的方式重命名(基于位置映射)或者分别选择进行投影/排序/重命名(基于名称映射)。

官方示例好像有些错误,如果定义的是Row类型,在转换的时候,$(“name”).as(“myName”)是会报错的,因为row的字段名称只有f0、f1,所以不会有name。

import static org.apache.flink.table.api.Expressions.$;

import java.time.Instant;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

.....

	public static void test3() throws Exception {
		// 1、创建运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		StreamTableEnvironment tenv = StreamTableEnvironment.create(env);

		DataStream<Row> dataStream = env.fromElements(Row.of("alan", 18), Row.of("alanchan", 19), Row.of("alanchanchn", 20), Row.of("alan", 20));

		// Convert DataStream into Table with renamed field names "myName", "myAge"
		// (position-based)
		Table table = tenv.fromDataStream(dataStream, $("myName"), $("myAge"));
//		table.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                         myName |       myAge |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		// Convert DataStream into Table with renamed fields "myName", "myAge"
		// (name-based)
		Table table2 = tenv.fromDataStream(dataStream, $("f0").as("myName"), $("f1").as("myAge"));
		table2.execute().print();
//		+----+--------------------------------+-------------+
//		| op |                         myName |       myAge |
//		+----+--------------------------------+-------------+
//		| +I |                           alan |          18 |
//		| +I |                       alanchan |          19 |
//		| +I |                    alanchanchn |          20 |
//		| +I |                           alan |          20 |
//		+----+--------------------------------+-------------+
//		4 rows in set
		
		// Convert DataStream into Table with projected field "name" (name-based)
		Table table3 = tenv.fromDataStream(dataStream, $("name"));
//		table3.execute().print();
//		+----+--------------------------------+
//		| op |                           name |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		| +I |                           alan |
//		+----+--------------------------------+
//		4 rows in set
		
		// Convert DataStream into Table with projected and renamed field "myName"
		// (name-based)
		Table table4 = tenv.fromDataStream(dataStream, $("f0").as("myName"));
		table4.execute().print();
//		+----+--------------------------------+
//		| op |                         myName |
//		+----+--------------------------------+
//		| +I |                           alan |
//		| +I |                       alanchan |
//		| +I |                    alanchanchn |
//		| +I |                           alan |
//		+----+--------------------------------+
//		4 rows in set
		
		env.execute();
	}

以上,本文是Flink table api 与 datastream api的集成的第三篇,主要 changelog流处理、管道示例、TypeInformation 和 DataType 转换和老版本table和datastream转换,并以具体的示例进行说明。

你可能感兴趣的:(#,Flink专栏,flink,大数据,flink,hive,flink,sql,flink,kafka,flink,流批一体化)