Mongodb模式成就系统的最佳存储

时间:2019-03-21 00:17:56

标签: database mongodb database-design schema mongoose-schema

我要在Mongodb中创建一个成就系统。但是我不确定如何将其格式化/存储在数据库中。

从用户的角度来看,应该有一个进步(每完成一个成就,他们就会存储一些progress value),我真的感到困惑,这是执行此操作的最佳方法,并且没有性能问题。

我该怎么办?,因为我不知道,我的想法也许是这样的:

我是否应该将每个成就存储在Achievement集合的唯一行中,并在该行中存储一个包含用户ID和成就进度的对象的用户数组?

然后我会在1000多个成就中遇到一个性能问题,那就是经常被检查的仙女吗?

还是我应该做些其他事情?

上述选项的示例架构:

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

4 个答案:

答案 0 :(得分:2)

即使问题是专门针对数据库设计的,我也会为跟踪/奖励逻辑提供解决方案,以便为数据库设计建立更准确的上下文。

我会将成就进度与已授予的成就分开存储,以便进行更清晰的跟踪和发现。

整个逻辑是基于事件的,并有多层事件处理。这为您在跟踪数据的方式上提供了大量的灵活性,并为您提供了一个非常好的跟踪历史的机制。基本上,您可以将其视为一种日志记录形式。

当然,您的系统设计和合同高度依赖于您要跟踪的信息及其复杂性。一个简单的 progress 字段可能无法满足每种情况(您可能想要跟踪更复杂的东西,而不是 X 和 Y 之间的简单数字)。还有一种情况是跟踪数据更新非常频繁(例如,在游戏中移动的距离)。您没有提供有关成就系统主题的任何背景信息,因此我们将坚持使用通用解决方案。这只是您应该注意的几件事,因为它会影响设计。

好的,那么,让我们从顶部开始,跟踪被跟踪数据的整个流程及其最终成就进度。假设我们正在跟踪用户登录的连续天数,当他达到 [10] 时,我们将授予他一项成就。

请注意,下面的所有内容都只是一个伪代码

那么,假设今天是 [2017 年 7 月 8 日]。目前,我们的 User 实体如下所示:

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: []
}

我们的成就集合包含以下实体:

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

用户尝试登录(或访问网站)。应用程序处理程序注意到这一点,并在处理登录逻辑后触发 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)
   }
}

我们有一个 ActionHandler 来监听 ACTION 类型的事件:

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

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

subscribersMap 为我们提供了一组处理程序,这些处理程序应该响应每个特定操作(这应该为我们解析为 USER_LOGIN)。在我们的例子中,我们可以有 1 或 2 个关注更新 lastLogin 实体中 consecutiveDaysuser 跟踪属性的用户跟踪信息。我们案例中的处理程序将更新跟踪信息并进一步触发新事件。 再次,为简洁起见,我们将两者合二为一:

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
      }
   });
}

此处,PROGRESS_RESETPROGRESS_INCREASE 具有相同的约定,但具有不同的语义含义,为了历史/跟踪目的,我会将它们分开。如果您愿意,可以将它们合并为一个 PROGRESS_UPDATE 事件。 基本上,我们更新依赖于 lastLogin 日期的跟踪字段并触发一个新的 ACHIEVEMENT_PROGRESS 事件,该事件应该由具有相同模式 (AchievementProgressHandler) 的单独处理程序处理。在我们的例子中:

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

然后,在 AchievementProgressHandler 中,我们遵循相同的模式:

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 包含每个特定成就的检查器函数,用于决定成就是否达到其期望值(进度为 100%)并应授予。这使我们能够处理各种复杂的情况。如果您只跟踪 Y 场景中的一个 X,您可以在所有成就之间共享该功能。

处理程序基本上是这样做的:

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. */
   }   
}

我们再次调度事件并使用事件处理程序 - achievementAwardHandler。您可以跳过事件创建步骤,直接将成就奖励给用户,但我们会使其与整个历史记录流程保持一致。 这里的一个额外好处是,您可以使用处理程序将成就授予推迟到特定的稍后时间,从而有效地为多个用户批量授予奖励,这有几个目的,包括性能增强。

基本上,这个伪代码处理从[用户操作]到[成就奖励]的流程,包括所有中间步骤。它不是一成不变的,您可以随心所欲地修改它,但总而言之,它为您提供了清晰的关注点分离,更清洁的实体,它的性能,让您添加复杂的检查和处理程序,这些检查和处理程序在相同的情况下易于推理time 提供了用户整体进度的重要历史记录。

关于数据库架构实体,我建议如下:

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

地点:

  • trackingData 是一个包含你想要的一切的对象 跟踪用户。美妙之处在于这里的属性是 独立于成就数据。您可以跟踪任何内容并最终将其用于成就目的。
  • achievementProgress<key: achievementID, value: data> 的地图或 一个包含每个成就的当前进度的数组。
  • achievements:一系列获奖成就。

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 是获得给定成就奖励的用户的集合。这是可选的,仅当您使用它并经常查询此数据时才出现在这里。

答案 1 :(得分:0)

您可能正在寻找的是徽章风格的实现。就像 Stack Overflow 用徽章奖励特定成就的用户一样。

方法 1:您可以在用户个人资料中为每个徽章设置标记。由于您是在 NoSQL 数据库中执行此操作,因此您只需为每个徽章设置一个标志。

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,
  }
});

如果您的应用架构是基于事件的,您可以触发向用户授予徽章。该操作只是在 User badges 数组中插入带有进度的 Badge 对象。

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

更新操作是查找并更新带有进度百分比数字的徽章数组

在用户界面上显示用户成就时,您可以循环遍历 badges 数组以显示该用户已获得的徽章及其取得的进展。

方法 2: 有一个单独的 mongo 集合用于徽章和用户映射。每当用户获得徽章时,您都会在该集合中插入一条记录。它将是用户 _id 和徽章 _id 以及进度值的一对一映射。但随着表的增长,您将需要建立索引以有效查询用户和徽章映射。

您必须根据您的特定用例对最佳方法进行分析。

答案 2 :(得分:0)

MongoDB 足够灵活,可以让团队快速开发应用程序,并在应用程序需要时让他们的模型产生摩擦。如果您从一开始就需要一个强大的模型,他们的方法是一种灵活的方法,可以指导您完成数据建模过程。

methodology 由以下组成:

  1. 工作量:此阶段是关于收集尽可能多的信息以了解您的数据。这将允许您制定假设,您的数据大小将针对它的性能(读取和写入),量化操作和限定操作。

您可以通过以下方式获得:

  • 场景
  • 原型
  • 生产日志和统计数据(如果您正在迁移)。
  1. 关系:确定数据中不同实体之间的关系,量化这些关系并应用嵌入或链接。一般来说,默认情况下您应该更喜欢嵌入,但请记住,数组不应无限制地增长 (6 Rules of Thumb for MongoDB Schema Design: Part 3)。

  2. 模式:应用架构设计模式。看看 Building with Patterns: A Summary,它提供了一个矩阵,突出显示了可能对给定用例有用的模式。

最后,此方法论的目标是帮助您创建一个模型,该模型可以在压力下扩展并表现良好。

答案 3 :(得分:0)

如果你像这样设计成就架构:

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

获得成就后,您只需添加另一个条目

为了获得成就 Map-Reduce 是在数据库上运行 map reduce 的好选择。您可以不定期运行它们,将它们用于离线计算您想要的数据。

基于 documentation 你可以像下面的照片一样 enter image description here