1

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
})
Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
  • What would you expect in the result when the new game is `Game{ID: 1, Players: []Player{{"foo", []{5.0},{"bar", []{6.7}}}}` ? – Alex Blex Aug 16 '17 at 16:47
  • I thought so =( upsert is not going to work for sub-documents. See https://stackoverflow.com/questions/23470658/mongodb-upsert-sub-document – Alex Blex Aug 16 '17 at 16:54
  • Could you add the query you have used to the question? I am extremely curious, as you are pushing to sub-sub-document. – Alex Blex Aug 16 '17 at 16:59
  • Thanks for the update. Just to confirm we are on the same page, the plain upsert query finds a game with id 1, and updates **first** player with players.game_id = 1, and **fails** if there is no such player, or no such game. Are you happy with this behaviour and just want to code it in go? The upsert part makes no sense in such scenario, so I wanted to confirm it. – Alex Blex Aug 16 '17 at 17:39
  • Hmm, not sure if get the last comment. I appreciate your are reducing the problem to mvce, but I think you stripped too much. To make it clear: with current editions new players cannot be added to the game with a single upsert of stats. Neither you can update multiple existing players in a game with single query. – Alex Blex Aug 16 '17 at 17:56
  • okay mate, I have added an answer of how to customise the behaviour. My point I was trying to make in the comments above is that the real problem lies in *"It doesn't matter which Mongo commands I'm going to use. I'll figure it out somehow."* - there is no such command. Unfortunately. – Alex Blex Aug 17 '17 at 10:02
  • No, not with current data structure. It does not support such operations. Sorry. – Alex Blex Aug 17 '17 at 10:37

1 Answers1

2

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})
Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • told ya =(, anyway I have updated the answer, to make it clear how the "customisation" works. – Alex Blex Aug 17 '17 at 10:34
  • I didn't get this part tbh. Not sure where complexity and errors popped up from, but if you want to make it programatically, just marshal the embedded Game and unmarshal it to bson.M type. Then you can manipulate with the map as you wish. Be aware of consequences tho. With hardcoded bson you catch all errors on compilation. Shifting it to runtime you actually increase code complexity and have to catch errors runtime. – Alex Blex Aug 17 '17 at 12:17