kettle跨库迁移【主键丢失问题-源码分析】
此片文章将深入kettle源码分析,实现目前迁库需要的功能,本迁库方案参考其他文章(kettle的一套流程完成对整个数据库的迁移)的同时,也分别用kettle-4.2、kettle-6.1工具实现过,也搭建kettle-6.1源码调试,修改编译后实现过。现在把其中遇到的问题,将结合源码分析的形式进行总结。
数据库迁移过程需要介质存放中间数据,Kettle有两种方案,一种是文件资源库,另一种是数据库资源库,如果迁移或抽取的数据量过大,建议采用数据库资源库。
迁库需求:SQLServer-->MySql
问题来源:在迁库过程中,表结构创建造成主键丢失问题
通过源码调试分析:kettle支持的主键字段类型是有限的,在主键为nvarchar的时候程序不设置主键。
流程介绍
因为其他文章已经有了介绍,下面就把大概的流程图附上。
整套流程分为:2个job,4个trans。
使用到的Trans插件:表输入、字段选择、复制记录到结果、从结果获取记录、设置变量、自定义java脚本、表输出。
主job为:
子job为:
要迁移的源库表名称获取,并设置到结果集,为下面的job使用
配置子job为前面的每一条记录(即每个表)执行一次该子job
获取记录中的表名称,并设置为到变量
读取当前表的结果信息,并在目标库中创建表(在个是难点)
从来源库抽取数据到目标库
主键丢失问题-源码分析
// 自定义脚本调用getDDL()方法
public String getDDL( String tableName, RowMetaInterface fields, String tk, boolean use_autoinc, String pk,
boolean semicolon ) throws KettleDatabaseException {
String retval;
// First, check for reserved SQL in the input row r...
databaseMeta.quoteReservedWords( fields );
String quotedTk = tk != null ? databaseMeta.quoteField( tk ) : null;
if ( checkTableExists( tableName ) ) {
retval = getAlterTableStatement( tableName, fields, quotedTk, use_autoinc, pk, semicolon );
} else {
retval = getCreateTableStatement( tableName, fields, quotedTk, use_autoinc, pk, semicolon );
}
return retval;
}
注意:第一次创建表走的getCreateTableStatement( ),代码如下:
/**
* Generates SQL
*
* @param tableName the table name or schema/table combination: this needs to be quoted properly in advance.
* @param fields the fields
* @param tk the name of the technical key field
* @param use_autoinc true if we need to use auto-increment fields for a primary key
* @param pk the name of the primary/technical key field
* @param semicolon append semicolon to the statement
* @return the SQL needed to create the specified table and fields.
*/
public String getCreateTableStatement( String tableName, RowMetaInterface fields, String tk,
boolean use_autoinc, String pk, boolean semicolon ) {
StringBuilder retval = new StringBuilder();
DatabaseInterface databaseInterface = databaseMeta.getDatabaseInterface();
retval.append( databaseInterface.getCreateTableStatement() );
retval.append( tableName + Const.CR );
retval.append( "(" ).append( Const.CR );
boolean isSetPk = false;
for ( int i = 0; i < fields.size(); i++ ) {
if ( i > 0 ) {
retval.append( ", " );
} else {
retval.append( " " );
}
ValueMetaInterface v = fields.getValueMeta( i );
retval.append( databaseMeta.getFieldDefinition( v, tk, pk, use_autoinc ) );
}
// At the end, before the closing of the statement, we might need to add
// some constraints...
// Technical keys
if ( tk != null ) {
if ( databaseMeta.requiresCreateTablePrimaryKeyAppend() ) {
retval.append( ", PRIMARY KEY (" ).append( tk ).append( ")" ).append( Const.CR );
}
}
// Primary keys
if ( pk != null ) {
if ( databaseMeta.requiresCreateTablePrimaryKeyAppend() ) {
retval.append( ", PRIMARY KEY (" ).append( pk ).append( ")" ).append( Const.CR );
}
}
retval.append( ")" ).append( Const.CR );
retval.append( databaseMeta.getDatabaseInterface().getDataTablespaceDDL( variables, databaseMeta ) );
if ( pk == null && tk == null && databaseMeta.getDatabaseInterface() instanceof NeoviewDatabaseMeta ) {
retval.append( "NO PARTITION" ); // use this as a default when no pk/tk is
// there, otherwise you get an error
}
if ( semicolon ) {
retval.append( ";" );
}
return retval.toString();
}
在构造每个Field时的函数代码如下:
public String getFieldDefinition( ValueMetaInterface v, String tk, String pks, boolean use_autoinc,
boolean add_fieldname, boolean add_cr ) {
String retval = "";
String fieldname = v.getName();
int length = v.getLength();
int precision = v.getPrecision();
if ( add_fieldname ) {
retval += fieldname + " ";
}
int type = v.getType();
switch ( type ) {
case ValueMetaInterface.TYPE_DATE:
retval += "DATETIME";
break;
case ValueMetaInterface.TYPE_BOOLEAN:
if ( supportsBooleanDataType() ) {
retval += "BOOLEAN";
} else {
retval += "CHAR(1)";
}
break;
case ValueMetaInterface.TYPE_NUMBER:
case ValueMetaInterface.TYPE_INTEGER:
case ValueMetaInterface.TYPE_BIGNUMBER:
// if(isContainPk(pks,fieldname))
// Technical key
// Primary key
if ((fieldname.equalsIgnoreCase(tk) || fieldname.equalsIgnoreCase(pks)) && !pks.contains(","))
{
if ( use_autoinc ) {
retval += "BIGINT AUTO_INCREMENT NOT NULL PRIMARY KEY";
} else {
retval += "BIGINT NOT NULL PRIMARY KEY";
}
} else {
// Integer values...
if ( precision == 0 ) {
if ( length > 9 ) {
if ( length < 19 ) {
// can hold signed values between -9223372036854775808 and 9223372036854775807
// 18 significant digits
retval += "BIGINT";
} else {
retval += "DECIMAL(" + length + ")";
}
} else {
retval += "INT";
}
} else {
// Floating point values...
if ( length > 15 ) {
retval += "DECIMAL(" + length;
if ( precision > 0 ) {
retval += ", " + precision;
}
retval += ")";
} else {
// A double-precision floating-point number is accurate to approximately 15 decimal places.
// http://mysql.mirrors-r-us.net/doc/refman/5.1/en/numeric-type-overview.html
retval += "DOUBLE";
}
}
}
break;
case ValueMetaInterface.TYPE_STRING: // 当类型为nvarchar时不设置primary key
if ( length > 0 ) {
if ( length == 1 ) {
retval += "CHAR(1) ";
} else if ( length < 256 ) {
retval += "VARCHAR(" + length + ")";
} else if ( length < 65536 ) {
retval += "TEXT";
} else if ( length < 16777216 ) {
retval += "MEDIUMTEXT";
} else {
retval += "LONGTEXT";
}
} else {
retval += "TINYTEXT";
}
// 修改kettle中varchar不能设置主键问题
if ((fieldname.equalsIgnoreCase(tk) || fieldname.equalsIgnoreCase(pks)) && !pks.contains(","))
{
if ( use_autoinc ) {
retval += " NOT NULL PRIMARY KEY";
} else {
retval += " NOT NULL PRIMARY KEY";
}
}
break;
case ValueMetaInterface.TYPE_BINARY:
retval += "LONGBLOB";
break;
default:
retval += " UNKNOWN";
break;
}
if ( add_cr ) {
retval += Const.CR;
}
return retval;
}
从代码中看出,如果主键类型为nvarchar的字段,v.getType = 2,走case为2时,就不会给该这段设置primary key了,问题就出现在这里。加中文注释的地方就是我修改后的。
if ( tk != null ) {
if ( databaseMeta.requiresCreateTablePrimaryKeyAppend() ) {
retval.append( ", PRIMARY KEY (" ).append( tk ).append( ")" ).append( Const.CR );
}
}
// Primary keys
if ( pk != null ) {
if ( databaseMeta.requiresCreateTablePrimaryKeyAppend() ) {
retval.append( ", PRIMARY KEY (" ).append( pk ).append( ")" ).append( Const.CR );
}
}
因为是跨库迁移,上面Database类中的最后设置主键对我是没起作用的, 经过调试,也发现了kettle的一些问题,databaseMeta.requiresCreateTablePrimaryKeyAppend() 该判断不满足条件,所以结合自己的需求修改后,成功将主键问题解决了。
文章更新中。。。
特别感谢: