Атомарный "findOrCreate" в MongoDB: findOne, вставить, если не существует, но не обновлять

115

как говорится в заголовке, я хочу выполнить поиск (один) для документа по _id, и если он не существует, создать его, а затем, независимо от того, был ли он найден или создан, вернуть его в обратном вызове.

Я не хочу обновлять его, если он существует, как я читал, это делает findAndModify. Я видел много других вопросов по этому поводу в Stackoverflow, но опять же, не хочу ничего обновлять.

Я не уверен, что, создавая (или не существуя), ЭТО на самом деле является обновлением, о котором все говорят, все это так сбивает с толку :(

Дисципол
источник

Ответы:

192

Начиная с MongoDB 2.4, больше не нужно полагаться на уникальный индекс (или любой другой обходной путь) для атомарных findOrCreateопераций.

Это стало возможным благодаря в $setOnInsertоператор нового 2.4, которая позволяет задать обновления , которые должны произойти только при вставке документов.

Это в сочетании с upsertопцией означает, что вы можете использовать его findAndModifyдля выполнения атомарной findOrCreateоперации.

db.collection.findAndModify({
  query: { _id: "some potentially existing id" },
  update: {
    $setOnInsert: { foo: "bar" }
  },
  new: true,   // return new doc if one is upserted
  upsert: true // insert the document if it does not exist
})

Поскольку $setOnInsertвлияет только на вставляемые документы, при обнаружении существующего документа никаких изменений не произойдет. Если документа не существует, он вставит его с указанным _id, а затем выполнит установку только для вставки. В обоих случаях документ возвращается.

числа1311407
источник
3
@Gank, если вы используете собственный драйвер mongodb для узла, синтаксис будет больше похож на collection.findAndModify({_id:'theId'}, <your sort opts>, {$setOnInsert:{foo: 'bar'}}, {new:true, upsert:true}, callback). См. Документацию
номера1311407
7
Есть ли способ узнать, был ли документ обновлен или вставлен?
doom
3
Если вы хотите проверить, редактировал ли документ указанный выше запрос ( db.collection.findAndModify({query: {_id: "some potentially existing id"}, update: {$setOnInsert: {foo: "bar"}}, new: true, upsert: true})) insert (upsert) , вам следует рассмотреть возможность использования db.collection.updateOne({_id: "some potentially existing id"}, {$setOnInsert: {foo: "bar"}}, {upsert: true}). Возвращается, {"acknowledged": true, "matchedCount": 0, "modifiedCount": 0, "upsertedId": ObjectId("for newly inserted one")}если документ вставлен, {"acknowledged": true, "matchedCount": 1, "modifiedCount": 0}если документ уже существует.
Константин Ван
8
кажется устаревшим в аромате findOneAndUpdate, findOneAndReplaceилиfindOneAndDelete
Равшан Самандаров
5
Но здесь нужно быть осторожным. Это работает, только если селектор findAndModify / findOneAndUpdate / updateOne однозначно идентифицирует один документ по _id. В противном случае upsert разделяется на сервере на запрос и обновление / вставку. Обновление по-прежнему будет атомарным. Но запрос и обновление вместе не будут выполняться атомарно.
Anon
15

Версии драйверов> 2

Используя последний драйвер (> версии 2) , вы будете использовать findOneAndUpdate, поскольку findAndModifyон устарел. Новый метод принимает 3 аргумент, filter, то updateобъект (который содержит ваши свойства по умолчанию, которые должны быть вставлены для нового объекта), и optionsгде вы должны указать операцию upsert.

Используя синтаксис обещания, это выглядит так:

const result = await collection.findOneAndUpdate(
  { _id: new ObjectId(id) },
  {
    $setOnInsert: { foo: "bar" },
  },
  {
    returnOriginal: false,
    upsert: true,
  }
);

const newOrUpdatedDocument = result.value;
hansmaad
источник
Спасибо, returnOriginalдолжно быть, returnNewDocumentя думаю - docs.mongodb.com/manual/reference/method/…
Доминик
Неважно, драйвер узла реализовал это иначе, и ваш ответ правильный
Доминик
6

Немного грязно, но можно просто вставить.

Убедитесь, что ключ имеет уникальный индекс (если вы используете _id, все в порядке, он уже уникален).

Таким образом, если элемент уже присутствует, он вернет исключение, которое вы можете перехватить.

Если его нет, будет вставлен новый документ.

Обновлено: подробное объяснение этого метода в документации MongoDB.

Madarco
источник
хорошо, это хорошая идея, но для уже существующего значения он вернет ошибку, но НЕ само значение, верно?
Discipol
3
Это на самом деле один из рекомендуемых решений для выделенных последовательностей операций (найти то создать , если не найдено, предположительно) docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations
numbers1311407
@Discipol, если вы хотите выполнить набор атомарных операций, вы должны сначала заблокировать документ, затем изменить его и в конце отпустить. Для этого потребуется больше запросов, но вы можете оптимизировать для наилучшего сценария и в большинстве случаев выполнять только 1-2 запроса. см .: docs.mongodb.org/manual/tutorial/perform-two-phase-commit
Мадарко
1

Вот что я сделал (драйвер Ruby MongoDB):

$db[:tags].update_one({:tag => 'flat'}, {'$set' => {:tag => 'earth' }}, { :upsert => true })}

Он обновит его, если он существует, и вставит его, если его нет.

Видар
источник