Mongo nested struct with omitempty

60 Views Asked by At

Consider the following 2 structs:

type Struct1 struct {
  foo string `bson:"foo,omitempty"`
  bar string `bson:"bar,omitempty"`
  Struct2 *struct2 `bson:"struct2,omitempty"`
}

type Struct2 struct {
  foo string `bson:"foo,omitempty"`
  bar string `bson:"bar,omitempty"`
}

Using FindOneAndUpdate and only setting values for foo in the parent and foo in the nested struct has the following behavior. It retains the existing value for the struct1 bar in the db document, great, but deletes the bar value nested in the struct2 object, not great. Is this by design?

If I add:

Struct2 *struct2 `bson:"struct2,omitempty,inline"`

It works fine, but I lose the structure of my document in Mongo which is not ideal.

EDIT -- Adding minimum reproducable example:

package main

import (
    "context"
    "fmt"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
    // Connect to mongo
    mongoClient, err := mongo.Connect(context.TODO(), options.Client().ApplyURI("mongodb://***:***@localhost:27017/test?connect=direct"))
    if err != nil {
        return
    }
    db := mongoClient.Database("test")
    coll := db.Collection("test")

    // Insert example record
    id, err := coll.InsertOne(context.TODO(), bson.D{
        {Key: "foo", Value: "foo_value"},
        {Key: "bar", Value: "bar_value"},
        {Key: "nestedObject", Value: bson.D{
            {Key: "foo", Value: "foo_value"}, 
            {Key: "bar", Value: "bar_value"},
        },
    }})
    if err != nil {
        fmt.Printf("Error: %v", err)
    }

    // Grab example record and print
    result := &bson.D{}
    coll.FindOne(context.TODO(), bson.M{"_id": id.InsertedID}).Decode(result)
    fmt.Printf("Initial DB record -> %v\n", result)

    // Update record, return SingleResult after and print
    aft := options.After
    opt := options.FindOneAndUpdateOptions{
        ReturnDocument: &aft,
    }

    // Just changing foo, not bar
    newResult := &bson.D{}
    if err := coll.FindOneAndUpdate(context.TODO(), bson.M{"_id": id.InsertedID},  bson.D{{Key: "$set", Value: bson.D{
        {Key: "foo", Value: "foo_changed"},
        {Key: "nestedObject", Value: bson.D{
            {Key: "foo", Value: "foo_changed"},
        },
    }}}}, &opt).Decode(newResult); err != nil {
        fmt.Printf("Error: %v", err)
    }
    fmt.Printf("Updated DB record -> %v\n", newResult)

}

Outputs the following, where did my nestedObject.bar go??? (note the non nested one is still there):

Initial DB record -> &[{_id ObjectID("65d8f675a5418eef83425156")} {foo foo_value} {bar bar_value} {nestedObject [{foo foo_value} {bar bar_value}]}]
Updated DB record -> &[{_id ObjectID("65d8f675a5418eef83425156")} {foo foo_changed} {bar bar_value} {nestedObject [{foo foo_changed}]}]

This explains why using ",inline" on the bson works fine, because the bson.D is flattened before updating.

The question is, is it possible to retain the nested structure and have mongo only update the provided values?

1

There are 1 best solutions below

1
On

Your update operation sets the nestedObject field to an object having a single foo property. That's why its bar field is gone.

If you just want to change foo inside the nested object nestedObject, use the "dot" notation:

// Just changing foo, not bar
newResult := &bson.D{}
if err := coll.FindOneAndUpdate(context.TODO(),
    bson.M{"_id": id.InsertedID},
    bson.D{{Key: "$set", Value: bson.D{
        {Key: "foo", Value: "foo_changed"},
        {Key: "nestedObject.foo", Value: "foo_changed"},
    }}}, &opt).Decode(newResult); err != nil {
    fmt.Printf("Error: %v", err)
}