使用Mongoose查找或创建数据

33

我有

Page.findById(pageId).then(page => {
  const pageId = page.id;
   ..
});

我的问题是,如果没有给出页面ID,它应该根据一些条件选择第一个可用的页面,方法如下:

Page.findOne({}).then(page => {
  const pageId = page.id;
  ..
});

但如果没有找到页面,就应该创建一个新页面并使用它,可以通过以下方式完成:

Page.create({}).then(page => {
  const pageId = page.id;
  ..
});

但是我如何将所有内容合并到尽可能少的行数中呢?

我的内部有很多逻辑。

page => { ... }

所以我非常希望能够聪明地做到这一点,这样我就可以避免像这样做了。

if (pageId) {
  Page.findById(pageId).then(page => {
    const pageId = page.id;
     ..
  });
} else {
  Page.findOne({}).then(page => {
    if (page) {
      const pageId = page.id;
      ..
    } else {
      Page.create({}).then(page => {
        const pageId = page.id;
        ..
      });
    }
  });
}
我想我可以使用类似以下方式将静态值分配给模式:

我在考虑是否可以使用静态值来为模式分配某些内容,例如:

pageSchema.statics.findOneOrCreate = function (condition, doc, callback) {
  const self = this;
  self.findOne(condition).then(callback).catch((err, result) => {
    self.create(doc).then(callback);
  });
};

你是否尝试在此处插入一个空文档:Page.create({}).then(page) - Stavros Zavrakas
是的,这是有意的 :-) - Jamgreen
8个回答

57
根据Mongoose的文档
根据之前的SO回答

Model.findByIdAndUpdate()

查找匹配的文档,根据更新参数进行更新,并传递任何选项,然后将找到的文档(如果有)返回给回调函数。

在选项中设置upsert为true:

upsert:bool-如果对象不存在则创建它。默认为false。

Model.findByIdAndUpdate(id, { $set: { name: 'SOME_VALUE' }}, { upsert: true  }, callback)

14
如果您没有id,findOneAndUpdate()函数也有相同的upsert选项:https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate - ahaurat
2
这是一张很好的Mongoose更新操作比较表,突出了原子操作。 - Dan Dascalescu

17

Yosvel Quintero 的回答有关,但对我无效:

pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, callback) {
    const self = this
    self.findOne(condition, (err, result) => {
        return result ? callback(err, result) : self.create(condition, (err, result) => { return callback(err, result) })
    })
}

然后像这样使用它:

Page.findOneOrCreate({ key: 'value' }, (err, page) => {
    // ... code
    console.log(page)
})

3
对于使用NodsJS和Mongoose的用户来说,好消息是'mongoose-find-one-or-create'这个npm插件非常好用,可以像一个插件一样容易地补丁到您的DB模式中。希望能帮到大家。 - NBaua
4
答案并不是原子性的。 - rels
你是正确的。要求:NodeJs和Mongoose。 - David Joos

13

承诺 async/await 版本。

Page.static('findOneOrCreate', async function findOneOrCreate(condition, doc) {
  const one = await this.findOne(condition);

  return one || this.create(doc);
});

使用方法

Page.findOneOrCreate({ id: page.id }, page).then(...).catch(...)

或者

async () => {
  const yourPage = await Page.findOneOrCreate({  id: page.id }, page);
}

这不是原子性的。 - Dan Dascalescu
如果多个客户端同时向集合写入数据,那么在.findOne()返回false之后,另一个客户端可能会创建一个文档,然后这段代码会再次创建一个文档。请参阅原子性 - Dan Dascalescu
那什么时候会变成真相呢?我相信,如果你有一个良好的系统设计,你的开发人员就不需要处理这个问题。如果在许多情况下findOne实际上并不好用,我相信mongoose/mongodb会将其标记为弃用。简单来说,可以将上述代码视为“查找或创建”的快捷方式。或者像另一个答案提到的那样使用upsert - Ninh Pham
没有“良好的系统设计”可以防止我所描述的情况。当然,在小型开发中,写入很少,这种情况不太可能发生。一个好问题是,使用这个答案中的两步方法与findOneAndUpdate答案中的单一原子步骤相比,真正的好处是什么。 - Dan Dascalescu

10

每个模式都可以为其模型定义实例和静态方法。静态方法基本上与方法相同,但允许定义直接存在于您的模型上的函数。

静态方法findOneOrCreate

pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, doc, callback) {
  const self = this;
  self.findOne(condition, (err, result) => {
    return result 
      ? callback(err, result)
      : self.create(doc, (err, result) => {
        return callback(err, result);
      });
  });
};

现在当你有一个Page的实例时,你可以调用findOneOrCreate

Page.findOneOrCreate({id: 'somePageId'}, (err, page) => {
  console.log(page);
});

1
使用Promise而不是回调函数,它会是什么样子? - Jamgreen
应该使用 PageSchema.static 而不是 PageSchema.statics - Merhawi Fissehaye
2
这不是原子性的。 - Dan Dascalescu

3

使用 async/await 的一行代码解决方案:

const page = await Page.findOne({}) || await Page.create({});


不是原子操作。原子解决方案必须使用.updateOne或.findOneAndUpdate,这两个方法都有upsert选项。 - Dan Dascalescu

0

如果你不想在模型中添加静态方法,可以尝试移动一些东西并且至少不要有这么多嵌套级别的回调函数:

function getPageById (callback) {
  Page.findById(pageId).then(page => {
    return callback(null, page);
  });
}

function getFirstPage(callback) {
  Page.findOne({}).then(page => {
    if (page) {
      return callback(null, page);
    }

    return callback();
  });
}

let retrievePage = getFirstPage;
if (pageId) {
  retrievePage = getPageById;
}

retrievePage(function (err, page) {
  if (err) {
    // @todo: handle the error
  }

  if (page && page.id) {
    pageId = page.id;
  } else {
    Page.create({}).then(page => {
      pageId = page.id;
    });
  }
});

0

这里发布的解决方案忽略了当字段或字段组合上存在唯一索引时,此模式最常见的情况。这个解决方案正确地考虑了唯一索引违规错误:

mongoose.plugin((schema) => {
  schema.statics.findOrCreate = async function findOrCreate(key, attrs) {
    try {
      return await this.create({ ...attrs, ...key });
    } catch (error) {
      const isDuplicateOnThisKey =
        error.code === 11000 &&
        Object.keys(error.keyPattern).sort().join(',') ===
          Object.keys(key).sort().join(',');
      if (isDuplicateOnThisKey) {
        const doc = await this.findOne(error.keyValue);
        doc.set(attrs);
        return await doc.save();
      }
      throw error;
    }
  };
});

使用方法:

await Post.findOrCreate({ slug: 'foobar' }, { title: 'Foo Bar', body });

-1

试试这个...

 var myfunc = function (pageId) {
  // check for pageId passed or not
 var newId = (typeof pageId == 'undefined') ? {} : {_id:pageId};

 Page.findOne(pageId).then(page => {
 if (page)
 const pageId = page.id;
 else {  // if record not found, create new

    Page.create({}).then(page => {
        const pageId = page.id;
    });
  }
});

 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接