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

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

问题发生位置

错误发生在以下调用链:

  1. ESSyncService.java - singleTableSimpleFiledUpdate 方法
  2. 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 使用的是这个方法)。

问题根本原因

原因分析

  1. SQL 配置问题

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

    • getESDataFromDmlData 方法(带 owner 参数)在第 316 行检查:

      java 复制代码
      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 匹配

问题流程图

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>={}"
关键修改点
  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 的处理:

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. 修改配置文件 :按照方案 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 配置中,即使是单表查询,也建议使用表别名,避免类似问题

参考

相关推荐
掘金者阿豪2 小时前
用 Rust 构建 Git 提交历史可视化工具
后端
大头an2 小时前
深入理解Spring核心原理:Bean作用域、生命周期与自动配置完全指南
java·后端
LucianaiB2 小时前
安利一个全栈开发神器:WeaveFox 帮你5分钟生成完整的全栈Web应用
后端
戴誉杰3 小时前
idea 2025.2 重置试用30天,无限期使用
java·ide·intellij-idea
小坏讲微服务3 小时前
Spring Cloud Alibaba 2025.0.0 整合 ELK 实现日志
运维·后端·elk·spring cloud·jenkins
IT_陈寒3 小时前
JavaScript性能优化:10个V8引擎隐藏技巧让你的代码快30%
前端·人工智能·后端
rannn_1114 小时前
【Javaweb学习|黑马笔记|Day5】Web后端基础|java操作数据库
数据库·后端·学习·javaweb
无限进步_4 小时前
C语言atoi函数实现详解:从基础到优化
c语言·开发语言·c++·git·后端·github·visual studio