Inanc Gumus
Inanc Gumus

Reputation: 27889

Customize mgo upsert operation

I've a game analytics rest API which stores the average performance statistics of the players. When a new statistic arrives, I want to update the existing game record in Mongodb by merging the new delta onto the existing document. I'm storing the past analytics data as well. So that, I can return data like the player's stats are decreasing or increasing since the game's last update.

The problem is: When I want to upsert my new game data into Mongodb with mgo, it overwrites all of a player's stats array. Actually, this is expected. I know how to fix it if I can modify my document that mgo tries to upsert into Mongodb.

Question: How can I customize mgo upsert behaviour? So that I can add a $push operator in front of Player.Stats to prevent Mongodb erasing the stats array inside the document.

My Real Question: It doesn't matter which Mongo commands I'm going to use. I'll figure it out somehow. What I actually want to know is: How can I customize the behaviour of mgo before upsert?

Some Solutions: I've tried some solutions myself before. Like, encoding/decoding Game struct into bson.M to customize it. However, I found it cumbersome and messy. If there's no other way, I'd use it.

Blocks: I don't want to hand-write all of my structs fields with bson.M, just to use a $push operator on one field. Because there are dozens of fields, that would be error-prone and will increase my code complexity.


Example:

// Assume that, this is an existing game in Mongodb:
existingGame := Game{
    ID: 1,
    Name: "Existing game",
    // The game has just one player
    Players: []Player{
        // The player has some stats. The newest one is 2.0.
        {1, "foo", []{3.5, 2.0}},
    }
}

// This is a new request coming to my API
// I want to upsert this into the existing Game
newGame := Game{
    ID: 1,
    Players: []Player{
        // As expectedly, this will reset player foo's stats to 5.0
        //
        // After upserting, I want it to be as: 
        //
        // []{3.5, 2.0, 5.0}
        //
        // in Mongodb
        {1, "foo", []{5.0}},
    }
}

// Example 2:
// If new Game request like this:
newGame := Game{ID: 1, Players: []Player{{1, "foo", []{5.0},{1, "bar", []{6.7}}}}
// I'm expecting this result:
Game{ID: 1, Players: []Player{{1, "foo", []{3.5, 2.0, 5.0},{1, "bar", []{6.7}}}}

func (db *Store) Merge(newGame *Game) error {
    sess := db.session.Copy()
    defer sess.Close()

    col := sess.DB("foo").C("games")
    // I want to modify newGame here to add a $push operator
    // into a new `bson.M` or `bson.D` to make mgo to upsert
    // my new delta without resetting the player stats
    _, err := col.UpsertId(newGame.ID, newGame)

    return err
}

type Game struct {
    ID int `bson:"_id"`
    Name string
    Players []Player `bson:",omitempty"`
    // ...I omitted other details for simplicity here...
}

type Player struct {
    // This connects the player to the game
    GameID int `bson:"game_id"`
    Name string
    // I want to keep the previous values of stats
    // So, that's why I'm using an array here
    Stats []float64
    // ...
}

I tried this Mongodb command in the console to update the specific game's player:

db.competitions.update({
   _id: 1,
   "players.game_id": 1
}, {
   $push: { 
       "players.$.stats": 3
   }
}, {
   upsert: true
})

Upvotes: 1

Views: 1088

Answers (1)

Alex Blex
Alex Blex

Reputation: 37048

To answer the "My Real Question: How can I customize the behaviour of mgo before upsert?" - you can customise bson marshalling by defining bson Getter to the model.

To illustrate how it works, lets simplify the model to avoid nested documents:

type Game struct {
    ID int `bson:"_id"`
    Name string
    Stats [] float64
}

With newGame as following:

newGame := Game{
    ID: 1,
    Name: "foo",
    Stats: []{5.0}
}

The update col.UpsertId(newGame.ID, newGame) by default marshals newGame into JSON, producing mongo query like:

update({_id:1}, {name: "foo", stats: [5]}, {upsert: true});

To make use of $set, $push etc, you can define a custom bson getter. E.g.

func (g Game) GetBSON() (interface{}, error) {
    return bson.M{
        "$set": bson.M{"name": g.Name}, 
        "$push": bson.M{"stats": bson.M{"$each": g.Stats}},
    }, nil
}

So the update col.UpsertId(newGame.ID, newGame) will produce a mongodb query

update({_id:1}, {$set: {name: "foo"}, $push: {stats: {$each: [5]}}}, {upsert: true});

To make it crystal clear - the custom marshaler will be used in all mgo queries, so you probably don't want to define it directly to the model, but to its derivative to use in upsert operations only:

type UpdatedGame struct {
    Game
}

func (g UpdatedGame) GetBSON() (interface{}, error) {
    return bson.M{....}
}

.....

newGame := Game{
    ID: 1,
    Name: "foo",
    Stats: []{5.0}
}

col.UpsertId(newGame.ID, UpdatedGame{newGame})

Upvotes: 2

Related Questions