0

Im going to create a achievement system in Mongodb. But im not sure how i would format/store it in the database.

As of the users should have a progress (on each achievement they would have some progress value stored), im really confused what would be the best way to perform this, and without having an performence issue.

what should i do?, cause i dont know, what i had in mind, was maybe something like:

Should i store each achievement in an unique row in a Achievement collection, and an user array within that row, containing object with userid and achievement progress?

Would i then get an performance issue when its 1000+ achievements, that is beeing checked fairy often?

or should i do something else?

example schema for the option above:

  {  
   name:{  
      type:String,
      default:'Achievement name'
   },
   users:[  
      {  
         userid:{  
            type:String,
            default:' users id here'
         },
         progress:{  
            type:Number,
            default:0
         }
      }
   ]
}
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
maria
  • 207
  • 5
  • 22
  • 56
  • 2
    *"what should i do?"* - In all honesty no-one can actually tell you. You seem to be asking for the "best practice way", but there actually is no such thing, and if anybody tells you differently then they are simply not speaking the truth. The **only** matter of import here is **what does your application do, and how do you use the data**. In reality the best way to measure that is with **testing**. So what you should do is record some data with ANY structure, and try some things. If you find out things are not working then look at changing them. – Neil Lunn Mar 21 '19 at 05:44
  • About the only concrete thing I can summize from what is in fact a "very broad" question is that your main usage of an "array" at any point of schema should be for "related data to a main object" where it is your intent to retrieve/update that data in concert with the *main* document content **the majority of the time**. If that is not your usage case, then you probably should not be storing in an array. – Neil Lunn Mar 21 '19 at 05:47
  • Some related (ish) reading: [MongoDB relationships: embed or reference?](https://stackoverflow.com/q/5373198/2313887) and [Mongoose populate vs object nesting](https://stackoverflow.com/q/24096546/2313887), with the latter having "some" mongoose specifics, but mostly applies to MongoDB in general as well. Both have similar conclusive points, though slightly different ones. – Neil Lunn Mar 21 '19 at 05:50
  • Hi, the `users` field should not grow without bound, the maximum BSON document size is 16 megabyte. – Haniel Baez Feb 14 '21 at 16:33

4 Answers4

4

Even though the question is specifically about the database design, I will give a solution for the tracking/awarding logic as well to establish more accurate context for the db design.

I would store the achievements progress separately from the already awarded achievements for cleaner tracking and discovery.

The whole logic is event based and has multiple layers of event handling. This gives you TONS of flexibility on how you track your data and also gives you a pretty good mechanism to track history. Basically, you can look at it as a form of logging.

Of course, your system design and contracts are highly dependent on the information you're gonna be tracking and its complexity. A simple progress field may not suffice for each case(you might want to track something more complex, not a simple number between X and Y). There is also the case of tracking data which updates quite frequently(as distance travelled in games, for example). You didn't give any context on the topic of your achievement system so we're gonna stick with a generic solution. It's just a couple of things that you should take a note about as it will affect the design.

Okay, so, let's start from the top and track the entire flow for a tracked piece of data and its eventual achievement progress. Let's say we're tracking consecutive days of user login and we're gonna award him with an achievement when he reaches [10].

Note that everything below is just a pseudo-code.

So, let's say today is [8th of July, 2017]. For now, our User entity looks like this:

User: {
   id: 7;
   trackingData: {
      lastLogin: 7 of July, 2017 (should be full DateTime object, but using this for brevity),
      consecutiveDays: 9
   },
   achievementProgress: [
      {
         achievementID: 10,
         progress: 9
      }
   ],
   achievements: []
}

And our achievements collection contains the following entity:

Achievement: {
   id: 10,
   name: '10 Consecutive Days',
   rewardValue: 10
}

The user tries to login(or visit the site). The application handler takes note of that and after handling the login logic fires an event of type ACTION:

ACTION_EVENT = {
   type: ACTION,
   name: USER_LOGIN,
   payload: {
      userID: 7,
      date: 8 of July, 2017 (should be full DateTime object, but using this for brevity)
   }
}

We have an ActionHandler which listens for events of type ACTION:

ActionHandler.handleEvent(actionEvent) {
   subscribersMap = Map<eventName, handlers>;

   subscribersMap[actionEvent.name].forEach(subscriber => subscriber.execute(actionEvent.payload));
}

subscribersMap gives us a collection of handlers that should respond to each specific action(this should resolve to USER_LOGIN for us). In our case we can have 1 or 2 that concern themselves with updating the user tracking information of lastLogin and consecutiveDays tracking properties in the user entity. The handlers in our case will update the tracking information and fire new events further down the line. Once again, for brevity, we're gonna incorporate both into one:

updateLoginHandler: function(payload) {
   user = db.getUser(payload.userID);
   
   let eventType;
   let eventValue;

   if (date - user.trackingData.lastLogin > 1 day) {
      user.trackingData = 1;

      eventType = 'PROGRESS_RESET';
      eventValue = 1;
   }
   else {
      const newValue = user.trackingData.consecutiveDays + 1;

      user.trackingData.consecutiveDays = newValue;

      eventType = 'PROGRESS_INCREASE';
      eventValue = newValue;
   }

   user.trackingData.lastLogin = payload.date;

   /* DISPATCH NEW EVENT OF TYPE ACHIEVEMENT_PROGRESS */
   AchievementProgressHandler.dispatch({
      type: ACHIEVEMENT_PROGRESS
      name: eventType,
      payload: {
         userID: payload.userID,
         achievmentID: 10,
         value: eventValue
      }
   });
}

Here, PROGRESS_RESET have the same contract as the PROGRESS_INCREASE but have a different semantic meaning and I would keep them separate for history/tracking purposes. If you wish, you can combine them into a single PROGRESS_UPDATE event. Basically, we update the tracked fields that are dependent on the lastLogin date and fire a new ACHIEVEMENT_PROGRESS event which should be handled by a separate handler with the same pattern(AchievementProgressHandler). In our case:

ACHIEVEMENT_PROGRESS_EVENT = {
   type: ACHIEVEMENT_PROGRESS,
   name: PROGRESS_INCREASE
   payload: {
      userID: 7,
      achievementID: 10,
      value: 10
   }
}

Then, in AchievementProgressHandler we follow the same pattern:

AchievementProgressHandler: function(event) {
   achievementCheckers = Map<achievementID, achievementChecker>;

   /* update user.achievementProgress code */

   switch(event.name): {
      case 'PROGRESS_INCREASE':
         achievementCheckers[event.payload.achievementID].execute(event.payload);
         break;
      case 'PROGRESS_RESET':
         ...
   }
}

achievementCheckers contains a checker function for each specific achievement that decides if the achievement has reached its desired value(a progress of 100%) and should be awarded. This enables us to handle all kinds of complex cases. If you only track a single X out of Y scenario, you can share the function between all achievements.

The handler basically does this:

achievementChecker: function(payload) {
   achievementAwardHandler;

   achievement = db.getAchievement(payload.achievementID);

   if (payload.value >= achievement.rewardValue) {
      achievementAwardHandler.dispatch({
         type: ACHIEVEMENT_AWARD,
         name: ACHIEVEMENT_AWARD,
         payload: {
            userID: payload.userID,
            achievementID: achievementID,
            awardedAt: [current date]
         }
      });

      /* Here you can clear the entry from user.achievementProgress as you no longer need it. You can also move this inside the achievementAwardHandler. */
   }   
}

We once again dispatch an event and use an event handler - achievementAwardHandler. You can skip the event creation step and award the achievement to the user directly but we keep it consistent with the whole history logging flow. An added benefit here is that you can use the handler to defer the achievement awarding to a specific later time thus effectively batching awarding for multiple users, which serve a couple of purposes including performance enhancement.

Basically, this pseudo code handles the flow from [a user action] to [achievement rewarding] with all intermediate steps included. It's not set in stone, you can modify it as you like but all in all, it gives you a clean separation of concerns, cleaner entities, it's performant, let's you add complex checks and handlers which are easy to reason about while in the same time provide a great history log of the user overall progress.

Regarding the DB schema entities, I would suggest the following:

User: {
   id: any;
   trackingData: {},
   achievementProgress: {} || [],
   achievements: []
}

Where:

  • trackingData is an object that contains everything you're willing to track about the user. The beauty is that properties here are independent from achievement data. You can track whatever and eventually use it for achievement purposes.
  • achievementProgress: a map of <key: achievementID, value: data> or an array containing the current progress for each achievement.
  • achievements: an array of awarded achievements.

and Achievement:

Achievement: {
   id: any,
   name: any,
   rewardValue: any (or any other field/fields. You have complete freedom to introduce any kind of tracking with the approach above),
   users?: [
      {
         userID: any,
         awardedAt: date
      }
   ]
}

users is a collection of users who have been rewarded the given achievement. This is optional and is here only if you have the use for it and query for this data frequently.

zhulien
  • 5,145
  • 3
  • 22
  • 36
0

What you might be looking for is a Badge style implementation. Just like Stack Overflow rewards it's users with badges for specific achievements.

Method 1: You can have flags in the user profile for each badge. Since you're doing it in NoSQL database, you just have to set a flag for each badge.

const badgeSchema = new mongoose.Schema({
  badgeName: {
    type: String,
    required: true,
  },
  badgeDescription: {
    type: String,
    required: true,
  }
});
const userSchema = new mongoose.Schema({
  userName: {
    type: String,
    required: true,
  },
  badges: {
    type: [Object],
    required: true,
  }
});

If your application architecture is event based, you can trigger awarding badges to users. And that operation is just inserting Badge object with progress in User badges array.

{ 
    badgeId: ObjectId("602797c8242d59d42715ba2c"),
    progress: 10
}

Update operation will be to find and update the badges array with progress percentage number

And while displaying user achievements on user interface, you can just loop over badges array to show the badges this user has achieved and their progress with it.

Method 2: Have a separate mongo collection for Badge and User Mapping. Whenever a user achieves a badge you insert a record in that collection. It will be one to one mapping of user _id and badge _id and progress value. But as the table will grow huge you will need to do indexing to efficiently query user and badge mapping.

You will have to do analysis on best approach according to your specific use case.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
sks147
  • 196
  • 2
  • 8
0

MongoDB is flexible enough to allow teams develop applications quickly, and involve their model with litter friction as the application needs it. In cases where you need a robust model from day one, theirs is a flexible methodology that can guide you through the process of modeling your data.

The methodology is composed of:

  1. Workload: This stage is about gathering as much information as possible to understand your data. This will allow you formulate assumptions about, you data size the operations that will be performance against it (reads and writes), quantify operations and qualify operations.

You can get this by:

  • Scenarios
  • Prototype
  • Production Logs & Stats (if you are migrating).
  1. Relationships: Identify the relationship between the different entities in your data, quantify those relationships and apply embedding or linking. In general you should prefer embedding by default, but remember that arrays should not grow without bound (6 Rules of Thumb for MongoDB Schema Design: Part 3).

  2. Patterns: Apply schema design patterns. Take a look at Building with Patterns: A Summary, it presents a matrix that highlights the pattern that could be useful for a given use case.

Finally, the goal of this methodology is help you create a model, that can scale and perform well under stress.

Haniel Baez
  • 1,646
  • 14
  • 19
0

If you design the achievement schema like this:

{
  name: {
    type: String,
    default: "Achievement name",
  },
  userid: {
    type: String,
    default: " users id here",
  },
  progress: {
    type: Number,
    default: 0,
  },
}
}

When an achievement is gained you just add another entry

for getting achievements Map-Reduce is a good candidate for running map reduce on the database. you can run them on a less regular basis, using them for offline computation of the data that you want.

based on documentation you can do like the following photo enter image description here

Mohammad Yaser Ahmadi
  • 4,664
  • 3
  • 17
  • 39