Canal ES Adapter pkVal 为 null 问题解决方案
问题描述
在使用 Canal ES Adapter 同步数据到 Elasticsearch 时,执行 UPDATE 操作时出现以下错误:
csharp
java.lang.RuntimeException: java.lang.NullPointerException: Cannot invoke "Object.toString()" because "pkVal" is null
错误发生在 ESSyncService.java 类的 singleTableSimpleFiledUpdate 方法中,调用 esTemplate.getESDataFromDmlData() 方法时返回了 null。
错误日志
kotlin
2025-11-17 13:08:13.129 [pool-3-thread-1] ERROR c.a.o.canal.client.adapter.es.core.service.ESSyncService - sync error, es index: shop_order, DML : Dml{destination='example', database='kenanai', table='shop_order', type='UPDATE', es=1763356092000, ts=1763356093118, sql='', data=[{id=180, order_no=test123131313, ...}], old=[{order_no=ORD20251117125806655, update_time=2025-11-17 12:58:06.0}]}
2025-11-17 13:08:13.132 [pool-3-thread-1] ERROR c.a.otter.canal.adapter.launcher.loader.AdapterProcessor - java.lang.NullPointerException: Cannot invoke "Object.toString()" because "pkVal" is null
java.lang.RuntimeException: java.lang.NullPointerException: Cannot invoke "Object.toString()" because "pkVal" is null
问题发生位置
错误发生在以下调用链:
- ESSyncService.java -
singleTableSimpleFiledUpdate方法 - ES7xTemplate.java -
getESDataFromDmlData方法(带 owner 参数的重载版本)
源代码分析
1. ESSyncService.java - singleTableSimpleFiledUpdate 方法
java
/**
* 单表简单字段update
*
* @param config es配置
* @param dml dml信息
* @param data 单行data数据
* @param old 单行old数据
*/
private void singleTableSimpleFiledUpdate(ESSyncConfig config, String owner, Dml dml, Map<String, Object> data,
Map<String, Object> old) {
ESMapping mapping = config.getEsMapping();
Map<String, Object> esFieldData = new LinkedHashMap<>();
// ⚠️ 问题发生在这里:idVal 返回 null
Object idVal = esTemplate.getESDataFromDmlData(mapping, owner, data, old, esFieldData);
if (logger.isTraceEnabled()) {
logger.trace("Main table update to es index, destination:{}, table: {}, index: {}, id: {}",
config.getDestination(),
dml.getTable(),
mapping.getIndex(),
idVal);
}
// ⚠️ 这里调用 update 时,idVal 为 null,导致后续 toString() 抛出 NullPointerException
esTemplate.update(mapping, idVal, esFieldData);
}
调用位置(第 208 行和第 262 行):
java
if (schemaItem.getAliasTableItems().size() == 1 && schemaItem.isAllFieldsSimple()) {
// ------单表 & 所有字段都为简单字段------
singleTableSimpleFiledUpdate(config, schemaItem.getMainTable().getAlias(), dml, data, old);
}
注意:owner 参数传入的是 schemaItem.getMainTable().getAlias(),对于没有表别名的 SQL,这个值会是 null。
2. ES7xTemplate.java - getESDataFromDmlData 方法(带 owner 参数)
java
@Override
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map<String, Object> dmlData,
Map<String, Object> dmlOld, Map<String, Object> esFieldData) {
SchemaItem schemaItem = mapping.getSchemaItem();
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
Object resultIdVal = null;
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
// ⚠️ 问题根源 1:如果 columnItem.getOwner() 为 null,直接跳过
if (columnItem.getOwner() == null || columnItem.getColumnName() == null) {
continue;
}
// ⚠️ 问题根源 2:如果 owner 不匹配,也跳过
if (!columnItem.getOwner().equals(owner)) {
continue;
}
String columnName = columnItem.getColumnName();
if (fieldItem.getFieldName().equals(idFieldName)) {
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
}
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
}
}
// 添加父子文档关联信息
putRelationData(mapping, schemaItem, dmlOld, esFieldData);
return resultIdVal; // ⚠️ 如果所有字段都被跳过,这里返回 null
}
3. ES7xTemplate.java - getESDataFromDmlData 方法(不带 owner 参数)
java
@Override
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData,
Map<String, Object> esFieldData) {
SchemaItem schemaItem = mapping.getSchemaItem();
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
Object resultIdVal = null;
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
Object value = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
if (fieldItem.getFieldName().equals(idFieldName)) {
resultIdVal = value;
}
if (!fieldItem.getFieldName().equals(mapping.getId())
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
}
}
// 添加父子文档关联信息
putRelationData(mapping, schemaItem, dmlData, esFieldData);
return resultIdVal;
}
注意 :这个不带 owner 参数的方法不检查 owner,所以 INSERT 操作正常(INSERT 使用的是这个方法)。
问题根本原因
原因分析
-
SQL 配置问题:
- 原始 SQL:
SELECT id as _id, ... FROM shop_order(没有表别名) - 对于单表查询,
schemaItem.getMainTable().getAlias()返回null columnItem.getOwner()也是null(因为没有表别名)
- 原始 SQL:
-
代码逻辑问题:
-
getESDataFromDmlData方法(带 owner 参数)在第 316 行检查:javaif (columnItem.getOwner() == null || columnItem.getColumnName() == null) { continue; // 跳过所有 owner 为 null 的字段 } -
对于没有表别名的 SQL,所有字段的
columnItem.getOwner()都是null,因此所有字段都被跳过 -
主键字段也被跳过,
resultIdVal始终为null
-
-
为什么 INSERT 正常,UPDATE 失败:
- INSERT 操作 :使用不带
owner参数的重载方法(第 283-306 行),不检查owner - UPDATE 操作 :使用带
owner参数的方法(第 308-337 行),需要owner匹配
- INSERT 操作 :使用不带
问题流程图
kotlin
UPDATE 操作
↓
singleTableSimpleFiledUpdate(config, schemaItem.getMainTable().getAlias(), ...)
↓
owner = schemaItem.getMainTable().getAlias() // 对于 "SELECT id FROM shop_order",返回 null
↓
getESDataFromDmlData(mapping, owner=null, data, old, esFieldData)
↓
遍历字段:
columnItem.getOwner() == null // 因为没有表别名
↓
if (columnItem.getOwner() == null) continue; // 跳过所有字段
↓
resultIdVal = null // 主键字段也被跳过
↓
return null
↓
esTemplate.update(mapping, null, esFieldData)
↓
pkVal.toString() // NullPointerException
解决方案
方案 1:给 SQL 添加表别名(推荐)
在 SQL 配置中给表添加别名,这样 owner 就不会是 null。
修改前(有问题的配置)
yaml
# client-adapter/launcher/src/main/resources/es7/shop_order.yml
dataSourceKey: defaultDS
destination: example
groupId: g1
esMapping:
_index: shop_order
_id: _id
pk: id
sql: "SELECT id as _id, order_no, user_id, ... FROM shop_order"
commitBatch: 3000
etlCondition: "where create_time>={}"
修改后(正确的配置)
yaml
# client-adapter/launcher/src/main/resources/es7/shop_order.yml
dataSourceKey: defaultDS
destination: example
groupId: g1
esMapping:
_index: shop_order
_id: _id
pk: id
sql: "SELECT a.id as _id, a.order_no, a.user_id, a.total_amount, a.pay_amount, a.freight_amount, a.pay_type, a.source_type, a.status, a.receiver_name, a.receiver_phone, a.receiver_address, a.note, a.payment_time, a.delivery_time, a.receive_time, a.comment_time, a.create_time, a.update_time, a.phone, a.nickename, a.buyer_id, a.buyer_type, a.seller_id, a.seller_type, a.identifier, a.item_count, a.item_price, a.order_closed_time, a.goods_id, a.pay_streamId, a.close_type, a.pay_stream_id FROM shop_order a"
commitBatch: 3000
etlCondition: "where a.create_time>={}"
关键修改点
- SQL 中添加表别名 :
FROM shop_order→FROM shop_order a - 所有字段添加表别名前缀 :
id→a.id,order_no→a.order_no,等等 - etlCondition 中添加表别名 :
where create_time>={}→where a.create_time>={}
修改后的效果
schemaItem.getMainTable().getAlias()返回"a"(不再是null)columnItem.getOwner()也是"a"columnItem.getOwner().equals(owner)匹配成功- 主键字段不会被跳过,
resultIdVal可以正确获取
方案 2:修改 Canal 源码(不推荐)
如果需要修改 Canal 源码,可以在 ES7xTemplate.java 的 getESDataFromDmlData 方法中添加对 owner 为 null 的处理:
java
@Override
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map<String, Object> dmlData,
Map<String, Object> dmlOld, Map<String, Object> esFieldData) {
SchemaItem schemaItem = mapping.getSchemaItem();
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
Object resultIdVal = null;
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
// 修改:当 owner 为 null 时,允许 columnItem.getOwner() 也为 null
if (columnItem.getColumnName() == null) {
continue;
}
// 修改:处理 owner 为 null 的情况
if (owner != null) {
if (columnItem.getOwner() == null || !columnItem.getOwner().equals(owner)) {
continue;
}
} else {
// owner 为 null 时,只处理 columnItem.getOwner() 也为 null 的字段
if (columnItem.getOwner() != null) {
continue;
}
}
String columnName = columnItem.getColumnName();
if (fieldItem.getFieldName().equals(idFieldName)) {
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
}
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
}
}
putRelationData(mapping, schemaItem, dmlOld, esFieldData);
return resultIdVal;
}
注意 :修改源码需要重新编译 Canal,且升级 Canal 版本时可能会丢失修改,不推荐使用此方案。
验证步骤
- 修改配置文件 :按照方案 1 修改
shop_order.yml - 重启 Canal Adapter:使配置生效
- 执行 UPDATE 操作 :在数据库中更新
shop_order表的记录 - 检查日志 :应该不再出现
pkVal is null的错误 - 验证 ES 数据:检查 Elasticsearch 中的数据是否正确更新
相关文件路径
- 配置文件 :
client-adapter/launcher/src/main/resources/es7/shop_order.yml - 源码文件 :
client-adapter/escore/src/main/java/com/alibaba/otter/canal/client/adapter/es/core/service/ESSyncService.javaclient-adapter/es7x/src/main/java/com/alibaba/otter/canal/client/adapter/es7x/support/ES7xTemplate.java
总结
- 问题 :UPDATE 操作时
pkVal为null,导致NullPointerException - 原因 :单表查询没有表别名,导致
owner为null,所有字段被跳过 - 解决 :在 SQL 配置中给表添加别名,确保
owner不为null - 最佳实践:Canal ES Adapter 的 SQL 配置中,即使是单表查询,也建议使用表别名,避免类似问题
参考
- Canal ES Adapter 官方文档
- Canal GitHub: github.com/alibaba/can...