How to insert a document with mgo and get the value returned

WagnerMatosUK Source

For the record, I'm learning Go. I'm trying to use and the mgo package and I'd like to insert a new document and return this newly created document to user (I'm trying to write a basic API). I've wrote the following code:

EDIT: Here's the struct for the model:

type Book struct {
  ISBN    string   `json:"isbn"`
  Title   string   `json:"title"`
  Authors []string `json:"authors"`
  Price   string   `json:"price"`

session := s.Copy()
defer session.Close()

var book Book
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&book)
if err != nil {
    ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)

c := session.DB("store").C("books")

info, err := c.Upsert(nil, book)

if err != nil {
    ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
log.Println("Failed insert book: ", err)

respBody, err := json.MarshalIndent(info, "", "  ")
if err != nil {

ResponseWithJSON(w, respBody, http.StatusOK)

Please note that Book is a struct I have created earlier. The above code does work but what it returns is the upsert result like so:

    "Updated": 1,
    "Removed": 0,
    "Matched": 1,
    "UpsertedId": null

Which is not the recently created object. How can I get the the recently created object to return as a response (please note that ideally I'd like the confirmation that the document was successfully inserted. I have seen other questions where the ID is generated beforehand but for what I've seen it doesn't confirm that the document was created was it?)



answered 6 months ago icza #1

Let's clear the concepts first. In MongoDB, each document must have an _id property which acts as its unique document identifier inside a collection. Either you provide the value of this _id or it is assigned automatically by MongoDB.

So it would be ideal (or it's strongly recommended) for your model types to include a field for the _id document identifier. Since we're talking about books here, books already have a unique identifier called ISBN, which you may opt to use as the value of the _id field.

The mapping between MongoDB fields and Go struct fields must be specified using the bson tag (not json). So you should provide bson tag values along with json tags.

So change your model to:

type Book struct {
  ISBN    string   `json:"isbn" bson:"_id"`
  Title   string   `json:"title" bson:"title"`
  Authors []string `json:"authors" bson:"authors"`
  Price   string   `json:"price" bson:"price"`

If you want to insert a new document (a new book), you should always use Collection.Insert().

And what will be the ID of the newly inserted document? The field you set to the Book.ISBN field as we declared it to be the document ID with the bson:"_id" tag.

You should only use Collection.Upsert() if you are not sure whether the document already exists, but either way you want it to be the document you have at hand. Collection.Upsert() will try to find a document to update, and if one is found, that will be updated. If no document is found, then an insert operation will be performed. The first parameter is the selector to find the document to be updated. Since you passed nil, that means any document may qualify, so one will be selected "randomly". So if you already have books saved, any may get selected and get overwritten. This is certainly not want you want.

Since now the ISBN is the ID, you should specify a selector that filters by ISBN, like this:

info, err := c.Upsert(bson.M{"_id": book.ISBN}, book)

Or since we're filtering by ID, use the more convenient Collection.UpsertId():

info, err := c.UpsertId(book.ISBN, book)

If you want to update an existing document, for that you may use Collection.Update(). This is similar to Collection.Upsert(), but the difference is that if no documents match the selector, an insert will not be performed. Updating a document matched by ID can also be done with the more convenient Collection.UpdateId() (which is analogue to Collection.UpsertId()).

For other documents which do not have a unique identifier naturally (like books having ISBN), you may use generated IDs. The mgo library provides the bson.NewObjectId() function for such purpose, which returns you a value of type bson.ObjectId. When saving new documents with Collection.Insert(), you can acquire a new unique ID with bson.NewObjectId() and assign it to the struct field that is mapped to the MongoDB _id property. If the insert succeeds, you can be sure the document's ID is what you just set before calling Collection.Insert(). This ID generation is designed to work even in a distributed environment, so it will generate unique IDs even if 2 of your nodes attempt to generate an ID at the same time.

So for example if you don't have the ISBN for a book when saving it, then you must have a separate, designated ID field in your Book type, for example:

type Book struct {
  ID      bson.ObjectId `bson:"_id"`
  ISBN    string        `json:"isbn" bson:"isbn"`
  Title   string        `json:"title" bson:"title"`
  Authors []string      `json:"authors" bson:"authors"`
  Price   string        `json:"price" bson:"price"`

And when saving a new book:

var book Book
// Fill what you have about the book
book.ID = bson.NewObjectId()

c := session.DB("store").C("books")
err = c.Insert(book)
// check error
// If no error, you can refer to this document via book.ID

comments powered by Disqus