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

Mongoose更新子文档数组遇ConflictingUpdateOperators错误求解决方案

问题:更新MongoDB子文档rooms时出现冲突错误

错误详情

MongoServerError: Updating the path 'rooms.$.updatedAt' would create a conflict at 'rooms.$'

Postman返回的错误信息:

{
    "ok": 0,
    "code": 40,
    "codeName": "ConflictingUpdateOperators",
    "$clusterTime": {
        "clusterTime": {
            "$timestamp": "7137698220989218823"
        },
        "signature": {
            "hash": "34spx6E0zZFa5bVYFSL2JyjFszQ=",
            "keyId": {
                "low": 2,
                "high": 1646669662,
                "unsigned": false
            }
        }
    },
    "operationTime": {
        "$timestamp": "7137698220989218823"
    }
}

相关代码

Property模型

import { IReview, ReviewSchema } from './reviews.model'
import { IRoom, RoomSchema } from './room.model'

type TCheapestRoom = {
    roomType: string
    bedType: string
    lastPrice: number
    actualPrice: number
}

export interface IProperty {
    propertyType: string
    name: string
    description: string
    city: string
    photos: [string]
    cheapestRoom: TCheapestRoom
    adress: string
    distance: number
    cancellationPolicy: string
    meals: string
    funThingsToDo: [string]
    accessibility: [string]
    rooms: IRoom[]
    reviews: IReview[]
}

interface IPropertyDocument extends IProperty, Document {
    createdAt: Date
    updatedAt: Date
}

const PropertySchema = new Schema<IPropertyDocument>(
    {
        propertyType: { type: String, required: true },
        name: { type: String, required: true },
        description: { type: String, required: true },
        city: { type: String, required: true },
        photos: { type: [String] },
        cheapestRoom: {
            roomType: { type: String },
            bedType: { type: String },
            lastPrice: { type: Number },
            actualPrice: { type: Number },
        },
        adress: { type: String, required: true },
        distance: { type: Number, required: true },
        cancellationPolicy: { type: String, required: true },
        meals: { type: String, required: true },
        funThingsToDo: { type: [String] },
        accessibility: { type: [String] },
        rooms: [RoomSchema],
        reviews: [ReviewSchema],
    },
    {
        timestamps: true,
    }
)

const PropertyModel = model<IPropertyDocument>('Property', PropertySchema)
export default PropertyModel

Room子文档模型

export interface IRoom {
    roomType: string
    bedTypes: [string]
    roomFacilities: [string]
    sleeps: number
    lastPrice: number
    actualPrice: number
    cancellation: string
    payment: string
    breakfast: string
    unavailableDates: [Date]
}

export interface IRoomDocument extends IRoom, Document {
    createdAt: Date
    updatedAt: Date
}

export const RoomSchema = new Schema<IRoomDocument>(
    {
        roomType: { type: String, required: true },
        bedTypes: { type: [String], required: true },
        roomFacilities: { type: [String], required: true },
        sleeps: { type: Number, required: true, default: 2 },
        lastPrice: { type: Number, required: true },
        actualPrice: { type: Number, required: true },
        cancellation: { type: String, required: true },
        payment: { type: String, required: true },
        breakfast: { type: String, required: true },
        unavailableDates: { type: [Date] },
    },
    {
        timestamps: true,
    }
)

const RoomModel = model<IRoomDocument>('Room', RoomModel)
export default RoomModel

Room控制器代码

import PropertyModel from '../models/property.model'
import RoomModel from '../models/room.model'
const updateRoom = async (req: Request, res: Response) => {
    try {
        const updatedProperty = await PropertyModel.findOneAndUpdate(
            { _id: req.params.propertyId, 'rooms._id': req.params.roomId },
            {
                $set: { 'rooms.$': req.body },
            },
            {
                new: true,
            }
        )
        res.status(201).json(updatedProperty)
    } catch (error) {
        console.log(error)
        res.status(400).json(error)
    }
}

export { addNewRoom, deleteRoom, updateRoom }

解决方案

错误原因

RoomSchema开启了timestamps: true,当使用$set: {'rooms.$': req.body}替换整个子文档时,Mongoose会自动尝试更新该子文档的updatedAt字段,这与“替换整个子文档”的操作产生冲突,触发ConflictingUpdateOperators错误(代码40)。

解决办法

方法1:原子更新子文档的指定字段(推荐)

不替换整个子文档,而是逐个更新请求体中的字段,同时手动更新updatedAt:

const updateRoom = async (req: Request, res: Response) => {
    try {
        const updateObj: Record<string, any> = {};
        // 遍历请求体字段,构建子文档字段的更新路径
        Object.keys(req.body).forEach(key => {
            updateObj[`rooms.$.${key}`] = req.body[key];
        });
        // 手动更新子文档的updatedAt
        updateObj['rooms.$.updatedAt'] = new Date();

        const updatedProperty = await PropertyModel.findOneAndUpdate(
            { _id: req.params.propertyId, 'rooms._id': req.params.roomId },
            { $set: updateObj },
            { new: true, runValidators: true }
        );

        if (!updatedProperty) {
            return res.status(404).json({ message: 'Property or Room not found' });
        }
        res.status(200).json(updatedProperty);
    } catch (error) {
        console.log(error);
        res.status(400).json(error);
    }
};

这种方式利用MongoDB的原子更新操作,避免并发问题,同时保留子文档的时间戳更新逻辑。

方法2:先查询再合并更新

如果需要替换大部分字段,可以先查询出目标文档,合并原有子文档字段和请求体内容后再保存:

const updateRoom = async (req: Request, res: Response) => {
    try {
        const property = await PropertyModel.findOne(
            { _id: req.params.propertyId, 'rooms._id': req.params.roomId }
        );

        if (!property) {
            return res.status(404).json({ message: 'Property or Room not found' });
        }

        const roomIndex = property.rooms.findIndex(
            r => r._id.toString() === req.params.roomId
        );
        if (roomIndex === -1) {
            return res.status(404).json({ message: 'Room not found' });
        }

        // 合并字段:保留原有_id、createdAt,覆盖请求体字段,更新updatedAt
        property.rooms[roomIndex] = {
            ...property.rooms[roomIndex].toObject(),
            ...req.body,
            updatedAt: new Date()
        };

        const updatedProperty = await property.save();
        res.status(200).json(updatedProperty);
    } catch (error) {
        console.log(error);
        res.status(400).json(error);
    }
};

注意:这种方式存在并发更新风险,若多个请求同时修改同一个文档,可能导致数据覆盖。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 15:12:49