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(开发技术)相关的推文

猜你喜欢

转载自www.cnblogs.com/Springmoon-venn/p/12664547.html
今日推荐