Mongoose子文档返回undefined问题排查与解决
reviews返回undefined问题 问题现象
基于《Getting MEAN》搭建的Express应用,调用GET /api/locations/:locationid/reviews/:reviewid接口时,已成功查询到目标location文档,但location.reviews返回undefined,导致if(location.reviews && location.reviews.length>0)判断失败,接口返回错误信息。
相关代码与输出
MongoDB文档
{ "_id": { "$oid": "65537eb149dd64103a4b7950" }, "name": "Starcups", "address": "125 High Street, Reading, RG6 1PS", "rating": 3, "facilities": ["Hot drinks", "Food", "Premium wifi"], "coords": [-0.9690884, 51.455041], "openingTimes": [ { "days": "Monday - Friday", "opening": "7:00am", "closing": "7:00pm", "closed": false }, { "days": "Saturday", "opening": "8:00am", "closing": "5:00pm", "closed": false }, { "days": "Sunday", "closed": true } ], "reviews": [ { "author": "Lekan Yusuf", "id": { "$oid": "6560c0dfadc556388f6b8e93" }, "rating": 4, "timestamp": { "$date": "1992-06-11T23:00:00Z" }, "reviewText": "Good wifi, moderate coffee" }, { "author": "Lekan Yusuf", "id": { "$oid": "6563c6c0d8b148207dcc9817" }, "rating": 4, "timestamp": { "$date": "1992-06-11T23:00:00Z" }, "reviewText": "Good wifi, moderate coffee" } ] }
路由配置
var express = require('express'); var router = express.Router(); var ctrlReviews =require('../controllers/reviews'); //reviews routes router.get('/locations/:locationid/reviews/:reviewid', ctrlReviews.reviewsReadOne); router.post('locations/:locationid/reviews', ctrlReviews.createReview); router.put('locations/:locationid/reviews/reviewid', ctrlReviews.reviewsUpdateOne); router.delete('locations/:locationid/reviews/reviewid', ctrlReviews.reviewsDeleteOne);
控制器核心代码
module.exports.reviewsReadOne = function(req, res){ console.log('in ReviewsReadOne'); if(req.params && req.params.locationid && req.params.reviewid){ console.log('locationid: '+ req.query.locationid + ' reviewid: '+ req.query.reviewid); Loc.findById(req.query.locationid, 'name reviews').exec( function(err,location){ var response, review; if(!location){ sendJsonResponse(res, 404, {"message": "locationid not found"}); return; } else if (err){ sendJsonResponse(res, 404, err); return; } console.log('location just before check:\n' + location); console.log('\n reviews: '+ location.reviews); if(location.reviews && location.reviews.length >0){ review = location.reviews.id(req.params.reviewid); if(!review){ sendJsonResponse(res, 400, {"message":"Review id not found"}); } else { response ={ location:{ name: location.name, id: req.query.id }, review:review }; sendJsonResponse(res, 200, response); } } else { sendJsonResponse(res, 400, {message:"Not found, locationid and reviewid are both required"}); } } ); } else{ sendJsonResponse(res, 400, {"message": "No location id and or review id"}); return; } }
终端输出
in ReviewsReadOne locationid: 65538583fc3b3ac2c0b83a8d reviewid: 6563c74141216ec9d9ec7c38 location just before check: { _id: new ObjectId("65538583fc3b3ac2c0b83a8d"), name: 'Starfi', reviews: [ { author: 'Subomi ogunderu', id: new ObjectId("6563c74141216ec9d9ec7c38"), rating: 3, timestamp: 1995-08-11T23:00:00.000Z, reviewText: 'Excellent wifi and the food is great' } ] } reviews: undefined GET /api/locations/locationid/reviews/reviewid?locationid=65538583fc3b3ac2c0b83a8d&reviewid=6563c74141216ec9d9ec7c38 400 566.482 ms - 66
问题原因
路径参数与查询参数混用
路由定义的是路径参数/:locationid/:reviewid,但请求时错误使用查询参数?locationid=xxx&reviewid=yyy,同时控制器中混用req.query(查询参数)和req.params(路径参数)获取ID,导致逻辑混乱。虽然偶然查到了location文档,但Mongoose Document对象的属性访问规则受到影响。Mongoose Document属性访问限制
打印location时显示的是文档的JSON序列化结果,但location本身是Mongoose的Document实例,若Schema未正确定义reviews为子文档数组,直接访问location.reviews会返回undefined。路由配置错误
POST/PUT/DELETE路由缺少开头的/,且PUT/DELETE的reviewid未定义为路径参数,会导致路由无法匹配。
解决方案
1. 修正请求URL格式
按照路由定义,使用路径参数传递ID,正确请求URL示例:
GET /api/locations/65538583fc3b3ac2c0b83a8d/reviews/6563c74141216ec9d9ec7c38
2. 修正控制器参数逻辑
统一使用req.params获取路径参数,同时可将Mongoose Document转为普通JS对象避免属性访问问题:
module.exports.reviewsReadOne = function(req, res){ console.log('in ReviewsReadOne'); if(req.params && req.params.locationid && req.params.reviewid){ console.log('locationid: '+ req.params.locationid + ' reviewid: '+ req.params.reviewid); Loc.findById(req.params.locationid, 'name reviews').exec( function(err,location){ var response, review; if(!location){ sendJsonResponse(res, 404, {"message": "locationid not found"}); return; } else if (err){ sendJsonResponse(res, 404, err); return; } // 转为普通JS对象,避免Mongoose Document属性访问限制 const locationObj = location.toObject(); console.log('location just before check:\n', locationObj); console.log('\n reviews: ', locationObj.reviews); if(locationObj.reviews && locationObj.reviews.length >0){ // 子文档数组的id方法需要使用原Document对象 review = location.reviews.id(req.params.reviewid); if(!review){ sendJsonResponse(res, 400, {"message":"Review id not found"}); } else { response ={ location:{ name: location.name, id: location._id // 使用文档自身的_id,而非req.query.id }, review: review.toObject() // 转为普通对象返回 }; sendJsonResponse(res, 200, response); } } else { sendJsonResponse(res, 400, {message:"No reviews found for this location"}); } } ); } else{ sendJsonResponse(res, 400, {"message": "No location id and or review id provided in URL path"}); return; } }
3. 确保Location Schema正确定义子文档数组
检查并修正Location模型的Schema,确保reviews被定义为子文档数组:
const mongoose = require('mongoose'); const reviewSchema = new mongoose.Schema({ author: String, rating: Number, timestamp: Date, reviewText: String }); const locationSchema = new mongoose.Schema({ name: String, address: String, rating: Number, facilities: [String], coords: [Number], openingTimes: [{ days: String, opening: String, closing: String, closed: Boolean }], reviews: [reviewSchema] // 必须明确定义为子文档数组 }); module.exports = mongoose.model('Loc', locationSchema);
4. 修正其他路由的路径错误
补充开头的/,并将PUT/DELETE的reviewid定义为路径参数:
//reviews routes router.get('/locations/:locationid/reviews/:reviewid', ctrlReviews.reviewsReadOne); router.post('/locations/:locationid/reviews', ctrlReviews.createReview); router.put('/locations/:locationid/reviews/:reviewid', ctrlReviews.reviewsUpdateOne); router.delete('/locations/:locationid/reviews/:reviewid', ctrlReviews.reviewsDeleteOne);
验证效果
修改完成后,使用正确的路径参数请求接口,终端中location.reviews将正常返回数组,接口会返回对应的review数据。
内容的提问来源于stack exchange,提问作者Femi

