mjmoody383
mjmoody383

Reputation: 358

MongoDB data model and update strategy

I have a MongoDB collection, named Players

_id, name, stats[ { stName, stValue, stCountC } ]

I will be updating various stats for each Player every day, but will only update the Stat values when a new value is found (ie, new.stCount > existing.stCountC). My general idea is to limit the stats array to 2 objects for each unique stName -- one for the Current value and one for the Previous value. By example therefore, let's say I find a new stat with count of 6, my query and update process would start with -

db.players.findOne({ name: "JSmith",
                     stats: { $elemMatch: { stName: "Avg", stCountC: 5 } } })

If the above query returns a document, then I update as follows -

1) Get JSmith's stValue where stName = "Avg" and stCountP = 4 -

db.players.findOne({ name: "JSmith", stats: { stName: "Avg", stCountP: 4 } },
                   { stats.$.stValue })

2) Insert this value to my StatsHistory collection, which holds all historical values for each player for a single stat type -

db.statshistory.update({ Name: "JSmith", StatName: "Avg" },
                       { $addToSet : { oldValues : { stValue: <val>, stCount: 4 } } })

3) Update my Players collection -

db.players.update({ Name: JSmith },
                  { $push: { stats: { stName: "Avg", stValue: "98", stCountC: 6 } }
                    $pull: { stats: { stName: "Avg", stCountP: 4 } } })

db.players.update({ name: "JSmith", stats.stName: "Avg", stats.stCountC: 5 },
                  { $rename: { "stats.stCountC": "stats.stCountP" } })

I will be presenting data grids of current stat values from the Players collection (ie one row per player, one column per stat name). I will also have views that show trends of stat values, and assume I would use MongoDB's Aggregation functions to return these from my StatsHistory collection.

Question: Does the above data model and find/update process seem logical?

I'm obviously new to MongoDB, so apologies for any syntax errors or if the above seems totally inappropriate to my needs. Any feedback is much appreciated!

Resolved: Thanks to idbentley for the suggestion below. It helped me devise the following database update procedure using async.js. Note the data model changed slightly -- the Players collection now only holds the most current stat values, each attempted update pass updates a LastScan date stamp on the main record, and each stat update provides a date stamp LastUpdate. Stat count is still used to check if returned stat is newer. This procedure also ensures that any new players / stats are upserted to the collections.

async.series([
  function(cb){ db.statshistory.update({ Name: <name>, StatName: <statname> },
                                       { $set : { LastScan: new Date() }},
                                       { upsert: true },
                                       function() { cb(); });
  }
  ,function(cb){ db.statshistory.update({ Name: <name>, StatName: <statname>, OldValues: { $not : { $elemMatch : { StCount: <statcount> }}}},
                                        { $push : { OldValues: { LastUpdate: new Date(), StCount: <statcount>, StValue: <statvalue> }}},
                                        function() { cb(); });
  }
  ,function(cb){ db.players.update({ Name: <name> },
                                   { $set : { LastScan: new Date() }},
                                   { upsert: true },
                                   function() { cb(); });
  }
  ,function(cb){ db.players.update({ Name: <name> },
                                   { $pull : { Stats: { StName: <statname>, StCount: { $ne: <statcount> }}}},
                                   function() { cb(); });
  }
  ,function(cb){ db.players.update({ Name: <name>, Stats: { $not : { $elemMatch : { StName: <statname>, StCount: <statcount> }}}},
                                   { $push : { Stats: { LastUpdate: new Date(), StCount: <statcount>, StValue: <statvalue> }}},
                                   function() { cb(); });
  }]
  ,function() { console.log('update complete'); }
)

Upvotes: 0

Views: 600

Answers (1)

idbentley
idbentley

Reputation: 4218

I think you might be making things more complicated than necessary.

My understanding of your requirements:

  1. Each player has a number of statistics that change over time.
  2. We should maintain a full history of a players stats over time, but most often we only need the current and previous stats

If I understand correctly, then I would change things slightly.

First, I would add a current flag to the embedded stats documents.

Then your workflow changes to:

db.statshistory.update( { Name: <name>, StatName: <statname> },
    { $addToSet : { oldValues : { <statobj> } } } );

db.player.update( { Name: <name>, stats:
    { $elemMatch: { stName: <statname>} } },
    { $pull: { current: false } } );

db.player.update( {Name: <name>, stats: 
    { $elemMatch: { stName: <statname>, current: true } } },
    { $set: { stats.current: false } } );

db.player.update({Name: <name>},
    { $addToSet: { <statobj>, current: true } });

Because this uses multiple updates, it won't work well in a multi-threaded environment.

Upvotes: 1

Related Questions