返回

高效存储与检索用户数据: MongoDB实践指南

javascript

用户数据高效存储与检索

应用程序中用户数据管理是一个核心挑战。特别是,如何高效存储和检索特定用户的notes? 直接将用户所有 notes 以数组形式存储,虽实现功能,但后续的修改、删除操作确较为繁琐。 我们将探讨更高效的方法。

单文档存储 vs 细粒度存储

当前方法将一个用户的所有 notes 存储在一个文档中的数组中。 这种方式的优点是结构简单,易于理解。 然而,随着用户 notes 数量增加,这个数组将变得庞大,加载整个文档来修改或检索单个note效率低下,特别是大型集合,影响应用响应速度。 细粒度的存储策略更合适。

方案一:独立文档存储

一种策略是为每条note创建一个独立的文档。 每条note都包含用户的信息,比如用户名。 MongoDB的索引可以确保按用户名快速查找特定用户的所有notes。

数据结构

每个文档代表一条note,格式如下:

{
    "_id": ObjectId(),
    "username": "Dhruv70",
    "topic": "hello",
    "content": "world"
}

代码示例

假设 MongoDB 模型名为 NoteModel。以下是在ExpressJS中创建和查找notes的代码。

  • 添加新的 Note:
router.post('/addnote', async (req, res) => {
    const { username, topic, content } = req.body;
    const newNote = new NoteModel({
        username: username,
        topic: topic,
        content: content,
    });

    try {
        await newNote.save();
        res.status(201).send({ message: 'Note added successfully.' });
    } catch (error) {
         res.status(500).send({ message: 'Error adding note.', error: error.message});
    }
});
  • 获取特定用户的所有Notes
router.get('/usernotes', async (req, res) => {
    const { username } = req.query;
    try {
       const notes = await NoteModel.find({ username: username });
        res.status(200).send(notes);
    } catch (error){
         res.status(500).send({message: 'Error fetching notes.', error: error.message});
    }
})

操作步骤

  1. 修改 Mongoose 模型 NoteModel,删除原有 notes 数组。
  2. 创建新的 POST API 端点 /addnote,处理新增 note 请求。
  3. 创建新的 GET API 端点 /usernotes, 根据用户名查询返回所有相关 notes。
  4. 测试 API, 验证存储和检索逻辑。

优势
这种方式下, 添加、修改、删除单一note无需加载其他数据。 MongoDB 检索支持基于用户名筛选的notes。 性能表现会好很多。

安全提示 : 客户端传入的数据必须校验,例如用户名不能为null或者空,可以加入正则表达式对数据格式进行验证, 保证入库的数据完整性和可用性。

方案二: 使用子文档和索引

若仍倾向于每个用户一个文档,可将每条 note 以子文档的形式存在于文档中,但不能用数组存储,可以存储为对象。对象里的键使用唯一的标识。 每个对象都应该有它自己的id值,例如一个时间戳或者uuid等,在修改时可以通过ID修改对应的子文档,不需要遍历数组。

数据结构

{
    "_id": ObjectId(),
    "username": "Dhruv70",
    "notes": {
      "1678886400":{
          "topic": "hello",
          "content": "world"
          } ,
      "1678890000":{
          "topic": "bye",
          "content": "universe"
          },
    }
}

代码示例

  • 添加 Note
router.post('/addnote', async (req, res) => {
  const { username, topic, content } = req.body;
    const noteId = Date.now().toString()  // 用时间戳作为键值,也可使用uuid

    try {
        const userDoc = await NoteModel.findOne({ username: username });

        if (userDoc) {
           userDoc.notes[noteId] = {topic:topic, content:content};
          await userDoc.save()
           res.status(201).send({message:"Note added successfully"})
        }else{
           const newUser =  new NoteModel({
               username: username,
                notes:{ [noteId] : {topic:topic, content:content} }
           });
           await newUser.save();
           res.status(201).send({message:"Note added successfully"})
        }
    } catch (error) {
       res.status(500).send({ message: 'Error adding note.', error: error.message });
    }

});
  • 根据用户获取 Note
router.get('/usernotes', async (req, res) => {
    const { username } = req.query;
    try{
        const userDoc = await NoteModel.findOne({username:username});
        if(userDoc && userDoc.notes)
           return  res.status(200).send(userDoc.notes)
         res.status(404).send({message:"no notes found for this user."})
    } catch(err) {
       res.status(500).send({message:"fetch error", error: err.message});
    }
})
  • 修改Note
router.post("/updatenote",async (req, res)=>{
     const {username,noteId,topic,content} = req.body
     try{
       const userDoc = await NoteModel.findOne({username:username});
        if(!userDoc || !userDoc.notes[noteId]) {
         return res.status(404).send({message: "note not found"})
        }

        userDoc.notes[noteId].topic = topic;
        userDoc.notes[noteId].content = content;
        await userDoc.save();

      return  res.status(201).send({message:"update success"})

     }catch (err){
         return  res.status(500).send({message: 'error updating the note.', error: err.message})
     }
})

操作步骤

  1. 修改NoteModel,使用notes对象替换原来的notes数组。
  2. 创建 POST 端点/addnote添加新的子文档.
  3. 创建 GET 端点/usernotes来读取某个用户所有的note.
  4. 创建 POST 端点/updatenote 来更新指定 note 的数据。
  5. 测试修改、读取note操作。

优势 : 使用键值对象可以做到直接索引note,避免遍历。 MongoDB的字段索引还能进一步提升查询性能。

安全提示 :用户发送的 noteId 需要检验,以防篡改;另外注意不要直接把客户端 noteId 直接放入mongo查询语句中, 应该对ID进行数据类型校验,例如判断id字符串是否是ObjectID合法值等等,否则有可能造成注入攻击。

根据应用程序需求选择合适的存储方式。 对于 Notes 管理这类需求,独立的细粒度存储方式更具有优势。 而对子文档对象的方式,通过ID索引也可以提高修改效率,但是需要注意潜在的安全风险。