Search code examples
mongodbgoobjectid

Inserting a document into MongoDB wthout _id field seems bugged


I might have found a weird bug.. I do update once in a while my mongodb-driver package as I use for year now I layer I've made a long time ago.

However, today, for some personal reasons, I have refactored my tests and now have e2e full tests suite over my layer. The thing is, while doing my tests, I could reproduce a bug I had in the past, which is when I don't set an ID for my document (_id bson field)

Situation:

For obvious reason, some piece of code are missing but I am quite sure you will be able to guess really easily ;)

The commented out code part is the tests that are creating the weird behavior

  • If I create a document (really simple struct) and set the _id field before inserting, it works perfectly.
  • If I create a document (really simple struct) and don't set the _id field before inserting, it inserts something that results as "null" while observing the database using Robo3T.

Code:

To reproduce that bug, there is my tests and the source code of my layer

Tests:

type e2eMongoDBSuite struct {
    suite.Suite
    _mongodb mongodb.IMongoDB
}

type e2eTestDocument struct {
    ID         *primitive.ObjectID `json:"_id" bson:"_id"`
    ValueStr   string              `json:"value_str" bson:"value_str"`
    ValueInt32 int32               `json:"value_int32" bson:"value_int32"`
}

const (
    //// ci
    connectionString = "mongodb://localhost:27017"
    databaseName     = "mongodb-database-e2e-tests"
    collection       = "mongodb-collection-e2e-tests"
    defaultTimeout   = 5 * time.Second
)

func (e2eSuite *e2eMongoDBSuite) SetupSuite() {
    var err error
    if e2eSuite._mongodb, err = mongodb.New(&mongodb.Config{
        ConnectionString: connectionString,
        DatabaseName:     databaseName,
        DefaultTimeout:   defaultTimeout,
    }); err != nil {
        panic(fmt.Errorf("couldn't setup suite: %w", err).Error())
    }
}

func (e2eSuite *e2eMongoDBSuite) TestInsertOneWithID_OK() {
    a := assert.New(e2eSuite.T())

    // given
    docID := e2eSuite._mongodb.GenerateNewObjectID()
    doc := &e2eTestDocument{ID: docID}

    // when
    insertedID, err := e2eSuite._mongodb.InsertOne(collection, doc)

    // expected
    a.NotNil(insertedID, "the inserted document ID should be returned")
    a.Equal(docID, insertedID, "the inserted document ID should be the same as the given one at creation")
    a.Nil(err, "there should be no error for this insert")

    // then
    _ = e2eSuite._mongodb.DeleteMany(collection, bson.D{})
}

//func (e2eSuite *e2eMongoDBSuite) TestInsertOneWithNoID_OK() {
//  a := assert.New(e2eSuite.T())
//
//  // given
//  doc := &e2eTestDocument{}
//
//  // when
//  insertedID, err := e2eSuite._mongodb.InsertOne(collection, doc)
//
//  // expected
//  a.NotNil(insertedID, "the inserted document ID should be returned")
//  //a.Equal(docID, insertedID, "the inserted document ID should be the same as the given one at creation")
//  a.Nil(err, "there should be no error for this insert")
//
//   // then
//   _ = e2eSuite._mongodb.DeleteMany(collection, bson.D{})
//}

func (e2eSuite *e2eMongoDBSuite) TestInsertManyWithIDs_OK() {
    a := assert.New(e2eSuite.T())

    // given
    docID1 := e2eSuite._mongodb.GenerateNewObjectID()
    docID2 := e2eSuite._mongodb.GenerateNewObjectID()
    doc1 := &e2eTestDocument{ID: docID1}
    doc2 := &e2eTestDocument{ID: docID2}

    // when
    insertedIDs, err := e2eSuite._mongodb.InsertMany(collection, []*e2eTestDocument{doc1, doc2})

    // expected
    a.NotNil(insertedIDs, "the inserted document IDs should be returned")
    a.Equal(2, len(insertedIDs), "the inserted document IDs amount should be of two")
    a.EqualValues([]*primitive.ObjectID{docID1, docID2}, insertedIDs, "the inserted document IDs should be the same as the given ones at creation")
    a.Nil(err, "there should be no error for this insert")

    // then
    _ = e2eSuite._mongodb.DeleteMany(collection, bson.D{})
}

//func (e2eSuite *e2eMongoDBSuite) TestInsertManyWithNoIDs_OK() {
//  a := assert.New(e2eSuite.T())
//
//  // given
//  doc1 := &e2eTestDocument{}
//  doc2 := &e2eTestDocument{}
//
//  // when
//  insertedIDs, err := e2eSuite._mongodb.InsertMany(collection, []*e2eTestDocument{doc1, doc2})
//
//  // expected
//  a.NotNil(insertedIDs, "the inserted document IDs should be returned")
//  a.Equal(2, len(insertedIDs), "the inserted document IDs amount should be of two")
//  a.Nil(err, "there should be no error for this insert")
//
//// then
//_ = e2eSuite._mongodb.DeleteMany(collection, bson.D{})
//}

func (e2eSuite *e2eMongoDBSuite) TearDownSuite() {
    _ = e2eSuite._mongodb.DropDatabase()
}

func TestE2eMongoDBSuite(t *testing.T) {
    suite.Run(t, new(e2eMongoDBSuite))
}

The layer i've developed:

// InsertOne inserts a document in a given collection and returns the inserted ObjectID.
func (m *mongoDB) InsertOne(collectionName string, document interface{}) (*primitive.ObjectID, error) {
    res, err := m.GetDatabase().Collection(collectionName).InsertOne(context.Background(), document)
    if err != nil {
        return nil, err
    }
    if oid, ok := res.InsertedID.(primitive.ObjectID); ok {
        return &oid, nil
    }
    return nil, nil
}

// InsertMany inserts documents in a given collection and returns the inserted ObjectIDs.
func (m *mongoDB) InsertMany(collectionName string, documents interface{}) ([]*primitive.ObjectID, error) {

    s := reflect.ValueOf(documents)
    if s.Kind() != reflect.Slice {
        panic("Documents given a non-slice type")
    }

    slice := make([]interface{}, s.Len())
    for i := 0; i < s.Len(); i++ {
        slice[i] = s.Index(i).Interface()
    }

    res, err := m.GetDatabase().Collection(collectionName).InsertMany(context.Background(), slice)
    if err != nil {
        return nil, err
    }

    var insertedIDs []*primitive.ObjectID
    for _, insertedID := range res.InsertedIDs {
        if oid, ok := insertedID.(primitive.ObjectID); ok {
            insertedIDs = append(insertedIDs, &oid)
        }
    }

    return insertedIDs, nil
}

Conclusion

I don't know if the behavior is logic or not but an id should be generated if my document as no ID :confused:

Note

A topic as been opened here: https://developer.mongodb.com/community/forums/t/inserting-a-document-in-go-with-no-id-set-results-into-a-weird-behavior/100359 (don't know if it's a bug or not so I thought I should post there too)

Thanks!

Max


Solution

  • If you insert a document containing the _id field, mongodb will use that as the document id, even if it is null. To have the _id auto-generated, you have to insert the document without the _id field. This should work:

    type Doc struct {
      ID         *primitive.ObjectID `json:"_id" bson:"_id,omitempty"`
      ...
    

    This will not marshal the _id field if it is nil, focing it to be auto-generated.