使用Golang实现MongoDB带分页过滤器的问题排查
问题描述
我用Golang为MongoDB实现带分页的过滤器时遇到两个问题:
- 最初传递过滤器Map时触发错误:
the match filter must be an expression in an object
- 修改代码后,过滤器本应选中所有内容,但查询返回空结果且无报错。
相关代码片段如下:
初始getFilter代码
func (app *Courses) getFilter(filter *Filter) ([]bson.M, error) { pipeline := make([]bson.M, 0) if filter.All { // Include all items } else { // Filter items based on the provided criteria if filter.Beginner { pipeline = append(pipeline, bson.M{"tags": "beginner"}) } if filter.Advanced { pipeline = append(pipeline, bson.M{"tags": "advanced"}) } if filter.Go { pipeline = append(pipeline, bson.M{"tags": "go"}) } } return pipeline, nil }
初始Handler代码
func (app *Courses) CoursesAllHandler(w http.ResponseWriter, r *http.Request) { ctx := context.Background() clog := log.GetLoggerFromContext(ctx) p := r.URL.Query().Get("page") ps := r.URL.Query().Get("pageSize") var filter Filter err := json.NewDecoder(r.Body).Decode(&filter) if err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) return } pipeline := make([]bson.M, 0) page, _ := strconv.Atoi(p) pageSize, _ := strconv.Atoi(ps) // Pagination skip := (page - 1) * pageSize limit := pageSize // Add filter pipeline, err = app.getFilter(&filter) if err != nil { clog.Error(err) } pipeline = append(pipeline, bson.M{"$match": pipeline}) // Add pagination stages to the pipeline pipeline = append(pipeline, bson.M{"$skip": skip}) pipeline = append(pipeline, bson.M{"$limit": limit}) res, err := app.repo.GetAll(ctx, pipeline) if err != nil { clog.Error(err) return } err = app.helper.WriteJSON(w, http.StatusOK, envelope{"data": res, "metadata": "none"}, nil) if err != nil { clog.ErrorCtx(err, log.Ctx{ "header": w.Header(), "request_url": r.URL.String(), }) } }
修改后的Handler代码
func (app *Courses) CoursesAllHandler(w http.ResponseWriter, r *http.Request) { ctx := context.Background() clog := log.GetLoggerFromContext(ctx) var filter Filter err := json.NewDecoder(r.Body).Decode(&filter) if err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) return } filter.All = true pipeline := make([]bson.M, 3) // Add filter matches, err := app.getFilter(&filter) if err != nil { clog.Error(err) } pipeline[0] = bson.M{"$skip": 1} pipeline[1] = bson.M{"$limit": 5} pipeline[2] = bson.M{"$match": matches} res, err := app.repo.GetAll(ctx, pipeline) if err != nil { clog.Error(err) return } err = app.helper.WriteJSON(w, http.StatusOK, envelope{"data": res, "metadata": "none"}, nil) if err != nil { clog.ErrorCtx(err, log.Ctx{ "header": w.Header(), "request_url": r.URL.String(), }) } }
修改后的getFilter代码
func (app *Courses) getFilter(filter *Filter) (bson.M, error) { match := bson.M{} tags := []string{} if filter.All { // Include all items tags = append(tags, "beginner") tags = append(tags, "intermediate") ..... } else { // Filter items based on the provided criteria if filter.Beginner { tags = append(tags, "beginner") } if filter.Advanced { tags = append(tags, "advanced") } if filter.Go { tags = append(tags, "go") } ........ } match = bson.M{ "tags": bson.M{"$in": tags}, } return match, nil }
GetAll方法代码
func (r *CourseRepo) GetAll(ctx context.Context, pipeline []bson.M) ([]Course, error) { clog := log.GetLoggerFromContext(ctx) cur, err := r.collection.Aggregate(ctx, pipeline) ... }
问题根源
- 初始代码的$match参数错误:
pipeline = append(pipeline, bson.M{"$match": pipeline})把整个pipeline数组塞进$match条件,MongoDB要求$match的参数是单个bson对象,而非数组,因此触发第一个错误。 - 聚合管道顺序错误:修改后的代码把
$skip、$limit放在$match之前,导致先分页再过滤。如果前N条数据不符合过滤条件,就会返回空结果。 - 硬编码覆盖过滤参数:
filter.All = true强制覆盖了请求传入的过滤条件,逻辑不符合需求。 - 空tags数组的匹配问题:当filter.All为false且未勾选任何标签时,会生成
{"tags": {"$in": []}},此条件MongoDB会匹配不到任何数据。
修正方案
1. 修复getFilter函数
处理空tags数组的情况,filter.All为true时直接返回空匹配(匹配所有数据),无标签条件时不添加tags过滤:
func (app *Courses) getFilter(filter *Filter) (bson.M, error) { match := bson.M{} tags := []string{} if filter.All { // 匹配所有数据,无需添加任何条件 return match, nil } // 收集选中的标签条件 if filter.Beginner { tags = append(tags, "beginner") } if filter.Advanced { tags = append(tags, "advanced") } if filter.Go { tags = append(tags, "go") } // 仅当有标签条件时,添加$in匹配 if len(tags) > 0 { match["tags"] = bson.M{"$in": tags} } return match, nil }
2. 修复Handler函数
调整管道顺序:先执行$match过滤数据,再执行$skip和$limit分页;恢复分页参数的读取逻辑,去掉硬编码的filter.All = true:
func (app *Courses) CoursesAllHandler(w http.ResponseWriter, r *http.Request) { ctx := context.Background() clog := log.GetLoggerFromContext(ctx) // 读取分页参数,设置默认值避免page为0导致skip负数 page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } pageSize, _ := strconv.Atoi(r.URL.Query().Get("pageSize")) if pageSize < 1 { pageSize = 10 } skip := (page - 1) * pageSize // 解析请求中的过滤参数 var filter Filter err := json.NewDecoder(r.Body).Decode(&filter) if err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) return } // 获取过滤条件 matchStage, err := app.getFilter(&filter) if err != nil { clog.Error(err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // 构建正确的聚合管道:先过滤,再分页 pipeline := []bson.M{} if len(matchStage) > 0 { pipeline = append(pipeline, bson.M{"$match": matchStage}) } pipeline = append(pipeline, bson.M{"$skip": skip}, bson.M{"$limit": pageSize}, ) // 执行查询 res, err := app.repo.GetAll(ctx, pipeline) if err != nil { clog.Error(err) http.Error(w, "Failed to fetch courses", http.StatusInternalServerError) return } // 返回结果,补充分页元数据 metadata := map[string]int{ "page": page, "pageSize": pageSize, "count": len(res), } err = app.helper.WriteJSON(w, http.StatusOK, envelope{"data": res, "metadata": metadata}, nil) if err != nil { clog.ErrorCtx(err, log.Ctx{ "header": w.Header(), "request_url": r.URL.String(), }) } }
3. 可选:添加总条数统计(优化分页体验)
如果需要返回符合条件的总数据条数给前端,可使用$facet阶段同时获取分页数据和总数:
// 构建带总数统计的管道 pipeline := []bson.M{} if len(matchStage) > 0 { pipeline = append(pipeline, bson.M{"$match": matchStage}) } pipeline = append(pipeline, bson.M{ "$facet": bson.M{ "data": []bson.M{ {"$skip": skip}, {"$limit": pageSize}, }, "total": []bson.M{ {"$count": "count"}, }, }, })
需要对应调整GetAll方法的返回结构,解析data和total字段后再返回给前端。
内容的提问来源于stack exchange,提问作者Mike3355
相关产品推荐
相关产品推荐

