You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB 3.6升级至6.0.5后replaceRoot聚合报错求助

问题排查:MongoDB 3.6升级到6.0.5后筛选功能报错

问题背景

将MongoDB从3.6版本升级到6.0.5后,应用部分筛选功能无法正常工作,触发如下错误:

MongoDB\Driver\Exception\CommandException: PlanExecutor error during aggregation :: caused by :: 'newRoot' expression must evaluate to an object, but resulting value was: ",{"_id":{"$oid":"644b54928b59d02b7225ee1b"},"hotelCode":"0001","hotelId":[{"test":"0001"}],"name":"ROSE DUBAI","address1":"metro station","location":"","latitude":25.25424,"longitude":55.30114,"phone":"971-4-3558355","thumbNailUrl":"/images/?im in /vendor/mongodb/mongodb/src/Operation/Aggregate.php:219

提供的PHP代码

$map_js_string = "function () {
                var selected_room_types = {$selected_room_types};
                var selected_meal_bases = {$selected_meal_bases};
                var selected_supplier   = {$selected_supplier};
                var selected_refund_value = {$selected_refund_value};
                var matching_room_count = 0;
                var moreDelete = true;
                while (moreDelete) {
                    moreDelete = false;
                    var suppliers = Object.keys(this.HotelProperty);
                    for (var supplier_index = 0; !moreDelete && supplier_index < suppliers.length; supplier_index++) {
                        if (this.HotelProperty.hasOwnProperty(suppliers[supplier_index])) {
                            for (var index = 0; !moreDelete && index < this.HotelProperty[suppliers[supplier_index]].length; index++) {
                                for (var room_index = 0; !moreDelete && undefined !== this.HotelProperty[suppliers[supplier_index]][index] && undefined !== this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates && room_index < this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates.length; room_index++) {
                                    var room_category = this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates[room_index].room_category;
                                    var meal_basis = this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates[room_index].meal_basis;
                                    if(undefined !== this.HotelProperty[suppliers[supplier_index]][index].RateInfo.refund_policy) {
                                        var refund_policy = this.HotelProperty[suppliers[supplier_index]][index].RateInfo.refund_policy;
                                    } else {
                                        var refund_policy = '';
                                    }
                                    var source_type = this.HotelProperty[suppliers[supplier_index]][index].RateInfo.supplier_data.sourceType;
                                    if (typeof meal_basis == 'string' && typeof room_category == 'string' && typeof source_type == 'string') {
                                        meal_basis = meal_basis.replace(/&/, 'and');
                                        meal_basis = meal_basis.replace(/\\|/, '');
                                        meal_basis = meal_basis.replace(/&lt;br&gt;/i, '');
                                        meal_basis = meal_basis.replace(/[\\r\\n]/g, '');
                                        if ((selected_meal_bases.length == 0 || selected_meal_bases.indexOf(meal_basis.toLowerCase()) != -1) && (selected_room_types.length == 0 || selected_room_types.indexOf(room_category.toLowerCase()) != -1) && (selected_supplier.length == 0 || selected_supplier.indexOf(source_type.toLowerCase()) != -1) && (selected_refund_value.length == 0 || selected_refund_value.indexOf(refund_policy.toLowerCase()) != -1)) {
                                            matching_room_count++;
                                        } else {
                                            // delete the room
                                            this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates.splice(room_index, 1);
                                            if (this.HotelProperty[suppliers[supplier_index]][index].RateInfo.room_rates.length == 0) {
                                                this.HotelProperty[suppliers[supplier_index]].splice(index, 1);
                                                if (this.HotelProperty[suppliers[supplier_index]].length == 0) {
                                                    if (Array.isArray(this.HotelProperty)) {
                                                        this.HotelProperty.splice(suppliers[supplier_index], 1);
                                                    } else {
                                                        delete this.HotelProperty[suppliers[supplier_index]];
                                                    }
                                                }
                                            }
                                            // break loop and start over
                                            moreDelete = true;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                if (matching_room_count > 0) {
                    emit(this._id, this);
                }
            }";
            
          
$map_js_function = new MongoDB\BSON\Javascript($map_js_string);

$reduce_js_string = "function(key, val_arr) {
                var joined = '';
                for(var i in val_arr) {
                    joined = joined + ',' + JSON.stringify(val_arr[i]);
                }
                return joined;
            }";
$reduce_js_function = new MongoDB\BSON\Javascript($reduce_js_string);
             
$mr_status = $dbnew->command(array(
    "mapreduce" => $temp_paging_collection,
    "map" => $map_js_function,
    "reduce" => $reduce_js_function,
    "out" => $mapreduce_result,
));
unset($mr_status);

$array_agree = array(
    array('$replaceRoot' => array('newRoot' => '$value')),
    array('$sort' => $sorting),
    array('$out' => $paging_collection),
);

$out = $dbnew->$mapreduce_result->aggregate($array_agree, $options);

问题原因分析

错误提示明确指出newRoot需要是对象,但实际得到的是带前置逗号的JSON拼接字符串。问题根源在reduce函数:

  • map函数中emit(this._id, this)输出的是MongoDB文档对象
  • 但reduce函数把多个值转成JSON字符串拼接,导致mapreduce结果集合中的value字段是字符串而非对象
  • MongoDB 6.0.5对类型检查比3.6更严格,直接拒绝了将字符串作为newRoot的操作

解决思路

1. 修复reduce函数,保证返回类型一致

reduce函数的返回值类型必须和map函数emit的value类型完全一致(这里是对象)。根据业务逻辑,同一个_id应该只会被emit一次,因此可以直接返回数组第一个元素:

function(key, val_arr) {
    // 同一key仅对应一个文档,直接返回第一个元素
    return val_arr[0];
}

如果存在同一key对应多个文档的场景,需要编写逻辑正确合并对象,而非拼接字符串。

2. 长期方案:用聚合管道替代MapReduce

MongoDB 6.0已将MapReduce标记为废弃功能,官方推荐使用聚合管道(Aggregation Pipeline)替代。聚合管道通过原生操作符(如$filter、$addFields、$project等)实现逻辑,性能更好、兼容性更强,也避免后续版本的兼容性问题。

例如,可以将map中的筛选逻辑用聚合管道重写:

  • 用$addFields处理HotelProperty的嵌套筛选
  • 用$filter过滤不符合条件的room_rates、供应商数据
  • 用$match保留有匹配房间的文档

内容的提问来源于stack exchange,提问作者sunil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 16:44:59