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

Запросы данных

Существует два основных типа запросов AQL:

  • запросы, которые обращаются к данным (читают документы)
  • запросы, которые изменяют данные (создают, обновляют, заменяют, удаляют документы)

Запросы доступа к данным

Извлечение данных из базы данных с помощью AQL всегда включает операцию RETURN. Его можно использовать для возврата статического значения, например строки:

1
RETURN "Hello ArangoDB!"

Результатом запроса всегда является массив элементов, даже если был возвращен один элемент, и в этом случае он содержит один элемент: ["Hello ArangoDB!"].

Функцию DOCUMENT() можно вызвать для получения одного документа через его дескриптор документа, например:

1
RETURN DOCUMENT("users/phil")

RETURN обычно сопровождается циклом FOR для перебора документов коллекции. Следующий запрос выполняет тело цикла для всех документов коллекции, называемой пользователями. В этом примере каждый документ возвращается без изменений:

1
2
FOR doc IN users
  RETURN doc

Вместо того, чтобы возвращать необработанный документ, можно легко создать проекцию:

1
2
FOR doc IN users
  RETURN { user: doc, newAttribute: true }

Для каждого пользовательского документа возвращается объект с двумя атрибутами. Значением атрибута user установлено содержимое пользовательского документа, а newAttribute — это статический атрибут с логическим значением true.

В тело цикла можно добавить такие операции, как FILTER, SORT и LIMIT, чтобы сузить и упорядочить результат. Вместо показанного выше вызова DOCUMENT() можно также получить документ, который описывает пользователя phil следующим образом:

1
2
3
FOR doc IN users
  FILTER doc._key == "phil"
  RETURN doc

В этом примере используется ключ документа, но любой другой атрибут может также использоваться для фильтрации. Поскольку ключ документа гарантированно уникален, этому фильтру может соответствовать не более одного документа. Для других атрибутов это может быть не так. Чтобы вернуть подмножество активных пользователей (определяемых атрибутом, называемым status), отсортированных по имени в порядке возрастания, вы можете сделать:

1
2
3
4
FOR doc IN users
  FILTER doc.status == "active"
  SORT doc.name
  LIMIT 10

Обратите внимание, что операции не обязательно должны выполняться в фиксированном порядке и что их порядок может существенно повлиять на результат. Ограничение количества документов перед фильтром, как правило, не то, что вам нужно, потому что оно легко упускает множество документов, которые удовлетворяют критерию фильтра, но игнорируются из-за преждевременного предложения LIMIT. По вышеупомянутым причинам LIMIT обычно ставится в самом конце, после FILTER, SORT и других операций.

См. «Операции высокого уровня» для получения более подробной информации.

Запросы на изменение данных

AQL поддерживает следующие операции модификации данных:

  • INSERT: вставлять новые документы в коллекцию
  • UPDATE: частично обновить существующие документы в коллекции
  • REPLACE: полностью заменить существующие документы в коллекции
  • REMOVE: удалить существующие документы из коллекции
  • UPSERT: условно вставить или обновить документы в коллекции

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

Ниже вы найдете несколько простых примеров запросов, в которых используются эти операции. Операции подробно описаны в главе «Операции высокого уровня».

Изменение одного документа

Начнем с основ: операции INSERT, UPDATE и REMOVE для отдельных документов. Вот пример, который вставляет документ в коллекцию users с помощью операции INSERT:

1
2
3
4
5
INSERT {
  firstName: "Anna",
  name: "Pavlova",
  profession: "artist"
} INTO users

Коллекция должна существовать до выполнения запроса. Запросы AQL не могут создавать коллекции.

Если вы запустите приведенный выше запрос, результатом будет пустой массив, потому что мы не указали, что возвращать, используя ключевое слово RETURN. Это необязательно в запросах на изменение, но обязательно в запросах на доступ к данным. Несмотря на пустой результат, приведенный выше запрос по-прежнему создает новый пользовательский документ.

Вы можете предоставить ключ для нового документа; если он не указан, ArangoDB создаст его для вас.

1
2
3
4
5
6
INSERT {
  _key: "GilbertoGil",
  firstName: "Gilberto",
  name: "Gil",
  city: "Fortalezza"
} INTO users

Поскольку ArangoDB не содержит схем, атрибуты документов могут различаться:

1
2
3
4
5
6
7
INSERT {
  _key: "PhilCarpenter",
  firstName: "Phil",
  name: "Carpenter",
  middleName: "G.",
  status: "inactive"
} INTO users
1
2
3
4
5
6
INSERT {
  _key: "NatachaDeclerck",
  firstName: "Natacha",
  name: "Declerck",
  location: "Antwerp"
} INTO users

Операция UPDATE позволяет добавлять или изменять атрибуты существующих документов. Следующий запрос изменяет ранее созданного пользователя, изменяя атрибут status и добавляя атрибут location:

1
2
3
4
UPDATE "PhilCarpenter" WITH {
  status: "active",
  location: "Beijing"
} IN users

Операция REPLACE является альтернативой операции UPDATE, которая позволяет заменить все атрибуты документа (кроме атрибутов, которые нельзя изменить, например _key):

1
2
3
4
5
6
7
REPLACE {
  _key: "NatachaDeclerck",
  firstName: "Natacha",
  name: "Leclerc",
  status: "active",
  level: "premium"
} IN users

Вы можете удалить документ с помощью операции REMOVE, требуя только ключ документа для его идентификации:

1
REMOVE "GilbertoGil" IN users

Изменение нескольких документов

Операции модификации данных обычно сочетаются с циклами FOR для перебора заданного списка документов. При желании их можно комбинировать с операторами FILTER и т. п.

Чтобы создать несколько новых документов, используйте операцию INSERT вместе с FOR. Вы также можете использовать INSERT для создания копий существующих документов из других коллекций или для создания синтетических документов (например, в целях тестирования). Следующий запрос создает 1000 тестовых пользователей с некоторыми атрибутами и сохраняет их в коллекции users:

1
2
3
4
5
6
7
8
9
FOR i IN 1..1000
  INSERT {
    id: 100000 + i,
    age: 18 + FLOOR(RAND() * 25),
    name: CONCAT('test', TO_STRING(i)),
    status: i % 2 == 0 ? "active" : "not active",
    active: false,
    gender: i % 3 == 0 ? "male" : i % 3 == 1 ? "female" : "diverse"
  } IN users

Давайте изменим существующие документы, соответствующие некоторому условию:

1
2
3
FOR u IN users
  FILTER u.status == "not active"
  UPDATE u WITH { status: "inactive" } IN users

Вы также можете обновить существующие атрибуты на основе их предыдущего значения:

1
2
3
FOR u IN users
  FILTER u.active == true
  UPDATE u WITH { numberOfLogins: u.numberOfLogins + 1 } IN users

Приведенный выше запрос работает только в том случае, если в документе уже присутствует атрибут numberOfLogins. Если неясно, есть ли в документе атрибут numberOfLogins, увеличение необходимо сделать условным:

1
2
3
4
5
FOR u IN users
  FILTER u.active == true
  UPDATE u WITH {
    numberOfLogins: HAS(u, "numberOfLogins") ? u.numberOfLogins + 1 : 1
  } IN users

Обновления нескольких атрибутов могут быть объединены в один запрос:

1
2
3
4
5
6
FOR u IN users
  FILTER u.active == true
  UPDATE u WITH {
    lastLogin: DATE_NOW(),
    numberOfLogins: HAS(u, "numberOfLogins") ? u.numberOfLogins + 1 : 1
  } IN users

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

Вы можете копировать документы из одной коллекции в другую, читая из одной коллекции и записывая в другую. Скопируем содержимое коллекции users в коллекцию backup:

1
2
FOR u IN users
  INSERT u IN backup

Обратите внимание, что обе коллекции должны уже существовать на момент выполнения запроса. Запрос может завершиться ошибкой, если коллекция backup уже содержит документы, так как выполнение вставки может попытаться снова вставить тот же документ (определяемый атрибутом _key). Это вызывает нарушение ограничения уникального ключа и прерывает запрос. В режиме одного сервера все изменения, сделанные запросом, также откатываются. Чтобы такая операция копирования работала во всех случаях, целевую коллекцию можно предварительно очистить, используя запрос REMOVE или усекая ее другими способами.

Чтобы не просто частично обновить, а полностью заменить существующие документы, воспользуйтесь операцией REPLACE. Следующий запрос заменяет все документы в коллекции backup документами, найденными в коллекции users. Документы, общие для обоих сборников, заменены. Все остальные документы остаются без изменений. Документы сравниваются по их атрибутам _key:

1
2
FOR u IN users
  REPLACE u IN backup

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

Чтобы сделать запрос успешным независимо от ошибок, используйте опцию запроса ignoreErrors:

1
2
FOR u IN users
  REPLACE u IN backup OPTIONS { ignoreErrors: true }

Это продолжает выполнение запроса, если во время операции REPLACE, UPDATE, INSERT или REMOVE возникают ошибки.

Наконец, давайте найдем несколько документов в коллекции users и удалим их из коллекции backup. Связь между документами в обеих коллекциях устанавливается через ключи документов:

1
2
3
FOR u IN users
  FILTER u.status == "deleted"
  REMOVE u IN backup

В следующем примере удаляются все документы как из users, так и из backup:

1
2
3
LET r1 = (FOR u IN users  REMOVE u IN users)
LET r2 = (FOR u IN backup REMOVE u IN backup)
RETURN true

Изменение подструктур

Чтобы изменить списки в документах, например, чтобы обновить определенные атрибуты объектов в массиве, вы можете вычислить новый массив, а затем обновить соответствующий атрибут документа. Это может включать использование подзапросов и временных переменных.

Создайте коллекцию с именем complexCollection и выполните следующий запрос:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FOR doc IN [
  {
    "topLevelAttribute": "a",
    "subList": [
      {
        "attributeToAlter": "value to change",
        "filterByMe": true
      },
      {
        "attributeToAlter": "another value to change",
        "filterByMe": true
      },
      {
        "attributeToAlter": "keep this value",
        "filterByMe": false
      }
    ]
  },
  {
    "topLevelAttribute": "b",
    "subList": [
      {
        "attributeToAlter": "keep this value",
        "filterByMe": false
      }
    ]
  }
] INSERT doc INTO complexCollection

Следующий запрос обновляет атрибут верхнего уровня документов subList. Значения attributeToAlter во вложенном объекте изменяются, если соседний атрибут filterByMe имеет значение true:

1
2
3
4
5
6
7
8
9
FOR doc in complexCollection
  LET alteredList = (
    FOR element IN doc.subList
      RETURN element.filterByMe
        ? MERGE(element, { attributeToAlter: "new value" })
        : element
  )
  UPDATE doc WITH { subList: alteredList } IN complexCollection
  RETURN NEW
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[
  {
    "_key": "2607",
    "_id": "complexCollection/2607",
    "_rev": "_fWb_iOO---",
    "topLevelAttribute": "a",
    "subList": [
      {
        "attributeToAlter": "new value",
        "filterByMe": true
      },
      {
        "attributeToAlter": "new value",
        "filterByMe": true
      },
      {
        "attributeToAlter": "keep this value",
        "filterByMe": false
      }
    ]
  },
  {
    "_key": "2608",
    "_id": "complexCollection/2608",
    "_rev": "_fWb_iOO--_",
    "topLevelAttribute": "b",
    "subList": [
      {
        "attributeToAlter": "keep this value",
        "filterByMe": false
      }
    ]
  }
]

Чтобы повысить производительность запроса, вы можете обновлять документы только в том случае, если в subList есть изменения, которые необходимо сохранить. Вместо непосредственного сравнения текущего и измененного списка вы можете сравнить их хеш-значения с помощью функции HASH(), которая работает быстрее для больших объектов и массивов. Вы также можете заменить подзапрос встроенным выражением:

1
2
3
4
5
6
7
8
9
FOR doc in complexCollection
  LET alteredList = doc.subList[*
    RETURN CURRENT.filterByMe
    ? MERGE(CURRENT, { attributeToAlter: "new value" })
    : CURRENT
  ]
  FILTER HASH(doc.subList) != HASH(alteredList)
  UPDATE doc WITH { subList: alteredList } IN complexCollection
  RETURN NEW

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

Запросы на изменение данных могут дополнительно возвращать документы. Чтобы сослаться на вставленные, удаленные или измененные документы в операторе RETURN, операторы модификации данных вводят псевдозначения OLD и/или NEW:

1
2
3
FOR i IN 1..100
  INSERT { value: i } IN test
  RETURN NEW
1
2
3
4
FOR u IN users
  FILTER u.status == "deleted"
  REMOVE u IN users
  RETURN OLD
1
2
3
4
FOR u IN users
  FILTER u.status == "not active"
  UPDATE u WITH { status: "inactive" } IN users
  RETURN NEW

NEW относится к вставленной или измененной версии документа, а OLD относится к версии документа перед обновлением или удалением. Операторы INSERT могут ссылаться только на псевдозначение NEW, а операции REMOVE — только на OLD. UPDATE, REPLACE и UPSE`RT могут относиться к любому из них.

Во всех случаях возвращаются полные документы со всеми их атрибутами, включая потенциально автоматически сгенерированные атрибуты, такие как _id, _key и _rev, и атрибуты, не указанные в выражении частичного обновления.

Проекции документов OLD и NEW

Можно вернуть проекцию документов OLD или NEW вместо возврата всего документа. Это можно использовать для уменьшения количества данных, возвращаемых запросами.

Например, следующий запрос возвращает только ключи вставленных документов:

1
2
3
FOR i IN 1..100
  INSERT { value: i } IN test
  RETURN NEW._key

Использование OLD и NEW в одном запросе

Для операций UPDATE, REPLACE и UPSERT можно использовать как OLD, так и NEW, чтобы вернуть предыдущую версию документа вместе с обновленной версией:

1
2
3
4
FOR u IN users
  FILTER u.status == "not active"
  UPDATE u WITH { status: "inactive" } IN users
  RETURN { old: OLD, new: NEW }

Вычисления в OLD или NEW

Также можно выполнить дополнительные вычисления с операторами LET между частью изменения данных и окончательным RETURN запроса AQL. Например, следующий запрос выполняет операцию upsert и возвращает информацию о том, был ли обновлен существующий документ или вставлен новый документ. Он делает это, проверяя переменную OLD после UPSERT и используя оператор LET для сохранения временной строки для типа операции:

1
2
3
4
5
UPSERT { name: "test" }
  INSERT { name: "test" }
  UPDATE { } IN users
LET opType = IS_NULL(OLD) ? "insert" : "update"
RETURN { _key: NEW._key, type: opType }

Ограничения

Имя измененной коллекции (users и backup в приведенных выше случаях) должно быть известно исполнителю AQL во время компиляции запроса и не может изменяться во время выполнения. Допускается использование параметра привязки для указания имени коллекции.

Невозможно использовать несколько операций модификации данных для одной и той же коллекции в одном запросе или выполнять операцию модификации данных для определенной коллекции операцией чтения для той же коллекции. Также невозможно выполнить какую-либо операцию модификации данных с помощью запроса обхода (который может считывать из произвольных коллекций, не обязательно известных в начале обхода).

Это означает, что вы не можете помещать несколько операторов REMOVE или UPDATE для одной и той же коллекции в один и тот же запрос. Однако можно изменять разные коллекции, используя несколько операций модификации данных для разных коллекций в одном запросе. Если у вас есть запрос с несколькими местами, в котором необходимо удалить документы из одной коллекции, рекомендуется собрать эти документы или их ключи в массив и удалить документы из этого массива с помощью одной операции REMOVE.

За операциями модификации данных могут дополнительно следовать операции LET для выполнения дальнейших вычислений и операция RETURN для возврата данных.

Транзакционное исполнение

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

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

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

Комментарии