Debezium 解析 Mysql 默认值

Debezium在很长一段时间内都是不支持解析Mysql column default值,issues https://issues.jboss.org/browse/DBZ-191;

下面具体描述如何实现

1. 获取默认值

MySqlDdlParser.class这个类主要用来解析ddl语法,它支持对default语法的解析,内部有一个处理default值的方法,只不过该方法会忽略默认值。

protected void parseDefaultClause(Marker start) {
    tokens.consume("DEFAULT");
    if (isNextTokenQuotedIdentifier()) {
        // We know that it is a quoted literal ...
        parseLiteral(start);
    } else {
        if (tokens.matchesAnyOf("CURRENT_TIMESTAMP", "NOW")) {
            parseCurrentTimestampOrNow();
            parseOnUpdateOrDelete(tokens.mark());
        } else if (tokens.canConsume("NULL")) {
            // do nothing ...
        } else {
            parseLiteral(start);
        }
    }
}

从这个方法入手,对其进行改造,处理默认值; 首先ColumnEditor, Column需要增加下面的2个方法

/**
 * Get the default value of the column
 *
 * @return the default value
 */
Object defaultValue();

/**
 * Determine whether this column's has a default value
 *
 * @return {@code true} if the default value was provided, or {@code false} otherwise
 */
boolean hasDefaultValue();

parseDefaultClause方法改造

protected void parseDefaultClause(Marker start, ColumnEditor column) {
    tokens.consume("DEFAULT");
    if (isNextTokenQuotedIdentifier()) {
        // We know that it is a quoted literal ...
        Object defaultValue = parseLiteral(start);
        column.defaultValue(defaultValue);
    } else {
        if (tokens.matchesAnyOf("CURRENT_TIMESTAMP", "NOW")) {
            parseCurrentTimestampOrNow();
            parseOnUpdateOrDelete(tokens.mark());
            //CURRENT_TIMESTAMP and NOW default value will be replaced with epoch timestamp
            column.defaultValue("1970-01-01 00:00:00");
        } else if (tokens.canConsume("NULL")) {
            // If the default value of column is Null, we will set default value null;
            column.defaultValue(null);
        } else {
            Object defaultValue = parseLiteral(start);
            column.defaultValue(defaultValue);
        }
    }
}

parseLiteral()方法能够解析出默认值,通过这个方法得到的默认值,设置给column.defaultValue;由于CURRENT_TIMESTAMPNOW的特殊性,根据gunnarmorling探讨,决定将这2种默认值设置为epoch timestmap。通过parseDefaultClause方法,ddl语句的默认值都能够被解析获取。这部分工作量其实比较小。

2. avro schema 默认值

默认值最后会在TableSchemaBuilder.addField方法里设置给SchemaBuilder对象

protected void addField(SchemaBuilder builder, Column column, ColumnMapper mapper) {
    SchemaBuilder fieldBuilder = valueConverterProvider.schemaBuilder(column);
    if (fieldBuilder != null) {
        if (mapper != null) {
            // Let the mapper add properties to the schema ...
            mapper.alterFieldSchema(column, fieldBuilder);
        }
        if (column.isOptional()) fieldBuilder.optional();

        //增加的代码部分
        // if the default value is provided
        if (column.hasDefaultValue()) {
            fieldBuilder.defaultValue(column.defaultValue());
        }

        builder.field(column.name(), fieldBuilder.build());
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("- field '{}' ({}{}) from column {}", column.name(), builder.isOptional() ? "OPTIONAL " : "",
                         fieldBuilder.type(),
                         column);
        }
    } else {
        LOGGER.warn("Unexpected JDBC type '{}' for column '{}' that will be ignored", column.jdbcType(), column.name());
    }
}

如果列有默认值,会调用fieldBuilder.defaultValue(column.defaultValue());fieldBuilder.defaultValue方法会验证默认值的类型,如果默认值类型不合法会抛出异常。如果合法,avro schema里就有了默认值。

3. 转化默认值为合法类型

122个步骤分别为获取默认值,和最后的设置schema默认值。这2部分都是相对来讲简单,最难的部分是将1里获取的默认值类型转化为2里能够成功设置的值类型。

这步转化的步骤会在每一列column被成功解析后处理,这个处理过程在MysqlDdlParser.parseCreateColumn方法内。

protected Column parseCreateColumn(Marker start, TableEditor table, String columnName, String newColumnName) {
    ...

    parseColumnDefinition(start, columnName, tokens, table, column, isPrimaryKey);

    //增加的代码
    convertDefaultValueToSchemaType(column);

    // Update the table ...
    Column newColumnDefn = column.create();
    table.addColumns(newColumnDefn);
    ...
}

parseColumnDefinition方法会解析mysql ddl,也是在这个方法内,debezium获取mysql的默认值。这个方法之后转化默认值,调用convertDefaultValueToSchemaType

private void convertDefaultValueToSchemaType(ColumnEditor columnEditor) {
    final Column column = columnEditor.create();
    // if converters is not null and the default value is not null, we need to convert default value
    if (converters != null && columnEditor.defaultValue() != null) {
        Object defaultValue = columnEditor.defaultValue();
        final SchemaBuilder schemaBuilder = converters.schemaBuilder(column);
        if (schemaBuilder == null) {
            return;
        }
        final Schema schema = schemaBuilder.build();
        //In order to get the valueConverter for this column, we have to create a field;
        //The index value -1 in the field will never used when converting default value;
        //So we can set any number here;
        final Field field = new Field(column.name(), -1, schema);
        final ValueConverter valueConverter = converters.converter(column, field);
        if (defaultValue instanceof String) {
            defaultValue = defaultValuePreConverter.convert(column, (String)defaultValue);
        }
        defaultValue = valueConverter.convert(defaultValue);
        columnEditor.defaultValue(defaultValue);
    }
}

debezium本来已经有一个类型转换器,每当mysql有数据变更时,变更的内容都会被这个转换器处理。所以在这个方法内,可以实例化一个ValueConverter,借助它来转换数据类型。但是ValueConverter的目标是实时变更内容的转化,这些内容基本都是合法的内容,然而默认值不一定都是合法的,所以对于默认值转化不能完全支持。因此必须要有一个预先处理器,将默认值转化为ValueConverter能够处理的类型,defaultValue = defaultValuePreConverter.convert(column, (String)defaultValue)。少数的类型必须修改ValueConverter实现。

MySqlDefaultValuePreConverter.convert用来预处理字符类型的默认值,将其转化为ValueConverter能够处理的值。

public Object convert(Column column, String value) {
    if (value == null) {
        return value;
    }
    switch (column.jdbcType()) {
    case Types.DATE:
        return convertToLocalDate(value);
    case Types.TIMESTAMP:
        return convertToLocalDateTime(column, value);
    case Types.TIMESTAMP_WITH_TIMEZONE:
        return convertToTimestamp(value);
    case Types.TIME:
        return convertToDuration(column, value);
    case Types.BOOLEAN:
        return convertToBoolean(value);
    case Types.BIT:
        return convertToBits(column, value);

    case Types.TINYINT:
    case Types.SMALLINT:
        return convertToSmallInt(value);

    case Types.NUMERIC:
    case Types.DECIMAL:
        return convertToDecimal(value);

    case Types.FLOAT:
    case Types.DOUBLE:
        return convertToDouble(value);
    case Types.BIGINT:
        return convertToBigInt(value);
    case Types.INTEGER:
        return convertToInteger(value);
    }
    return value;
}

经过MySqlDefaultValuePreConverterValueConverter转化后的默认值是能够被SchemaBuilder接受的类型。由于debezium的实现,还是做了一些取舍。
1. mysqlDATETIME类型,会被debezium解析为timestamp类型,所以默认值必须要大于1970-01-01 00:00:00
2. mysql timestamp默认值0000-00-00 00:00:00,会被解析为1970-01-01 00:00:00

总结

经过这3个步骤后,debezium能够实现mysql默认值的解析了。

=v=

猜你喜欢

转载自blog.csdn.net/sweatott/article/details/80572828
今日推荐