Canal ES Adapter pkVal 为 null 问题解决方案

2025-12-04 0 455

Canal ES Adapter pkVal 为 null 问题解决方案

问题描述

在使用 Canal ES Adapter 同步数据到 Elasticsearch 时,执行 UPDATE 操作时出现以下错误

java.lang.RuntimeException: java.lang.NullPointerException: Cannot invoke \"Object.toString()\" because \"pkVal\" is null

错误发生在 ESSyncService.java 类的 singleTableSimpleFiledUpdate 方法中,调用 esTemplate.getESDataFromDmlData() 方法时返回了 null

错误日志

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

问题发生位置

错误发生在以下调用链:

  1. ESSyncService.javasingleTableSimpleFiledUpdate 方法
  2. ES7xTemplate.javagetESDataFromDmlData 方法(带 owner 参数的重载版本)

源代码分析

1. ESSyncService.java – singleTableSimpleFiledUpdate 方法

/**
 * 单表简单字段update
 *
 * @param config es配置
 * @param dml dml信息
 * @param data 单行data数据
 * @param old 单行old数据
 */
private void singleTableSimpleFiledUpdate(ESSyncConfig config, String owner, Dml dml, Map data,
                                          Map old) {
    ESMapping mapping = config.getEsMapping();
    Map 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 行):

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 参数)

@Override
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map dmlData,
                                   Map dmlOld, Map 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 参数)

@Override
public Object getESDataFromDmlData(ESMapping mapping, Map dmlData,
                                   Map 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 使用的是这个方法)。

问题根本原因

原因分析

  1. SQL 配置问题

    • 原始 SQL:SELECT id as _id, ... FROM shop_order(没有表别名)
    • 对于单表查询,schemaItem.getMainTable().getAlias() 返回 null
    • columnItem.getOwner() 也是 null(因为没有表别名)
  2. 代码逻辑问题

    • getESDataFromDmlData 方法(带 owner 参数)在第 316 行检查:
      if (columnItem.getOwner() == null || columnItem.getColumnName() == null) {
          continue;  // 跳过所有 owner 为 null 的字段
      }
      
    • 对于没有表别名的 SQL,所有字段的 columnItem.getOwner() 都是 null,因此所有字段都被跳过
    • 主键字段也被跳过,resultIdVal 始终为 null
  3. 为什么 INSERT 正常,UPDATE 失败

    • INSERT 操作:使用不带 owner 参数的重载方法(第 283-306 行),不检查 owner
    • UPDATE 操作:使用带 owner 参数的方法(第 308-337 行),需要 owner 匹配

问题流程图

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

修改前(有问题的配置)
# 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>={}\"
修改后(正确的配置)
# 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>={}\"
关键修改点
  1. SQL 中添加表别名FROM shop_orderFROM shop_order a
  2. 所有字段添加表别名前缀ida.idorder_noa.order_no,等等
  3. 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.javagetESDataFromDmlData 方法中添加对 ownernull 的处理:

@Override
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map dmlData,
                                   Map dmlOld, Map 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. 修改配置文件:按照方案 1 修改 shop_order.yml
  2. 重启 Canal Adapter:使配置生效
  3. 执行 UPDATE 操作:在数据库中更新 shop_order 表的记录
  4. 检查日志:应该不再出现 pkVal is null 的错误
  5. 验证 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.java
    • client-adapter/es7x/src/main/java/com/alibaba/otter/canal/client/adapter/es7x/support/ES7xTemplate.java

总结

  • 问题:UPDATE 操作时 pkValnull,导致 NullPointerException
  • 原因:单表查询没有表别名,导致 ownernull,所有字段被跳过
  • 解决:在 SQL 配置中给表添加别名,确保 owner 不为 null
  • 最佳实践:Canal ES Adapter 的 SQL 配置中,即使是单表查询,也建议使用表别名,避免类似问题

参考

  • Canal ES Adapter 官方文档
  • Canal GitHub: github.com/alibaba/can…

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 Canal ES Adapter pkVal 为 null 问题解决方案 https://www.zuozi.net/3687.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务