ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kettle Job循环获取结果集并传入转换:变量传递实战指南

Kettle Job循环获取结果集并传入转换:变量传递实战指南 简介Kettle作为常用的开源ETL工具其循环获取结果集并跨转换传递数据的实现方法适合有一定基础、需要处理批量数据迭代的开发与运维人员。资源包为一个PDF文档大小仅130KB内容紧凑地覆盖了完整实现链路从作业中通过上一个结果集对象获取数据行到设置数量、序号、标识、名称等变量再到循环控制与字段更新最终传入另一个转换并输出至本地文本文件。文档针对作业与转换的关键配置给出了说明与代码片段详细展示了获取结果集行、读写父作业变量等接口的实际用法以及空结果集、循环越界等边界情况的处理思路。对于需要在Kettle中实现批量表遍历、多表动态处理或跨转换参数传递的读者这份资料能提供直接可参考的解决方案。已有2365人学习下载适合希望快速掌握Kettle循环调度与参数传递技巧的使用者学习借鉴。1. Kettle 循环获取结果集并传入转换这个场景为什么值得花半小时搭一遍做 ETL 的同行应该都有过这种经历一个 Job 跑完查询得到一批表名或一批主键 ID接下来想对每一行分别做一次处理——比如逐个导出、逐个清洗、逐个调用接口。Kettle 里最自然的想法是“把结果集直接传给下一个转换”但真去拖步骤时会发现转换和转换之间根本不共享数据流结果集传过去是空的。这个资源给的方案是用 Job 级别的 JavaScript 步骤把结果集按行拆成变量再通过 parent_job.setVariable 塞给下游转换循环变量用 size 和 i 控制。整套逻辑不依赖第三方插件纯 Kettle 内置步骤就能跑通适合需要批量处理数据库查询结果、又不想写一堆临时表的场景。新手照着搭能绕开“结果集传不过去”的坑熟手也可以拿来跟自己的方案对比一下变量作用域的处理方式。2. 先搞懂 Job 与转换的边界为什么结果集不能直接跨转换传递2.1 Kettle 的执行模型Job 管流程转换管数据Kettle 里 Job作业和 Transformation转换是两套完全不同的执行体系。Job 负责编排流程按顺序执行、条件判断、循环跳转它的“数据”形态是变量和结果集Result而不是行流。转换才是真正处理数据的地方里面是一张步骤组成的图数据以行集RowSet的形式在步骤之间流动。这个区别直接导致了本资源的核心矛盾你在一个转换里查询出来的结果集比如 t1.ktr 里跑完 SQL 后得到的一堆 id 和 name是存在于这个转换的内存行集里的。转换跑完这个行集就释放了。下游的 var.ktr 是另一个独立进程至少是独立的执行单元它不可能直接读取上一个转换的行集。所以 Job 层面提供了一个中间产物叫“结果集”Result它能把上一个转换的输出行缓存下来下一个转换可以读取。听起来很方便但实际操作里有几个限制结果集只能在“同一个 Job 内的连续两个步骤”之间传递而且读取方必须是支持“从结果集读取”的步骤比如“复制记录到结果”“从结果获取记录”不是随手拖一个“表输入”就能接住。这正是很多人在 Job 里把两个转换串起来、却拿不到数据的原因——他们以为转换的输出会自动变成下游转换的输入。2.2 本资源的解法用 previous_result 把结果集转成变量理解了上面的模型再看资源里的设计就清晰了。它绕过了“步骤接收结果集”的依赖改用 JavaScript 步骤直接操作 Job 上下文里的 previous_result 对象。previous_result 是 Kettle Job 执行环境中保存的“上一个步骤的结果”本质上是一个包含多行数据的集合每行有若干个字段值。在作业 j1.kjb 里JavaScript 代码通过previous_result.getRows()拿到这个集合的所有行然后逐行取出字段值用parent_job.setVariable()把它写入 Job 级变量。写入之后这个变量对同一个 Job 后续启动的所有转换都可见包括 var.ktr。这样数据传递的链条就变成了t1.ktr 输出结果集 → Job 缓存结果集 → JavaScript 读取并拆成变量 → var.ktr 通过“获取变量”步骤消费。这套方案的代价也很明显变量是纯文本的类型信息会丢失而且一行一传如果结果集很大变量反复读写会有性能损耗。所以它适合的是“结果集行数不多但每行都要独立处理”的场景——比如十几张表的表名、几十个要清理的 ID、一批需要逐个跑批的日期。几千上万行的结果集就不建议这么干了直接用“复制记录到结果 从结果获取记录”在转换层面做循环更划算。2.3 变量作用域为什么必须用 parent_job 而不是 getVariable资源代码里反复出现的parent_job.setVariable()是另一个关键点。Kettle 里变量有好几层作用域系统级、作业级、转换级、步骤级。在 Job 的 JavaScript 步骤里脚本运行在 Job 的上下文环境中所以要写变量到 Job 作用域必须显式调用parent_job.setVariable()而不是setVariable()或者直接赋值。很多人在这一步翻车是因为在转换里的“JavaScript 代码”步骤写习惯了——那里的变量操作作用域是本转换setVariable()写入的变量只对当前转换的后续步骤可见Job 里下一个转换根本读不到。而parent_job正是从转换/步骤的 JavaScript 上下文指回其父级 Job 对象的引用只有通过它写入变量才能跨转换存活。另外注意JavaScript 步骤读取变量用的是parent_job.getVariable()这里如果变量还没有被创建返回的是 null 而不是空字符串。资源代码里第一段就对 prevRow 做了判空但如果后续代码里要用 size 或 id 变量最好也先判断一下否则数字运算时 null 会被强制转换成 0字符串拼接会变成字符串 null两个坑的表现形式完全不一样。3. 手把手复现这套循环逻辑作业脚本与转换配置逐行拆解3.1 作业 j1.kjb 的整体结构这套循环方案一共需要三个文件一个作业j1.kjb两个转换t1.ktr 和 var.ktr。作业负责编排循环逻辑t1.ktr 负责产出结果集var.ktr 负责消费变量并输出。整个作业一共三个步骤顺着跑一遍就理解了t1.ktr执行查询产生结果集必须勾选“将结果集传递给下一步骤”这类选项或者说作业方式直接执行转换时转换的输出行默认会写入 Job 结果集。JavaScript 1首次执行从 previous_result 取全部行初始化循环变量 size、i并取出第 0 行的 id 和 name。JavaScript 2判断 i 是否小于 size是则更新 id 和 namei 自增返回 true 表示“还有下一行”。其中 JavaScript 2 的返回值是关键Kettle 作业中 JavaScript 步骤的返回值是布尔类型true 表示作业继续执行后续步骤在这个场景里 true 表示“循环未结束继续跑下游转换”。false 则表示“已处理完流程可以结束了”。3.2 JavaScript 1初始化循环变量并取出第一行第一个 JavaScript 步骤的代码是整套逻辑的入口。原资源的代码有个小瑕疵——prevRow.size()0应该是prevRow.size()0这里我按修正后的版本写var prevRow previous_result.getRows(); // 获取上一个步骤传递的结果集返回的是一个数组/列表 if (prevRow null || prevRow.size() 0) { // 结果集为空直接返回 false作业不再往下走 false; } else { parent_job.setVariable(tables, prevRow); // 把整个行集合存为变量注意是 List 对象 parent_job.setVariable(size, prevRow.size()); // 行总数循环要用 parent_job.setVariable(i, 0); // 循环计数器从第 0 行开始 parent_job.setVariable(id, prevRow.get(0).getString(id, )); // 取出第 0 行的 id 字段 parent_job.setVariable(name, prevRow.get(0).getString(name, )); // 取出第 0 行的 name 字段 true; }这段代码的逻辑很直白先取上一环节的结果集如果为空就直接终止如果不为空就把行总数记到 size 里计数器 i 归零同时把第 0 行的 id 和 name 预置到变量里。这里有几个值得注意的点。getRows()返回的是一个 List 结构不是数组所以取长度用size()而不是length。getString(id, )是 Kettle ResultRow 对象的方法第二个参数是字段不存在时的默认值这里传空字符串是为了避免下游拿到 null。另外parent_job.setVariable(tables, prevRow)这一行实际存入的是一个 Java 对象如果后续想把它转成字符串用需要自己做处理作业变量面板里显示不出来但不影响在 JavaScript 里继续操作它。3.3 JavaScript 2循环判断与行数据更新第二个 JavaScript 步骤承担“循环推进器”的角色。每次作业循环回到这个步骤时它都要判断是否还有下一行需要处理var prevRow previous_result.getRows(); // 再次获取结果集注意这里不能依赖上一次的引用 var size new Number(parent_job.getVariable(size)); // 读取总数转成数值类型 var i new Number(parent_job.getVariable(i)) 1; // 读取当前计数并加 1因为第一行已处理过 if (i size) { // 还有下一行取出对应行的字段值覆盖变量 parent_job.setVariable(id, prevRow.get(i).getString(id, )); parent_job.setVariable(name, prevRow.get(i).getString(name, )); } parent_job.setVariable(i, i); // 无论是否还有下一行都要更新计数器 true;这段代码的精妙之处在于它总是返回 true。即使 i 已经大于等于 size它也返回 true——但这不代表循环会无限跑下去。因为当第 i 行不存在时id 和 name 变量不会再被更新下游转换处理的是最后一次有效数据而真正终止循环的机制在作业编排层面JavaScript 2 之后接的判断逻辑或者依赖 size 变量在后续作业项里被判断。不过这里有一个更稳妥的写法也是我实际项目里习惯的做法把返回值改成i size让作业在循环结束后自动走到“下一步”而不是继续重复。如果资源原版是恒返回 true那作业结构里必须有一个“条件判断”步骤检查 i 是否等于 size否则会死循环。搭建的时候建议顺手加一个“校验字段的值”或者“条件判断”步骤保险很多。3.4 转换 t1.ktr产出结果集的关键配置t1.ktr 在这个方案里是“数据源”。它可以是表输入、CSV 文件输入、甚至另一个转换的输出只要最终有行数据产出就行。但它有一个硬性要求必须以“作业里被调用”的方式执行并且输出行要能被 Job 捕获为结果集。在实际配置中我一般用“表输入”步骤查出一批 id 和 name然后直接连到“虚拟”步骤或者空操作步骤确保行流有终点。Kettle 在执行转换时如果转换是作业步骤调用的且没有勾选“不将结果集传递给作业”那么转换的输出行会自动写入作业的结果集缓冲区。注意这里有一个容易忽略的细节如果 t1.ktr 里加了“文本文件输出”之类的步骤行流会被消费掉结果集里可能拿不到数据。正确做法是让查询结果原样流入最后一个步骤不在这一步做输出或写入操作。字段命名也必须在 t1.ktr 里确认好。JavaScript 里用的是prevRow.get(i).getString(id)也就是说结果集的字段名必须叫 id 和 name。如果数据库里字段名是别的比如 USER_ID记得起别名。这个错误很隐蔽JavaScript 不会报错只会给你返回默认值空字符串下游文件里全是空内容排查起来非常费劲。3.5 转换 var.ktr消费变量并输出到本地文件var.ktr 这边要做的就是读取作业变量组装成想要的文本格式输出到 txt 文件。先看这一步的配置流程添加“获取变量”步骤Get Variables。在“字段”区域新增两行字段名填 id变量名填 id字段名填 name变量名填 name。类型都选 String。这个步骤的作用是把作业变量拉回到转换的行流里。添加“文本文件输出”步骤Text File Output。文件路径填一个本地路径比如/data/output/result.txt。分隔符按需设置如果一行只输出一个字段分隔符留空就行。把“获取变量”步骤连到“文本文件输出”步骤然后运行 var.ktr。但这里有一个新手必踩的坑直接单独运行 var.ktr 会看到 id 和 name 都是空的。因为“获取变量”步骤读的是本转换的变量空间而作业变量只在作业执行期间注入到转换里单独跑转换变量空间里根本没有这两个变量。所以调试 var.ktr 时要么在转换的属性里手动给这两个变量填默认值要么勾选“从父级作业获取变量”——后者只在作业中运行时才有效单独运行依然没有。我之前调试这类方案时踩过这个坑单独跑 var.ktr 输出文件是空的第一反应是“获取变量”步骤配错了反复改了几次都没用后来把整个作业跑起来才正常。从那以后我凡是调试跨转换变量传递都直接跑 Job不在转换层面纠结。3.6 作业级循环编排两个 JavaScript 步骤加一个转换怎么连最后把作业里的步骤串联起来。作业 j1.kjb 的步骤顺序是“转换”步骤指定 t1.ktr。这一步的连线不是普通流程线而是作业的“结果”连接。执行完毕后t1.ktr 的输出行进入作业结果集。“JavaScript 代码”步骤贴入 3.2 节的初始化代码。“转换”步骤指定 var.ktr。这是循环体每次执行就把当前 id 和 name 输出到文件。“JavaScript 代码”步骤贴入 3.3 节的循环推进代码。从步骤 4 拉一条线回到步骤 3构成循环。连线时选择“执行下一个作业项”即可JavaScript 步骤返回 true 就会继续执行下游步骤。整体流程就是先初始化并处理第一行 → 跑 var.ktr 输出第一行 → JavaScript 2 判断并更新下一行 → 再跑 var.ktr → 再判断……直到 i 超过 size最后一次 var.ktr 输出的其实是最后一行数据然后循环结束。这里有一个容易被忽略的细节因为 JavaScript 2 即使没有下一行也返回 true 并更新了 i所以最后一次循环会多执行一次 var.ktr。如果不想让最后一次重复输出可以把 JavaScript 2 改成var i new Number(parent_job.getVariable(i)) 1; if (i size) { parent_job.setVariable(id, prevRow.get(i).getString(id, )); parent_job.setVariable(name, prevRow.get(i).getString(name, )); parent_job.setVariable(i, i); true; // 有下一行才继续循环 } else { false; // 循环结束 }这样改的话最后一次循环体执行后就不会再触发下一次 var.ktr输出文件里的行数和结果集行数完全一致。4. 避坑指南这套方案在实际跑批中的六个高发问题4.1 prevRow 判空条件写错结果集为空时直接报错现象作业启动后第一个 JavaScript 步骤报Cannot read property size of null或类似的空指针异常作业终止。原因原资源代码里写的是prevRow null (prevRow.size() 0)两个条件用 AND 连接。如果 prevRow 为 null后面的prevRow.size()仍然会执行照样报错如果 prevRow 不为 null 但 size 为 0前面的 null 判断为 false整个条件不成立走进了 else 分支。正确逻辑是“要么为空要么没有行”都算空应该用 OR。解决把判断改成if (prevRow null || prevRow.size() 0)。同时建议在判空分支里加一个日志输出方便确认是“没结果”还是“结果为空”。这是第一个 JavaScript 步骤里最不值得踩的坑。4.2 循环计数器 i 的初始值和自增时机错位导致跳过首行或重复末行现象输出文件里第一行丢了或者最后一行出现了两次。原因如果 JavaScript 1 已经把第 0 行的 id 和 name 取出来并执行了第一次 var.ktr那 JavaScript 2 再自增时就应该直接跳到第 1 行。如果 JavaScript 2 在自增之后用prevRow.get(i)取值那取到的已经是 i1 行第 1 行就被跳过去了。相反如果自增放在取值之后最后一次循环会把最后一行再取一次。解决定一个规矩——JavaScript 1 负责处理第 0 行JavaScript 2 先用 i1 定位下一行再自增。这样每个循环体处理的行号分别是 0、1、2……正好和结果集行号对齐。我通常还会在 JavaScript 2 里把当前处理的行号写到日志里跑完对一下行数一眼就能看出有没有跳行。4.3 previous_result 在每次循环时不能重新获取而 JavaScript 2 里又调用了一次现象循环跑了几轮后取到的 id 和 name 全部变成空字符串或者抛异常提示结果集已被清除。原因Kettle 的 Job 结果集是有生命周期的。第一次 getRows() 取到全部行后有些版本里这个结果集已经被标记为“已读取”后续再次调用拿到的可能是空集合。但资源代码在 JavaScript 2 里又写了一次previous_result.getRows()这在某些 Kettle 版本里能拿到数据在另外一些版本里拿不到。解决最稳的做法是 JavaScript 1 里把结果集的行数、以及每一行的 id 和 name 全部存成带索引的变量比如id_0、id_1……JavaScript 2 只靠i变量和数组变量去取值。如果不想大改也可以把结果集存进parent_job.setVariable(tables, prevRow)后用tables.get(i)来取——注意变量里存的对象在 Job 生命周期内是保留的。如果这两种都嫌麻烦就用 JavaScript 1 初始化时把数据拆成两个数组变量存进去后续只碰数组。4.4 作业里跑了多个转换结果集串了现象var.ktr 输出的内容不是 t1.ktr 的结果而是上一个无关转换的输出。原因作业结果集是全局的前一个“转换”步骤的输出行会覆盖或追加到结果集里。如果作业里在 t1.ktr 之前还有一个查询步骤它的输出也会进入 previous_resultJavaScript 1 拿到的就是混合数据。同理如果作业里有两个并发的转换步骤结果集会互相污染。解决在 JavaScript 1 之前保证上一个作业项就是 t1.ktr中间不要插其他会产生输出的步骤。如果作业结构复杂可以考虑在 t1.ktr 跑完后先用“写日志”步骤打印行数确认一下再进 JavaScript。还有一个小技巧如果结果集必须复用可以用previous_result.getRows()只调用一次保存成变量后就不要再依赖 previous_result 了。4.5 下游转换拿到的变量是 null 或字符串 null现象var.ktr 的文本文件输出里出现了字面量 null而不是预期的 id 值。原因JavaScript 里parent_job.setVariable(id, prevRow.get(0).getString(id, ))已经把 id 设置成了空字符串这没问题。但如果 t1.ktr 的查询结果里 id 字段本身是空值getString 返回的是 null再存入变量后很多 Kettle 组件会把它显示为 null 字符串。另外如果 JavaScript 2 在某次循环里因为上一行已经取不到而对变量赋了 null也会出现这个现象。解决把默认值改成一个不可能出现的占位符或者干脆在这样的场景里用getString(id, )加上if (id null) return 的双保险。更彻底的做法是在 t1.ktr 的 SQL 里就写COALESCE(id, ) AS id从源头杜绝 null 进结果集。4.6 变量在转换里读取不到输出文件时间戳也没更新现象作业正常跑完状态是绿色成功但 txt 文件里是空的连上次的内容都没覆盖。原因var.ktr 里的“文本文件输出”步骤可能配置了“文件名包含日期”或者“追加模式”导致文件写到别的位置去了。另外如果“获取变量”步骤里变量名填错了转换行流里的 id 和 name 字段会一直为 null文本文件输出默认不写 null 字段文件自然就是空的。作业变量传递成功与否可以在 var.ktr 里加一个“写日志”步骤验证把 id 和 name 打印出来。这一步输出能看到变量值就能判断问题是出在传递环节还是输出环节。解决在 var.ktr 的“获取变量”步骤后面临时接一个“写日志”日志级别选“基本日志”运行作业后翻日志看有没有输出。有输出说明变量传递正常问题在文件输出配置没输出就回头检查变量名和 t1.ktr 的字段名。5. 验证这套方案是否正确日志、文件、行数三重确认5.1 第一重在作业里加行数比对先确认 t1.ktr 的结果集行数。最简单的方法是让 t1.ktr 的最后一步接一个“写日志”步骤输出行数加上一个“统计行数”步骤的计数。Kettle 里可以用“组最后一行”或者“统计”步骤来实现不过手写起来最快的方式是在 t1.ktr 中查询结果流里加一个“JavaScript 代码”步骤从结果集的getRows().size()拿到行数写入一个变量。然后在作业里做一次“变量值”的条件判断比对实际行数和size变量是否一致。如果实在不想拆分这么细还有一个省事办法在作业结束后用 shell 脚本数一下输出文件的非空行数再对比 t1.ktr 应该产出的行数。行数对不上就说明循环逻辑有跳行或重复。经验值是三次循环就能看出跳行规律比如只输出了奇数行说明自增写在了取值前面。5.2 第二重输出文件内容逐行核对文本文件输出的内容格式建议用“分隔符 每条记录一行”的方式便于人工核对。比如 id 和 name 中间用逗号分隔每一行是一条记录。文件里前几行和最后几行尤其重要前几行验证初始化的第 0 行有没有被正确处理最后几行验证循环结束条件是否正确。常见规律是输出行数大于结果集行数说明最后一次循环多跑了一次行列顺序颠倒了说明 i 的自增和取值顺序有问题某些行是全空说明结果集里有 null 字段。我一般会拿前 3 行、中间 1 行、后 3 行跟数据库里的数据做抽样对比三次抽样都一致整个方案的正确性就基本能确认了。5.3 第三重变量变化轨迹日志最让人头疼的是“变量是什么值”在黑匣子里看不到。给 JavaScript 1 和 JavaScript 2 各加一行日志转储就能破案// 在 JavaScript 2 末尾追加 var logLine i i , id parent_job.getVariable(id) , name parent_job.getVariable(name); // 把 logLine 写到系统日志里在 Kettle 的 JavaScript 步骤里可以直接用 Java 的日志对象org.pentaho.di.core.logging.LogChannel或者干脆用println()输出到控制台。更实用的是在 var.ktr 的“获取变量”步骤后面加一个“写日志”步骤把每一条进入转换的行打印出来。这样日志里就能完整看到每一次循环时 id 和 name 的值配合行号比对整条链路的正确性就一目了然。验证完了可以顺手把多余的“写日志”步骤删掉只保留输出逻辑避免影响生产环境日志量。这个方案我前前后后在几个项目里用过。最初照着资源源码搭的时候第一版就是把判空条件写错了作业一直报错后来逐行打印变量才定位到是和||的锅。从那以后我每次搭建跨转换的循环方案都会强制走一遍“先确认结果集行数 → 再确认变量值 → 最后确认输出文件”的验证流程三步都过了才敢让作业进调度里跑。希望这篇拆解能帮你少踩几个我当年踩过的坑照着搭一遍半小时内就能跑通。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进