Fiber/MongoDB中复用模型并移除响应字段的技术问题
问题描述
我希望避免冗余代码,尽量不重复声明模型。目前遇到两个核心问题:
- 第47行使用现有
user模型执行userCollection.FindOne(ctx, filter, opts).Decode(&user)时,第46行设置的投影选项(排除password字段)未生效,模型没有更新,必须声明新的var user2 = models.User并解码到user2才能生效。 - 如果使用新的
user2模型,JSON响应里的password字段会显示为null,能不能不新建模型就完全移除这个字段?我试过创建不带password字段的UserResponse模型,也在CreateUser函数里声明过第二个模型来查看FindOne带选项的输出。
原代码
func CreateUser(c *fiber.Ctx) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var user models.User //validate the request body if err := c.BodyParser(&user); err != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": err.Error()}}) } //use the validator library to validate required fields if validationErr := validate.Struct(&user); validationErr != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": validationErr.Error()}}) } var email = &user.Email count, err := userCollection.CountDocuments(ctx, bson.M{"email": email}) if err != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": "Something went wrong"}}) } if count > 0 { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": "Email already in use"}}) } //set the status, hash password, set activate token and updated at status := 0 password := hashPassword(*user.Password) activateToken := uuid.New().String() updatedAt, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) //create user object user.ID = primitive.NewObjectID() user.Password = &password user.Status = &status user.ResetToken = &activateToken user.CreatedAt = updatedAt user.UpdatedAt = updatedAt result, err := userCollection.InsertOne(ctx, user) if err != nil { return c.Status(http.StatusInternalServerError).JSON(responses.UserResponse{Status: http.StatusInternalServerError, Message: "error", Data: &fiber.Map{"data": err.Error()}}) } //get created user from the DB and cast it into UserResponse model filter := bson.M{"_id": result.InsertedID} opts := options.FindOne().SetProjection(bson.M{"password": 0}) userCollection.FindOne(ctx, filter, opts).Decode(&user) //return created user return c.Status(http.StatusOK).JSON(responses.UserResponse{Status: http.StatusOK, Message: "success", Data: &fiber.Map{"data": user}}) }
解决方案
问题1:投影选项解码到现有模型不生效
MongoDB的Decode方法只会覆盖数据库返回的字段,不会清空原有字段。你之前的user变量已经存储了Password的值,投影排除password后数据库不会返回该字段,所以Decode不会修改原有变量里的Password,导致看起来投影没生效。
解决办法:不需要新建模型,在Decode前把user重置为零值即可:
// 重置user为零值,清除原有字段数据 user = models.User{} userCollection.FindOne(ctx, filter, opts).Decode(&user)
问题2:移除JSON响应中的password字段(无需新建模型)
要让JSON序列化时完全移除password字段(而非显示null),有两种实用方法:
方法1:修改模型结构体标签
给models.User的Password字段添加omitempty标签,当字段为零值(比如nil)时,JSON序列化会自动忽略它:
type User struct { // 其他字段保持不变 Password *string `bson:"password" json:"password,omitempty"` }
结合问题1的重置操作,Decode后Password会是nil,序列化时就不会出现这个字段。
方法2:序列化前手动清空字段
如果不想修改模型标签,可以在返回响应前手动把Password设为nil:
userCollection.FindOne(ctx, filter, opts).Decode(&user) user.Password = nil // 清空后,带omitempty标签会被忽略,无标签则用下面的方法
如果没有omitempty标签,还可以用fiber.Map手动构建响应数据,只保留需要的字段:
return c.Status(http.StatusOK).JSON(responses.UserResponse{ Status: http.StatusOK, Message: "success", Data: &fiber.Map{ "data": fiber.Map{ "id": user.ID, "email": user.Email, "status": user.Status, "reset_token": user.ResetToken, "created_at": user.CreatedAt, "updated_at": user.UpdatedAt, // 只添加需要的字段,主动排除password }, }, })
优化后的完整代码
func CreateUser(c *fiber.Ctx) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var user models.User //validate the request body if err := c.BodyParser(&user); err != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": err.Error()}}) } //use the validator library to validate required fields if validationErr := validate.Struct(&user); validationErr != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": validationErr.Error()}}) } var email = &user.Email count, err := userCollection.CountDocuments(ctx, bson.M{"email": email}) if err != nil { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": "Something went wrong"}}) } if count > 0 { return c.Status(http.StatusBadRequest).JSON(responses.UserResponse{Status: http.StatusBadRequest, Message: "error", Data: &fiber.Map{"data": "Email already in use"}}) } //set the status, hash password, set activate token and updated at status := 0 password := hashPassword(*user.Password) activateToken := uuid.New().String() updatedAt, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) //create user object user.ID = primitive.NewObjectID() user.Password = &password user.Status = &status user.ResetToken = &activateToken user.CreatedAt = updatedAt user.UpdatedAt = updatedAt result, err := userCollection.InsertOne(ctx, user) if err != nil { return c.Status(http.StatusInternalServerError).JSON(responses.UserResponse{Status: http.StatusInternalServerError, Message: "error", Data: &fiber.Map{"data": err.Error()}}) } //get created user from the DB filter := bson.M{"_id": result.InsertedID} opts := options.FindOne().SetProjection(bson.M{"password": 0}) // 重置user为零值,避免原有字段干扰 user = models.User{} if err := userCollection.FindOne(ctx, filter, opts).Decode(&user); err != nil { return c.Status(http.StatusInternalServerError).JSON(responses.UserResponse{Status: http.StatusInternalServerError, Message: "error", Data: &fiber.Map{"data": err.Error()}}) } //return created user return c.Status(http.StatusOK).JSON(responses.UserResponse{Status: http.StatusOK, Message: "success", Data: &fiber.Map{"data": user}}) }
内容的提问来源于stack exchange,提问作者shimmy
相关产品推荐
相关产品推荐

