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(/<br>/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
相关产品推荐
相关产品推荐

