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

使用Golang实现MongoDB带分页过滤器的问题排查

问题描述

我用Golang为MongoDB实现带分页的过滤器时遇到两个问题:

  1. 最初传递过滤器Map时触发错误:
the match filter must be an expression in an object 
  1. 修改代码后,过滤器本应选中所有内容,但查询返回空结果且无报错。

相关代码片段如下:

初始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)
    ...
}

问题根源
  1. 初始代码的$match参数错误:pipeline = append(pipeline, bson.M{"$match": pipeline}) 把整个pipeline数组塞进$match条件,MongoDB要求$match的参数是单个bson对象,而非数组,因此触发第一个错误。
  2. 聚合管道顺序错误:修改后的代码把$skip、$limit放在$match之前,导致先分页再过滤。如果前N条数据不符合过滤条件,就会返回空结果。
  3. 硬编码覆盖过滤参数:filter.All = true强制覆盖了请求传入的过滤条件,逻辑不符合需求。
  4. 空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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 00:43:14