Перейти к содержанию

UPSERT

Ключевое слово UPSERT может быть использовано для проверки существования определенных документов, а также для их обновления/замены, если они существуют, или создания, если их не существует.

Каждая операция UPSERT ограничена одной коллекцией, и имя коллекции не должно быть динамическим. Только один оператор UPSERT для коллекции разрешен в одном запросе AQL, и за ним не могут следовать операции чтения или записи, которые обращаются к той же коллекции, операции обхода или функции AQL, которые могут читать документы.

Синтаксис

Синтаксис операций upsert и repsert следующий:

UPSERT searchExpression INSERT insertExpression UPDATE updateExpression IN collection
UPSERT searchExpression INSERT insertExpression REPLACE updateExpression IN collection

Оба варианта могут опционально заканчиваться предложением OPTIONS { ... }.

При использовании варианта UPDATE операции upsert найденный документ будет частично обновлен, то есть будут обновлены или добавлены только атрибуты, указанные в updateExpression. При использовании REPLACE варианта операции upsert (repsert), существующие документы будут заменены контекстами updateExpression.

Обновление документа изменит номер ревизии документа на генерируемое сервером значение. Системные атрибуты _id, _key и _rev не могут быть обновлены, _from и _to могут.

Выражение searchExpression содержит документ, который нужно искать. Это должен быть литерал объекта без динамических имен атрибутов. Если в коллекции такой документ не найден, в коллекцию будет вставлен новый документ, указанный в insertExpression.

Если хотя бы один документ в коллекции соответствует searchExpression, он будет обновлен с помощью updateExpression. Если более одного документа в коллекции соответствует поисковомувыражению, не определено, какой из документов будет обновлен. Поэтому часто целесообразно убедиться другими способами (например, с помощью уникальных индексов, логики приложения и т.д.), что только один документ соответствует searchExpression.

Следующий запрос будет искать в коллекции users документ с определенным значением атрибута name. Если документ существует, то его атрибут logins будет увеличен на единицу. Если он не существует, будет вставлен новый документ, состоящий из атрибутов name, logins и dateCreated:

1
2
3
UPSERT { name: 'superuser' }
INSERT { name: 'superuser', logins: 1, dateCreated: DATE_NOW() }
UPDATE { logins: OLD.logins + 1 } IN users

Обратите внимание, что в случае UPDATE можно ссылаться на предыдущую версию документа, используя псевдо-значение OLD.

Параметры запроса

ignoreErrors

Опция ignoreErrors может быть использована для подавления ошибок запроса, которые могут возникнуть при попытке нарушить ограничения уникального ключа.

keepNull

При обновлении или замене атрибута с нулевым значением ArangoDB не будет удалять атрибут из документа, а сохранит для него нулевое значение. Чтобы избавиться от атрибутов в операции upsert, установите для них значение null и предоставьте опцию keepNull.

mergeObjects

Опция mergeObjects определяет, будет ли содержимое объекта объединено, если атрибут объекта присутствует как в запросе UPDATE, так и в обновляемом документе.

Значение по умолчанию для mergeObjects равно true, поэтому нет необходимости указывать его явно.

waitForSync

Для обеспечения долговечности данных при возврате запроса обновления существует опция запроса waitForSync.

ignoreRevs

Чтобы случайно не обновить документы, которые были написаны и обновлены с момента последнего получения, вы можете использовать опцию ignoreRevs, чтобы позволить ArangoDB сравнивать значение _rev и добиваться успеха, только если они совпадают, или позволить ArangoDB игнорировать их (по умолчанию):

1
2
3
4
5
FOR i IN 1..1000
  UPSERT { _key: CONCAT('test', i)}
    INSERT {foobar: false}
    UPDATE {_rev: "1287623", foobar: true }
  IN users OPTIONS { ignoreRevs: false }

Вам необходимо добавить значение _rev в updateExpression. Оно не будет использоваться в searchExpression. Хуже того, если вы используете устаревшее значение _rev в searchExpression, UPSERT запустит путь INSERT вместо пути UPDATE, потому что он не нашел документ, точно соответствующий searchExpression.

exclusive

Движок RocksDB не требует блокировок на уровне коллекции. Различные операции записи в одну и ту же коллекцию не блокируют друг друга, пока нет конфликтов запись-запись на одних и тех же документах. С точки зрения разработки приложений может быть желательным иметь исключительный доступ на запись в коллекции, чтобы упростить разработку. Обратите внимание, что записи не блокируют чтения в RocksDB. Исключительный доступ также может ускорить запросы на модификацию, так как мы избегаем проверки конфликтов.

Используйте опцию exclusive для достижения этого эффекта на основе каждого запроса:

1
2
3
4
5
FOR i IN 1..1000
  UPSERT { _key: CONCAT('test', i) }
  INSERT { foobar: false }
  UPDATE { foobar: true }
  IN users OPTIONS { exclusive: true }

indexHint

Опция indexHint будет использоваться в качестве подсказки для поиска документа, выполняемого в рамках операции UPSERT, и может помочь в таких случаях, как UPSERT не выбирает лучший индекс автоматически.

1
2
3
4
UPSERT { a: 1234 }
  INSERT { a: 1234, name: "AB" }
  UPDATE { name: "ABC" } IN myCollection
  OPTIONS { indexHint: "index_name" }

Подсказка индекса передается во внутренний цикл FOR, который используется для поиска. Также смотрите Опция indexHint операции FOR.

forceIndexHint

Делает индекс или индексы, указанные в indexHint, обязательными, если они включены. По умолчанию false. Также смотрите Опция forceIndexHint операции FOR.

1
2
3
4
UPSERT { a: 1234 }
  INSERT { a: 1234, name: "AB" }
  UPDATE { name: "ABC" } IN myCollection
  OPTIONS { indexHint: … , forceIndexHint: true }

Возвращаемые документы

Операторы UPSERT могут по желанию возвращать данные. Для этого за ними должен следовать оператор RETURN (допускаются также промежуточные операторы LET). Эти операторы могут по желанию выполнять вычисления и ссылаться на псевдо-значения OLD и NEW. В случае, если upsert выполнил операцию вставки, OLD будет иметь значение null. В случае если upsert выполнил операцию обновления или замены, OLD будет содержать предыдущую версию документа, до обновления/замены.

NEW всегда будет заполнен. Он будет содержать вставленный документ, если upsert выполнил вставку, или обновленный/замещенный документ, если он выполнил обновление/замену.

Это также можно использовать для проверки того, выполнил ли upsert вставку или обновление:

1
2
3
4
UPSERT { name: 'superuser' }
INSERT { name: 'superuser', logins: 1, dateCreated: DATE_NOW() }
UPDATE { logins: OLD.logins + 1 } IN users
RETURN { doc: NEW, type: OLD ? 'update' : 'insert' }

Транзакционность

На одном сервере апсерты выполняются транзакционно по принципу "все или ничего".

Если используется движок RocksDB и включены промежуточные фиксации, запрос может выполнять промежуточные фиксации транзакций в случае, если запущенная транзакция (AQL-запрос) достигнет заданных пороговых значений размера. В этом случае операции запроса, выполненные до сих пор, будут зафиксированы и не будут откатаны в случае последующего отмены/отката. Это поведение можно контролировать, изменяя настройки промежуточной фиксации для движка RocksDB.

Для коллекций с шардированием весь запрос и/или операция upsert могут не быть транзакционными, особенно если они затрагивают разные шарды и/или DB-серверы.

Ограничения

  • Части поиска и вставки/обновления/замены выполняются одна за другой, так что в промежутке могут выполняться другие операции в других потоках. Это означает, что если несколько запросов UPSERT выполняются одновременно, все они могут определить, что целевой документ не существует, а затем создать его несколько раз!

    Обратите внимание, что из-за этого разрыва между поиском и вставкой/обновлением/заменой, даже при наличии уникального индекса могут возникать ошибки дублирования ключей или конфликты. Но если они возникнут, код приложения/клиента может повторно выполнить тот же самый запрос.

    Чтобы этого не произошло, необходимо добавить уникальный индекс к атрибуту(ам) поиска. Обратите внимание, что в кластере уникальный индекс может быть создан только в том случае, если он равен атрибуту ключа шарда коллекции или, по крайней мере, содержит его как часть.

    Альтернативой тому, чтобы заставить оператор UPSERT работать атомарно, является использование опции exclusive для ограничения параллелизма записи для этой коллекции до 1, что помогает избежать конфликтов, но плохо для throughput!

  • При использовании очень больших транзакций в UPSERT (например, UPSERT над всеми документами в коллекции) может быть запущен промежуточный коммит. Этот промежуточный коммит запишет данные, которые были изменены на данный момент. Однако это будет иметь побочный эффект: атомарность этой операции больше не может быть гарантирована, и ArangoDB не может гарантировать, что чтение ваших собственных записей в upsert будет работать.

1
Это будет проблемой только в том случае, если вы пишете запрос, в котором условие поиска попадает на один и тот же документ несколько раз, и только в том случае, если у вас большие транзакции. Чтобы избежать этой проблемы, вы можете увеличить пороговые значения `intermediateCommit` для количества данных и операций.
  • Атрибут(ы) поиска из поискового выражения должен быть проиндексирован для улучшения производительности UPSERT. В идеале, поисковое выражение содержит ключ шарда, так как это позволяет ограничить поиск одним шардом.

Комментарии