MongoDB问题Map key a.b contains dots but no replacement was configured解决

问题

业务人员在使用我最近全职维护的数据产品时,反馈的一个问题。

这个功能模块,支持各种类型的数据源,如:Hive,MySQL,SQL Server,Oracle,ClickHouse,Impala等。支持多段SQL,多段SQL子句以英文分号分隔,且最后一个SQL子句必须是select查询子句;select查询,即形成所谓的数据集。最后生成一个定时任务,xxl-job定时调度执行SQL,更新数据集。页面上可以点击按钮【读取数据】执行多段SQL,然后可以点击按钮【预览数据】最多预览30行记录。后台实现中,读取数据会把数据集,即SQL执行结果储存到MongoDB数据库。

读取数据,执行SQL,报错信息如下:Map key t.自然日 contains dots but no replacement was configured! Make sure map keys don't contain dots in the first place or configure an appropriate replacement

业务反馈的问题SQL比较复杂,经过简化后的SQL如下:

drop table if exists test.test_hive_field;

create table test.test_hive_field as
select
100 as userscore,
'031818' as user_id,
'2022-05-18' as dt;

select
tt.userscore,
tt.user_id as userId,
tt.dt 
from (
select * from test.test_hive_field t limit 1
) tt;

由于是生产问题,并且ELK日志找不到可用可靠的错误日志,走很多弯路,包括尝试在堡垒机里调试定位问题。事实上最简单的路径应该是,找到可用的测试环境的数据源,在本地开发测试环境下复现,然后断点调试问题。并且,没有第一时间去百度,Google,浪费不少时间,兜了不少圈子。

题外话,在使用测试环境hive数据源,遇到另一个报错:

org.apache.hadoop.util.Shell [<clinit>:694] Did not find winutils.exe: {}
java.io.FileNotFoundException: java.io.FileNotFoundException: HADOOP_HOME and hadoop.home.dir are unset. -see https://wiki.apache.org/hadoop/WindowsProblems

后来终于可以在本地调试代码:
在这里插入图片描述
可以看到,问题根源在于:数据insert到MongoDB集合时报错。

数据长啥样呢?
在这里插入图片描述
查询结果字段为全小写,有 dot 符号。

解决

既然已经能够通过调试来定位问题,并且能知道有问题的代码出现在那一行,那解决问题就不远。

Google搜索得到比较靠谱的mongodb-escape-dots-in-map-key。意思很明显,就是:.以及$符号是MongoDB保留关键词,字段命名时不能含有这些符号。

MongoDB的模版方法模式MongoTemplate配置如下:

MongoDatabaseFactory mongoDbFactory = new SimpleMongoClientDatabaseFactory(mongoUri.getUri());
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
MongoMappingContext mongoMappingContext = new MongoMappingContext();
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
mongoConverter.setMapKeyDotReplacement(".");
mongoConverter.with(mongoDbFactory);
mongodbMap.put(mongodbNameTemp, new MongoTemplate(mongoDbFactory, mongoConverter));

注,Spring-data-mongodb版本号为:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
  <version>2.3.4.RELEASE</version>
</dependency>

结果还是不行,报错信息如下:Invalid BSON field name tt.dt

mongo-database-invalid-bson-field-name-exception
意思是配置MappingMongoConverter行不通,只能把点去掉,下面这行代码简直nonsense,doing-nothing,用.来替换(设置,set)字段里面的.

mongoConverter.setMapKeyDotReplacement(".");

替换成:

mongoConverter.setMapKeyDotReplacement("-");

解决问题,但是这意味着存储到MongoDB数据库的字段名,已经不是最初SQL里面指定的查询字段名,而变成如下样式:
在这里插入图片描述
解决方法就是在从MongoDB取数时,再转换一下,将-转换成.。后面就没有继续往下钻研。

大致思路如上。

解决思路二

之所以有思路二,是因为我们支持各种不用的SQL方言,上面出现问题的数据源是Hive,具体来说,其URL为:jdbc:hive2://100.200.300.400:10000/edw

作为背景,需要知道的是,仅仅只是hive数据源有上面这个问题,其他数据源,则没有这个问题。

有一个impala数据源,其URL为:jdbc:hive2://impala.pddcorp.com:25005,有类似的SQL:

select
tt.guaranteeremainingamount,
tt.user_id as userId,
tt.dt 
from (
select * from cdr.hnsx_baihang_change t limit 10
) tt

一直以来没有发现问题,调试截图如下:
在这里插入图片描述
查询结果字段全小写,没有 dot 符号。故而数据insert到MongoDB,不会报错。

所以解决思路在hive数据源的查询结果的特殊处理上:

String columnLabel = metaData.getColumnLabel(j);
if (columnLabel.contains(".") && this.datasourceJson.getString(Constant.JDBC_URL).contains("10000")) {
    columnLabel = columnLabel.substring(columnLabel.indexOf(".") + 1);
}

如果是10000端口,就表示是hive数据源;如果含有.,则截图后面的字段。

问题,为什么impala和hive数据源使用同一个hive-jdbc,查询字段不一样?

注:
使用的hive-jdbc版本为:

<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-jdbc</artifactId>
    <version>2.1.1-cdh6.2.1</version>
</dependency>

备注

代码片段,一直想不明白,为什么不用@Component注解配置类,要写成Util工具类:

@Slf4j
public class MongodbHashCluster {
    private final SortedMap<Long, String> vNodes = new TreeMap<>();
    private final Map<String, MongoTemplate> mongodbMap = new HashMap<>();
    public MongodbHashCluster(MongoUriList mongoUriList) {
        log.info("获取配置:" + mongoUriList);
        if (!CollectionUtils.isEmpty(mongoUriList.getList())) {
            int k = 0;
            int DEFAULT_VNODE_NUM = 150;
            for (MongoUriList.MongoUri mongoUri : mongoUriList.getList()) {
                String mongodbNameTemp = mongoUri.getName();
                if (mongodbNameTemp == null) {
                    mongodbNameTemp = "mongodb_" + ++k;
                }
                log.info("获取mongodb配置:" + mongodbNameTemp);
                // 核心配置
                MongoDatabaseFactory mongoDbFactory = new SimpleMongoClientDatabaseFactory(mongoUri.getUri());
                DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory);
                MongoMappingContext mongoMappingContext = new MongoMappingContext();
                MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
                mongoConverter.setMapKeyDotReplacement(".");
                mongoConverter.with(mongoDbFactory);
                mongodbMap.put(mongodbNameTemp, new MongoTemplate(mongoDbFactory, mongoConverter));
                for (int i = 0; i < DEFAULT_VNODE_NUM; i++) {
                    long hashCode = hash2(mongodbNameTemp + "_vnode_" + i);
                    vNodes.put(hashCode, mongodbNameTemp);
                }
            }
        }
    }

    private String get(String key) {
        long hashCode = hash2(key);
        SortedMap<Long, String> subMap = vNodes.tailMap(hashCode);
        if (!subMap.isEmpty()) {
            return subMap.get(subMap.firstKey());
        }
        return vNodes.get(vNodes.firstKey());
    }

    public MongoTemplate getMongoTemplate(String key) {
        try {
            String mongoTemplateName = get(key);
            return mongodbMap.get(mongoTemplateName);
        } catch (Exception e) {
            log.error("get mongoTemplate error", e);
        }
        return null;
    }

    /**
     *  MurMurHash算法,是非加密HASH算法,性能很高,
     *  比传统的CRC32, MD5, SHA-1(这两个算法都是加密HASH算法,复杂度本身就很高,带来的性能上的损害也不可避免)
     *  等HASH算法要快很多,而且据说这个算法的碰撞率很低.
     *  http://murmurhash.googlepages.com/
     */
    public static Long hash2(String key) {
        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;
        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);
        long m = 0xc6a4a7935bd1e995L;
        int r = 47;
        long h = seed ^ (buf.remaining() * m);
        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();
            k *= m;
            k ^= k >>> r;
            k *= m;
            h ^= k;
            h *= m;
        }
        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(
                    ByteOrder.LITTLE_ENDIAN);
            // for big-endian version, do this first:
            // finish.position(8-buf.remaining());
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }
        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;
        buf.order(byteOrder);
        return h;
    }
}

参考

mongodb-escape-dots-in-map-key
mongo-database-invalid-bson-field-name-exception

posted @ 2022-05-20 23:48  johnny233  阅读(121)  评论(0编辑  收藏  举报  来源