☰
Kettle 4.4循环变量实战:${TABLENAME}作用域与替换时机
2026/10/26 2:02:38 网站建设 项目流程

简介:围绕 Kettle 循环变量设置展开的 docx 技术文档,面向使用 Kettle 做数据抽取、转换与加载的 ETL 开发者及数据集成人员,重点解决多表批量处理中的 SQL 表名替换、变量传递与循环作业控制问题。压缩包内含 1 个 docx 文件,大小约 282KB,内容集中,便于按主题查阅和对照实践。文档结合 User_Tables 存放表名、统计表 A/B 总行数等场景,梳理 Trans 脚本存取表名、获取表的数量、循环控制器、获取表行数与计数器累加等步骤,并说明如何把当前表名赋值给 TABLENAME 变量以驱动循环。尤其针对变量一次只能接收一个值的限制,以及遍历多表并逐个赋值的过程给出较完整说明,可作为相关批量抽取流程搭建时的参考。已有 2545 人学习,适合需要理解 Kettle 变量设置与循环作业实现思路的中初级读者查漏补缺。

1. 循环变量不是拖三个步骤就完事:Kettle 4.4 里那个不生效的 ${TABLENAME}

kettle 设置循环变量这件事,我见过太多人卡在同一个位置:转换里明明设了${TABLENAME},跑起来还是去查第一张表,要么干脆报一句表不存在。Kettle 4.4 的变量有作用域、有替换时机,作业和转换之间还隔着一层变量空间传递,任何一环没对上,日志都绿油油的,结果表里只有一张表的数据。这套环境在按表批量抽取、批量统计行数的场景里极其常见:元数据表里躺着一批表名,SQL 除了表名之外一字不差,手写 N 个转换既丑又没法维护。装不好会怎样?作业跑完你以为是成功的,第二天对账发现少了一大半数据。下面这套记录,适合正在用 Kettle 4.4 做批量 ETL、又不想给每张表复制一份转换的开发者,从 JDK 和驱动开始装,一路装到循环真的转起来。

2. 装 Kettle 4.4 之前先把环境钉死:JDK、驱动与目录

Kettle 4.4 是解压即用的绿色包,没有安装程序,也没有注册表写入,看起来省事,实际上环境依赖全靠外部三样东西撑着:JDK 版本、数据库驱动、目录位置。这三样任何一样不对,后面配循环变量时你会怀疑人生——因为报错信息根本不指向真正的原因。

2.1 JDK 选 1.7 而不是最新的:版本对照与 JAVA_HOME

Kettle 4.4 的编译目标是 JDK 1.6,官方当年推荐的运行环境也停在 1.6/1.7。我这边实测过三轮,结论写在下表里,你按自己的机器对号入座。

JDK 版本Spoon 能否启动实际表现我的建议
1.6能基础步骤正常,部分插件因为编译版本更高加载失败可用,但不推荐
1.7能变量设置、JavaScript 步骤、Trans API 调用都稳定首选,我全部环境用这个
1.8多数能Rhino 脚本引擎偶发异常,老插件类加载报 NoClassDefFoundError别用,排错成本高于收益

装 JDK 这一步没什么花活,关键是环境变量写在哪里。我一般不会去改/etc/profile,而是单独丢一个 profile.d 文件,出问题直接删掉就行,不会污染别的东西。

# 解压路径不要带空格和中文,Kettle 的启动脚本对空格处理得很糟糕 tar -zxvf jdk-7u80-linux-x64.tar.gz -C /opt/ # 单独一个环境文件,比直接改 /etc/profile 好回滚 cat > /etc/profile.d/kettle-env.sh <<'EOF' export JAVA_HOME=/opt/jdk1.7.0_80 # Kettle 的启动脚本优先读 PENTAHO_JAVA_HOME,没配才回落到 JAVA_HOME export PENTAHO_JAVA_HOME=$JAVA_HOME export KETTLE_HOME=/opt/data-integration export PATH=$JAVA_HOME/bin:$PATH EOF source /etc/profile.d/kettle-env.sh java -version # 确认输出是 1.7.x,别信 which java

这里有个容易被忽略的参数:PENTAHO_JAVA_HOME。spoon.sh、kitchen.sh、pan.sh三个脚本里都先判断这个变量,有值就直接用它指定的 JDK,完全绕开JAVA_HOME。机器上装了多个 JDK 的时候,只配JAVA_HOME经常出现「命令行 java 是 1.7,Spoon 起来还是 1.8」的玄学现场,两个都配上最省心。

提示:如果你在 Windows 上装,改的是set JAVA_HOME=...和set PENTAHO_JAVA_HOME=...,同样两个都要设,别只设一个。

2.2 解压即安装:data-integration 目录里哪些路径必须认

把下载到的压缩包解开,得到一个>cd $KETTLE_HOME chmod +x *.sh # 解压后脚本经常没有执行权限 ./spoon.sh & # 图形界面,远程机器上要转发 X11 或直接用命令行

chmod +x这一步看起来废话,但我见过至少三次「装完了打不开」最后发现是压缩包解压后权限丢失。脚本执行完,Spoon 主界面出来,左下角会显示当前 JDK 版本,顺手看一眼是不是 1.7,别等配完变量才发现跑在 1.8 上。

2.3 数据库驱动落在 libext:别丢进 lib

Kettle 自带一堆数据库驱动,但 MySQL 的驱动因为授权原因不在里面,得自己放。放的位置只有一处:libext。放lib也能跑,但下次升级或者换个版本覆盖安装,你放进去的 jar 就没了,而且这种丢失是静默的——连不上库的时候才会发现。

# 驱动版本别追新,5.1.x 跟 Kettle 4.4 的连接池配合最稳 cp mysql-connector-java-5.1.30-bin.jar $KETTLE_HOME/libext/ # 确认文件到位,注意权限要和目录里其它 jar 一致 ls -l $KETTLE_HOME/libext/ | grep -i mysql

放好之后重启 Spoon,新建一个数据库连接,点「Test」。测试通过说明驱动进 classpath 了;如果报No suitable driver,八成是文件名带了(1)这种后缀,或者放到了lib但没重启。这个验证动作不要跳,后面循环报错时你会反复怀疑是不是驱动问题,先把这条路排除掉。

2.4 准备循环用的元数据表:User_Tables 的最小结构

循环的源头是一张存表名的元数据表。原文里的场景是系统用户的User_Tables表,我把结构精简到刚好够循环用,字段别贪多。

CREATE TABLE user_tables ( id INT NOT NULL AUTO_INCREMENT, table_name VARCHAR(64) NOT NULL COMMENT '待抽取的物理表名', enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1 参与循环', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO user_tables (table_name) VALUES ('t_order_01'), ('t_order_02'), ('t_order_03');

id只负责排序,真正进变量的是table_name。表里加enabled是为了后面能临时踢掉某张表而不删记录,实际抽取时 SQL 带上WHERE enabled = 1。这里有个不起眼但很关键的细节:查询一定要带ORDER BY id,不写排序时不同驱动版本返回的行顺序可能不一致,循环跑出来的顺序每次都不一样,排查问题时会让你怀疑变量错乱。

顺手把公共连接参数写进kettle.properties,后面作业里所有连接都引用这几个变量,换环境只改一个文件。

# ~/.kettle/kettle.properties ETL_DB_HOST=127.0.0.1 ETL_DB_PORT=3306 ETL_DB_NAME=etl_meta ETL_DB_USER=etl_reader

改完这个文件必须重启 Spoon 才生效,它只在启动时读一次。这个文件里的变量属于全局变量,作用域比「设置变量」步骤低,但比命令行传参高,优先级的事放到下一章说。

3. 变量的作用域与替换时机:${TABLENAME} 到底在哪一层生效

循环变量配不对,十次里有七次是作用域选错,两次是替换时机没对上,剩下一次是变量名拼错。这一章把这三件事拆开讲清楚,后面拼作业的时候你就知道每个参数为什么那么填。

3.1 三种作用域:JVM、父作业、根作业的差别

Kettle 的「设置变量」步骤里有「变量类型」这个下拉框,三个选项看着像同义词,实际生效范围差很远。

变量类型生效范围典型用途注意点
Java Virtual Machine整个 JVM 进程,所有作业和转换都能读到脚本里动态拉起的转换、跨作业共享不会自动清理,下一轮要主动覆盖
父作业(Parent Job)只在当前父作业及其子转换里可见作业往下传参,最常见的选项转换里设置的父作业变量出不了这个转换
根作业(Root Job)最顶层作业及其所有下级多层作业嵌套时统一取值层级深了容易搞不清谁设的

关键结论:如果你的循环是在作业里用脚本动态拉起转换,那变量最好设成 JVM 级,因为new Trans()创建出来的对象和当前作业不在同一个变量空间里,父作业级变量有时候传不过去。这个坑我在一个批量统计的作业里踩过,作业日志显示变量设成功了,转换里读出来还是空字符串,查了两个小时才定位到作用域。

反过来,如果变量只在同一个转换内部用,设父作业级就够,不需要动 JVM 级。JVM 级变量属于「脏了就很难洗干净」的一类,同一个 JVM 里连跑多个作业,前一个作业留下的${TABLENAME}会被后一个作业读到,看着像灵异事件。

3.2 设置变量步骤的四个参数怎么填

「设置变量」步骤在转换里,参数就四个,但每个都有讲究。

参数填什么说明
字段名(Field Name)上游流里承载表名的字段必须和上一步输出字段名完全一致,大小写敏感
变量名(Variable Name)TABLENAME全大写、不带${},用的时候才加
变量类型(Variable Type)按上一节选脚本拉起转换就选 JVM
默认值(Default Value)留空或给个兜底表名变量没被赋值时用这个,别留空字符串

典型配置长这样:上游一个「表输入」查出table_name,接「设置变量」,字段名填table_name,变量名填TABLENAME,类型选 JVM。这个步骤每流过一行就设置一次变量,多行流过时以后面的行为准——所以如果你想让这个转换一次设置多个表名,是做不到的,变量一次只装一个值,这也是为什么原文要额外做遍历。

-- 这一步的查询,注意带上排序,变量最终值取决于最后一行 SELECT table_name FROM user_tables WHERE enabled = 1 ORDER BY id;

变量名用全大写不是硬性要求,但强烈建议。Kettle 的变量名在部分步骤里区分大小写,在另一些地方又不区分,混着写迟早出事。我这边统一规矩:自定义变量全大写加下划线,Kettle 内置变量按原样写(比如Internal.Job.Filename.Directory)。

3.3 表输入里的变量替换:发生在解析期还是执行期

${TABLENAME}写在「表输入」的 SQL 里,这个替换不是在你点预览的时候发生的,而是在步骤初始化、真正执行 SQL 之前。理解这一点很重要,因为它决定了你什么时候给变量赋值才有效。

-- 表输入里的 SQL,表名部分用变量占位 SELECT COUNT(1) AS cnt FROM ${TABLENAME} WHERE 1 = 1;

「表输入」步骤里有个「替换 SQL 语句里的变量」选项(不同小版本叫法略有差异,位置在步骤配置的 SQL 区域下方),默认是勾上的。如果你发现 SQL 原样发给了数据库、报语法错误,先看这个勾还在不在。曾经有人在排查时把这个勾去掉又忘了恢复,后面所有循环全部失效。

替换时机的实践含义是:变量必须在转换执行之前就已经设好。动态拉起转换的写法里,trans.setVariable("TABLENAME", 值)要写在trans.execute(null)之前,写在之后没有任何作用。这个顺序错了不会报错,只是安静地查了第一张表。

注意:${TABLENAME}在 SQL 里替换是纯字符串拼接,不会自动加引号。表名不需要引号,但如果你的变量值要作为字符串常量出现在 SQL 里,得自己写成'${TABLENAME}',两层引号都要在。

优先级别记个大致顺序就够用:步骤内设置的变量 > 作业往下传的变量 >kettle.properties里的全局变量 > 命令行-param:传入的参数。真遇到冲突,用「获取变量」步骤把当前值打出来看一眼,比猜快得多。

4. 循环作业的拼装:把 for 循环完整搬进 Kettle

到这一步环境齐了,变量规则也清楚了,可以开始拼。先泼一盆冷水:Kettle 的作业(.kjb)是有向无环图,连线不能成环,所以你在 Spoon 里画不出「执行完再回到判断」的箭头。原文里那套「循环控制器、获取表行数、计数器累加」的语义,落地时要么靠作业递归调用自己,要么靠脚本里写真正的 for。我选后者,理由在 4.2 里说。

4.1 子转换取表名:表输入加复制记录到结果

第一个转换负责把待循环的表名捞出来,名字叫get_table_list.ktr,两个步骤:表输入、复制记录到结果。

表输入的 SQL 就是 3.3 里那条带ORDER BY id的查询。「复制记录到结果」这个步骤在核心对象树的「作业」分类下,作用是把当前流里的所有行挂到转换的执行结果上,供作业层读取。这个步骤本身没有参数,拖进去连上线就行。

-- get_table_list.ktr 里表输入的 SQL SELECT table_name FROM user_tables WHERE enabled = 1 ORDER BY id;

为什么不用「设置变量」直接循环?因为「设置变量」每行设置一次,最后只留下最后一行的值,前面查出来的表名全被覆盖了。所以第一段必须先拿到完整结果集,把「多值」这个状态保存下来,再在作业层逐一消费。这是整套方案里第一个必须想明白的点。

转换建好后单独跑一次,确认输出行数和user_tables里enabled = 1的记录数一致。这一步不对,后面循环的边界值一定错。

4.2 在作业里用 JavaScript 收住循环:for 的三要素

建一个作业loop_count.kjb,第一步是 Start,第二步是「转换」作业项,指向get_table_list.ktr,并把它的执行结果作为作业的 previous_result。然后接一个「JavaScript」作业项,负责把结果集摊平成可反复读取的形式。

为什么不直接用 previous_result 一路传?因为循环体里每执行一次转换,作业的 previous_result 就会被那个转换的结果覆盖掉。第二轮再去读 previous_result,拿到的是行数统计结果,不是表名列表。这个坑不写在文档里,只有在第二次循环时才会暴露,而且表现是「第二张表名变成了一个数字」。

我的做法是初始化时把表名拼成一个带分隔符的字符串,存进 JVM 级变量,后面每一轮从字符串里取。

// 作业项 JavaScript:build_loop,负责初始化循环上界和表名列表 var rows = previous_result.getRows(); var result = false; if (rows != null && rows.size() > 0) { var list = ""; for (var i = 0; i < rows.size(); i++) { if (i > 0) { list += "|"; } // 字段名要和转换里输出的字段名一致 list += rows.get(i).getString("table_name", ""); } parent_job.setVariable("TABLE_LIST", list); parent_job.setVariable("TABLE_COUNT", "" + rows.size()); parent_job.setVariable("IDX", "0"); result = true; } else { // 没有待处理的表,直接让作业走结束分支 parent_job.setVariable("TABLE_COUNT", "0"); result = false; } result; // 最后这个布尔值就是该作业项的执行结果

代码里几个点说明一下。previous_result.getRows()拿的是上一个作业项的结果集,类型是 Kettle 的Result对象里的行列表,size()是行数,getString(字段名, 默认值)按字段取值。parent_job.setVariable(名, 值)把值写进当前作业的变量空间,第二个参数必须是字符串,数字要手动转。分隔符用|而不是逗号,因为表名里出现逗号的概率远低于出现下划线,但仍建议你确认一遍业务表名规则。

脚本最后一行留一个布尔表达式,Kettle 用它作为这一步的执行结果,配合连线上的「评估」设置走不同分支。有些小版本对最后一行表达式的识别不稳定,兜底办法是同时parent_job.setVariable("LOOP_OK", "true"),再用「简单评估」作业项判断这个变量。两种方式我都用过,前者简洁,后者保险。

4.3 循环体执行统计转换:Trans API 与 TABLENAME 传参

真正的循环写在第二个 JavaScript 作业项里,叫run_loop。它内部是一个标准的 for 循环,三要素对应原文里的说法:循环变量是i,上界是TABLE_COUNT,每轮末尾i++。

// 作业项 JavaScript:run_loop,逐表执行统计转换 var listStr = parent_job.getVariable("TABLE_LIST", ""); var parts = listStr.split("\\|"); // split 参数是正则,竖线必须转义 var total = parts.length; var okCount = 0; var result = true; // 用作业自身所在目录拼相对路径,避免写死绝对路径 var dir = parent_job.getVariable("Internal.Job.Filename.Directory", ""); for (var i = 0; i < total; i++) { var tableName = parts[i]; parent_job.setVariable("IDX", "" + i); parent_job.setVariable("TABLENAME", tableName); // 每次循环新建 TransMeta,保证变量在 execute 之前就已注入 var meta = new TransMeta(dir + "/count_rows.ktr"); var trans = new Trans(meta); trans.setVariable("TABLENAME", tableName); // 必须写在 execute 之前 trans.execute(null); trans.waitUntilFinished(); if (trans.getErrors() > 0) { parent_job.setVariable("FAILED_TABLE", tableName); result = false; break; // 单表失败就中断,别硬跑 } okCount++; } parent_job.setVariable("LOOP_DONE", "" + okCount); result;

split("\\|")这里要特别说一句:Rhino 里调用的是 Java 的String.split,参数按正则解释,竖线在正则里是「或」,不转义会直接把字符串拆成单个字符。这个错误不会抛异常,只会让你看到表名一个个变成t、_、o。

Internal.Job.Filename.Directory是 Kettle 的内置变量,值是当前作业文件所在目录,用它拼相对路径,作业整体搬迁到别的机器上不用改脚本。同理转换里还有Internal.Transformation.Filename.Directory。

trans.setVariable写在execute之前是硬性要求,前面 3.3 讲过原因。trans.waitUntilFinished()让脚本同步等待,没这行循环会瞬间跑完,实际一个转换都没执行完。getErrors()大于 0 说明这个转换有步骤报错,我选择中断而不是继续,因为批量统计里一张表失败往往意味着源库连接或者权限有问题,继续跑只是把错误放大。

循环体转换count_rows.ktr很小:一个表输入调${TABLENAME},一个表输出把结果落到汇总表。

-- count_rows.ktr 里表输入的 SQL,变量在 execute 前被替换 SELECT '${TABLENAME}' AS table_name, COUNT(1) AS row_cnt FROM ${TABLENAME};

注意第一个${TABLENAME}外面包了单引号,这样它作为字符串常量进入结果集,后面表输出就能把「哪张表」和「多少行」一起写进去。表输出目标表建三列:table_name、row_cnt、batch_time,batch_time可以用 Kettle 的「获取系统信息」步骤取当前时间,方便按批次核对。

4.4 命令行跑法与参数覆盖

配好之后别急着进调度,先用kitchen.sh跑一遍。命令行执行和 Spoon 里执行走的是同一套逻辑,但少了图形界面的干扰,报错更干净。

cd $KETTLE_HOME ./kitchen.sh \ -file=/opt/etl/loop_count.kjb \ -level=Detailed \ -logfile=/opt/etl/logs/loop_count_$(date +%Y%m%d%H%M).log

-level=Detailed会打出每个作业项的执行明细,循环里每轮的表名都能在日志里看到,这是后面验证的主要依据。-logfile建议带上日期,循环任务通常要回溯「昨天那次跑到第几张表断的」。

需要临时换库或者换批次的时候,用-param:传参,不要在脚本里改:

./kitchen.sh -file=/opt/etl/loop_count.kjb \ -param:ETL_DB_HOST=10.0.0.12 \ -param:ETL_DB_NAME=etl_meta_test

-param:传进去的是全局变量,优先级低于kettle.properties。如果你发现传了没生效,检查一下属性文件里是不是也定义了同名变量——同名时属性文件赢,这是很多人第一次用命令行传参时踩的坑。

5. 排查:循环变量装完不跑的五种翻车现场

这套东西装下来,能一次跑通的比例不高。下面五条是我自己在不同机器、不同版本上整理出来的高频问题,按「现象、原因、解决」记录,遇到问题按顺序对一遍,基本能定位。

第一条:SQL 发到数据库里还是${TABLENAME}本身,报语法错误。

现象是日志里那行 SQL 原封不动带着${},数据库返回语法错误。原因有两个可能:一是「表输入」里的「替换 SQL 语句里的变量」没勾上,二是变量压根没设成功。先看勾选,再看变量。检查变量最直接的办法是在转换里加一个「写日志」步骤,把${TABLENAME}打出来,如果打出来还是带${}的字面量,说明变量空间里根本没这个键。解决方式是把设置变量的位置提到执行之前,脚本拉起的转换必须在execute前setVariable,作业项之间的传递则确认变量类型选的是 JVM 或父作业而不是根作业。

第二条:循环跑了,但每次都只处理第一张表。

现象是日志里 N 轮全是同一张表名,结果表里只有一行。原因通常是从 previous_result 读表名的写法被循环体覆盖了——第一次循环体转换执行完,作业的 previous_result 就变成了统计结果,第二轮取出来的是行数不是表名。解决办法就是 4.2 里的做法:初始化时把表名列表摊平成字符串变量,循环体只从字符串里按索引取,不碰 previous_result。这个问题最阴的地方在于第一轮完全正常,你会以为配好了。

第三条:变量值前后多了空格,或者大小写对不上,报表不存在。

从数据库取出来的字符串如果字段是CHAR类型,会被空格补齐,进到变量里就带着尾随空格,替换进 SQL 之后表名对不上。现象是报「表 xxx 不存在」,但你把日志里的表名复制出来在客户端执行又是好的,因为肉眼看不见空格。解决方式是在 SQL 里TRIM(table_name),或者在脚本里tableName.replace(/^\s+|\s+$/g, "")清一遍。大小写问题在 Linux 上的 MySQL 更常见,lower_case_table_names为 0 时表名严格区分大小写,元数据表里存的是小写、物理表是大写就会失败。

第四条:作业在 Spoon 里能跑,kitchen.sh一跑就报找不到转换文件。

现象是命令行执行时报Transformation not found或者空指针。原因几乎都是相对路径的问题:Spoon 里当前工作目录是你打开 Spoon 的位置,命令行里是执行kitchen.sh的位置,两者不一样。Internal.Job.Filename.Directory能解决大部分情况,但如果你的作业是从资源库导出的,这个变量可能为空。解决方式是命令行里显式cd到作业目录再执行,或者干脆用绝对路径,别在这种地方省事。

第五条:跑几十张表之后内存一路上涨,最后 OOM。

现象是循环跑到中途 Java 堆告警,日志里出现OutOfMemoryError。原因是每轮new TransMeta()都会加载一遍转换定义和插件,对象没有及时释放,表多了就积起来了。短期解决办法是调大PENTAHO_DI_JAVA_OPTIONS里的-Xmx,长期办法是控制单次循环的表数量,按批拆分,比如每次跑 50 张表,作业外部用调度器分片。另外循环体里如果用了「复制记录到结果」,记得在转换末尾把结果清掉,不然结果集会一直挂在作业对象上。

提示:这五条里有三条的日志都是「绿色成功」,光看作业执行结果看不出来,必须看结果表的行数和 Detailed 日志里的表名序列。养成跑完先数行数的习惯,比事后对账省事。

6. 验证跑通:三段自检确认循环真的走满了 N 张表

装完不等于跑通,跑通不等于跑对。我这边固定做三段自检,每段都有明确的期望值,对不上就回去查对应章节。

第一段查元数据基线。在数据库里数一遍应该参与循环的表有多少张,跟作业日志里TABLE_COUNT打出来的数字比。

-- 期望值:这个数字要等于日志里的 TABLE_COUNT SELECT COUNT(1) FROM user <p> <a href="https://download.csdn.net/download/chen___feng/9671969" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询