• Flink SQL 解析复杂(嵌套)JSON


    在 Flink 1.10 的 Table API 和 SQL 中,表支持的格式有四种:

    CSV Format
    JSON Format
    Apache Avro Format
    Old CSV Format

    官网地址如下:https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/connect.html#table-formats

    我用 JSON Format  比较多,也有嵌套的JSON 数据需要解析,大概描述一下。

     以下内容来下官网介绍:

    JSON格式允许读取和写入与给定格式 schema 相对应的JSON数据。 格式 schema 可以定义为Flink类型,JSON schema 或从所需的表 schema 派生。 Flink类型启用了更类似于SQL的定义并映射到相应的SQL数据类型。 JSON模式允许更复杂和嵌套的结构。
    如果格式 schema 等于表 schema,则也可以自动派生该 schema。 这只允许定义一次 schema 信息。 格式的名称,类型和字段的顺序由表的 schema 确定。 如果时间属性的来源不是字段,则将忽略它们。 表 schema 中的from定义被解释为以该格式重命名的字段。

    大概意思就是,flink 在解析json的时候,可以自己通过 schema(支持复杂的嵌套json),如果不提供 schema,默认使用 table schema 自动派生 json 的 schema(不支持复杂json)。

    官网对应 json format 的表的样例:

    CREATE TABLE MyUserTable (
      ...
    ) WITH (
      'format.type' = 'json',                   -- required: specify the format type
      'format.fail-on-missing-field' = 'true'   -- optional: flag whether to fail if a field is missing or not, false by default
    
      'format.fields.0.name' = 'lon',           -- optional: define the schema explicitly using type information.
      'format.fields.0.data-type' = 'FLOAT',    -- This overrides default behavior that uses table's schema as format schema.
      'format.fields.1.name' = 'rideTime',
      'format.fields.1.data-type' = 'TIMESTAMP(3)',
    
      'format.json-schema' =                    -- or by using a JSON schema which parses to DECIMAL and TIMESTAMP.
        '{                                      -- This also overrides the default behavior.
          "type": "object",
          "properties": {
            "lon": {
              "type": "number"
            },
            "rideTime": {
              "type": "string",
              "format": "date-time"
            }
          }
        }'
    )

    注:flink 1.10 字段的名称和类型可以从 table schema 中推断,不用写  format.fields.0.name 和 format.fields.0.data-type 了。

    CREATE TABLE user_log(
        user_id VARCHAR,
        item_id VARCHAR,
        category_id VARCHAR,
        behavior VARCHAR,
        ts TIMESTAMP(3)
    ) WITH (
        'connector.type' = 'kafka',
        'connector.version' = 'universal',
        'connector.topic' = 'user_behavior',
        'connector.properties.zookeeper.connect' = 'venn:2181',
        'connector.properties.bootstrap.servers' = 'venn:9092',
        'connector.startup-mode' = 'earliest-offset',
        'format.type' = 'json'
    );

    对应 json 数据如下:

    {"user_id": "315321", "item_id":"942195", "category_id": "4339722", "behavior": "pv", "ts": "2017-11-26T01:00:00Z"}

     对应的字段,会映射到对应的类型上,可以直接使用,比1.9 方便了不少。

    当然,这个并不是这里的主要内容。

    先来个嵌套的json看下:

    {"user_info":{"user_id":"0111","name":"xxx"},"timestam":1586670908699,"id":"10001"}

    这样的复杂sql该怎么解析呢?

    回来看下官网那段实例:

    'format.json-schema' =                    -- or by using a JSON schema which parses to DECIMAL and TIMESTAMP.
        '{                                      -- This also overrides the default behavior.
          "type": "object",
          "properties": {
            "lon": {
              "type": "number"
            },
            "rideTime": {
              "type": "string",
              "format": "date-time"
            }
          }
        }'

    SQL 的properties 中可以通过 属性 "format.json-schema"  设置输入的 json schema。

    Flink 的 json-schema 中支持如下的数据类型:

     再来看下刚刚的嵌套json:

    {"user_info":{"user_id":"0111","name":"xxx"},"timestam":1586670908699,"id":"10001"}

    第一层的 timestam、id 直接就映射到字段上,而 user_info 也是个json。

    从上面的实例上,可以看到 object 类型数据有  properties,而properties 的内容,怎么看都想是json的内层数据。

    所以上面的sql 对应的 json-schema 是这样的:

    'format.json-schema' = '{
            "type": "object",
            "properties": {
               "id": {type: "string"},
               "timestam": {type: "string"},
               "user_info":{type: "object",
                       "properties" : {
                           "user_id" : {type:"string"},
                           "name":{type:"string"}
                       }
                 }
            }
        }'

    从上面的 json schame 和 Flink SQL 的映射关系可以看出,user_info 对应的table 字段的类型是ROW,所以 table 的schema 是这样的:

    CREATE TABLE user_log(
        id VARCHAR,
        timestam VARCHAR,
        user_info ROW(user_id string, name string )
    )

    ROW 类型的 user_info,有两个字段:user_id 和 name

    注:使用的时候,直接用 "." 就可以了:如 user_info.user_id

    到此,嵌套json的 schame 就搞定了。

    下面我们再来看下  嵌套 json 数组:

    {"user_info":{"user_id":"0111","name":"xxx"},"timestam":1586670908699,"id":"10001","jsonArray":[{"name222":"xxx","user_id222":"0111"}]}

    这个又该怎么写 json schema 呢?

    官网有个实例说 json format 直接解析这样的复杂 json:

     "optional_address": {
          "oneOf": [
            {
              "type": "null"
            },
            {
              "$ref": "#/definitions/address"
            }
          ]
        }

    太长了,截取一段,官网明确说了支持这样的实例,也就是支持 json 数组

     json schema 和 Flink SQL 的映射关系中, json 的 array 对应 Flink SQL的 ARRAY[_]

    按照 object 类型的写法,写了个这样的:

    "jsonArray":{"type": "array",
                 "properties": {
                          "type": "object",
                          "properties" : {
                              "user_id222" : {type:"string"},
                              "name222" : {type:"string"}
                             }
                          }
                 }    

    收获了一个 exception:

    Caused by: java.lang.IllegalArgumentException: Arrays must specify an 'items' property in node: <root>/jsonArray
        at org.apache.flink.formats.json.JsonRowSchemaConverter.convertArray(JsonRowSchemaConverter.java:264)
        at org.apache.flink.formats.json.JsonRowSchemaConverter.convertType(JsonRowSchemaConverter.java:176)
        at org.apache.flink.formats.json.JsonRowSchemaConverter.convertObject(JsonRowSchemaConverter.java:246)

    然后,当然是 debug 代码了: org.apache.flink.formats.json.JsonRowSchemaConverter 就是解析 json-schema 的代码了

    JsonRowSchemaConverter 类有3个主要的方法分别对应解析不同类型的数据:

    // 解析 type 
    private static TypeInformation<?> convertType(String location, JsonNode node, JsonNode root)
    // 解析 object
    private static TypeInformation<Row> convertObject(String location, JsonNode node, JsonNode root)
    // 解析 array
    private static TypeInformation<?> convertArray(String location, JsonNode node, JsonNode root)

    convertType 方法在这里解析具体字段和类型:

    for (String type : types) {
                    // set field type
                    switch (type) {
                        case TYPE_NULL:
                            typeSet.add(Types.VOID);
                            break;
                        case TYPE_BOOLEAN:
                            typeSet.add(Types.BOOLEAN);
                            break;
                        case TYPE_STRING:
                            if (node.has(FORMAT)) {
                                typeSet.add(convertStringFormat(location, node.get(FORMAT)));
                            } else if (node.has(CONTENT_ENCODING)) {
                                typeSet.add(convertStringEncoding(location, node.get(CONTENT_ENCODING)));
                            } else {
                                typeSet.add(Types.STRING);
                            }
                            break;
                        case TYPE_NUMBER:
                            typeSet.add(Types.BIG_DEC);
                            break;
                        case TYPE_INTEGER:
                            // use BigDecimal for easier interoperability
                            // without affecting the correctness of the result
                            typeSet.add(Types.BIG_DEC);
                            break;
                        case TYPE_OBJECT:
                            typeSet.add(convertObject(location, node, root));
                            break;
                        case TYPE_ARRAY:
                            typeSet.add(convertArray(location, node, root));
                            break;
                        default:
                            throw new IllegalArgumentException(
                                "Unsupported type '" + node.get(TYPE).asText() + "' in node: " + location);
                    }
                }

    简单类型,就直接添加对应的 Flink SQL 类型, 复杂类型的 object、array 由单独的方法解析,这里我们看下 covertArray:

    private static TypeInformation<?> convertArray(String location, JsonNode node, JsonNode root) {
        // validate items
        if (!node.has(ITEMS)) {
            throw new IllegalArgumentException(
                "Arrays must specify an '" + ITEMS + "' property in node: " + location);
        }
        final JsonNode items = node.get(ITEMS);
    
        // list (translated to object array)
        if (items.isObject()) {
            final TypeInformation<?> elementType = convertType(
                location + '/' + ITEMS,
                items,
                root);
            // result type might either be ObjectArrayTypeInfo or BasicArrayTypeInfo for Strings
            return Types.OBJECT_ARRAY(elementType);
        }
        // tuple (translated to row)
        else if (items.isArray()) {
            final TypeInformation<?>[] types = convertTypes(location + '/' + ITEMS, items, root);
    
            // validate that array does not contain additional items
            if (node.has(ADDITIONAL_ITEMS) && node.get(ADDITIONAL_ITEMS).isBoolean() &&
                    node.get(ADDITIONAL_ITEMS).asBoolean()) {
                throw new IllegalArgumentException(
                    "An array tuple must not allow additional items in node: " + location);
            }
    
            return Types.ROW(types);
        }
        throw new IllegalArgumentException(
            "Invalid type for '" + ITEMS + "' property in node: " + location);
    }

    注:更多信息请查看源码(org.apache.flink.formats.json.JsonRowSchemaConverter)

    从上面的代码可以看出,从 convertTypes 中解析到是 array 类型的,就调用 convertArray 方法,而 convertArray 方法中第一步就是判断是否有个 ITEMS 字段,没有直接就报错:

    Arrays must specify an 'items' property in node: <root>/jsonArray

    有就  final JsonNode items = node.get(ITEMS)   get 出来继续解析,判断 items 是个 object 或 array (然后继续递归),都不是就抛出异常

    从源码可以看出 json 数组类型的 json schema 就是这样的:

    CREATE TABLE user_log(
        id VARCHAR,
        timestam VARCHAR,
        user_info ROW(user_id string, name string ),
        jsonArray ARRAY<ROW(user_id222 STRING, name222 STRING)>
    ) WITH (
        'connector.type' = 'kafka',
        'connector.version' = 'universal',
        'connector.topic' = 'complex_string',
        'connector.properties.zookeeper.connect' = 'venn:2181',
        'connector.properties.bootstrap.servers' = 'venn:9092',
        'connector.startup-mode' = 'earliest-offset',
        'format.type' = 'json',
        'format.json-schema' = '{
            "type": "object",
            "properties": {
               "id": {type: "string"},
               "timestam": {type: "string"},
               "user_info":{type: "object",
                       "properties" : {
                           "user_id" : {type:"string"},
                           "name":{type:"string"}
                       }
                 },
                "jsonArray":{"type": "array",
                             "items": {
                                      "type": "object",
                                      "properties" : {
                                          "user_id222" : {type:"string"},
                                          "name222" : {type:"string"}
                                         }
                                      }
                             }
            }
        }'
    );

    看过源码之后,对于上面的json schema 就没有难度了

    这里还要说下 json array 中有多个元素的案例:

    {"user_info":{"user_id":"0111","name":"xxx"},"timestam":1586676835655,"id":"10001","jsonArray":[{"name222":"xxx","user_id222":"0022"},{"name333":"name3333","user_id222":"user3333"},{"cc":"xxx333","user_id444":"user4444","name444":"name4444"}]}

    对应的 schema 也是这样的:

    "jsonArray":{"type": "array",
                             "items": {
                                      "type": "object",
                                      "properties" : {
                                          "user_id222" : {type:"string"},
                                          "name222" : {type:"string"}
                                         }
                                      }
                             }
            }

    因为在解析 json array 的时候,只能获取到一个 items 字段(多加也没用),会拿这个schema 去解析 json array 里面的所有元素,有对应字段就赋值,没用就为空

    表的列也是这样的:

    jsonArray ARRAY<ROW(user_id222 STRING, name222 STRING)>

    在查询中直接使用 jsonArray 会将所有数据直接查出来:

    INSERT INTO user_log_sink SELECT * FROM user_log;

    输出的数据如下:

    {"id":"10001","timestam":"1586676835655","user_info":{"user_id":"0111","name":"xxx"},"jsonArray":[{"user_id222":"0022","name222":"xxx"},{"user_id222":"user3333","name222":null},{"user_id222":null,"name222":null}]}

    json array 中的第一个元素 全部解出来了,第二个元素只有 user_id222 有值,第三个元素都没解析出来

     注:json array 是这样使用的:jsonArray[1].user_id222   # 代表 jsonArray 中的第一个元素的 user_id222 字段,数组下标从 1 开始,0 或 大于实际 json array 中的 长度会报 :  java.lang.ArrayIndexOutOfBoundsException: 1 

    欢迎关注Flink菜鸟公众号,会不定期更新Flink(开发技术)相关的推文

  • 相关阅读:
    [iOS微博项目
    [iOS微博项目
    [iOS微博项目
    [iOS微博项目
    APScheduler: standalone vs daemonic
    一句话解释jquery中offset、pageX, pageY、position、scrollTop, scrollLeft的区别
    cocos2d-x box2d使用调试绘图
    Maven管理Android项目1
    mina socket底层主流程源码实现
    5种IO模型
  • 原文地址:https://www.cnblogs.com/Springmoon-venn/p/12664547.html
Copyright © 2020-2023  润新知