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_TIMESTAMP
和NOW
的特殊性,根据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. 转化默认值为合法类型
1
和2
这2
个步骤分别为获取默认值,和最后的设置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;
}
经过MySqlDefaultValuePreConverter
和ValueConverter
转化后的默认值是能够被SchemaBuilder
接受的类型。由于debezium
的实现,还是做了一些取舍。
1. mysql
的DATETIME
类型,会被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=