ArangoDB гибридная (документная и графовая) база данных. К ее положительным сторонам относятся:
- мощный и удобный язык запросов AQL
- JOIN (даже более мощный чем в реляционных базах данных)
- репликация и шардинг
- ACID (в кластере работает только в платной версии)
Из менее существенных, но не менее удобных возможностей:
- нечеткий поиск
- встроенный в базу данных движок микросервисов Foxx
- работа в режиме подписки на изменения в базе данных
Справедливости ради отмечу и недостатки:
- отсутствие ODM
- низкая популярность (в сравнении например с MongoDB)
После анализа возможностей ArangoDB и, в особенности, после преодоления в последних версиях недостатков (таких как резкое падение производительности при превышении размера коллекции доступной оперативной памяти) и появлении новых возможностей (таких как нечеткий поиск) — пришло время испытаний в реальном приложении.
Возможности AQL (ArangoDB Query Language)
Один из главных вопросов, который меня волновал, будет ли выразительность AQL достаточной для выполнения всего спектра запросов в реальном приложении. И будет ли работа без ORM/ODM достаточно комфортной.
В ArangoDB есть несколько способов сделать запрос к данным. Есть привычный для тех, кто работает с MongoDB, объектно-ориентированный API, но такой способ в ArangoDB считается устаревшим и основной упор делается на запросы AQL.
Простейший запрос к одной коллекции выглядит так:
db.query({
query: `for doc in managers
filter doc.role == @role
sort doc.@field @order
limit @page * @perPage, @perPage
return doc`,
bindVars: { role, page, perPage, field, order },
});
Такой вот интересный язык запросов, построенный на ключевом слове FOR, которое в данном случае не означает перебор всех документов коллекции, если, конечно, по полю role создан индекс.
В большинстве случаев, для работы приложения нужно выбрать связанные объекты из нескольких коллекций. В библиотеке mongoose (MongoDB) для этого используют метод populate(). В ArangoDB это можно сделать одним запросом AQL:
db.query({
query: `
for mall in malls
for city in cities
filter mall.cityId == city._key
return merge(mall, { city })
`,
bindVars: { },
});
Это типичный INNER JOIN. Только немного удобнее, так как объект city будет присутствовать в виде вложенного объекта, а не сольётся в список полей, как это происходит в стандартном SQL.
Что касается LEFT JOIN — для его реализации нужно использовать подзапросы и ключевое слово LET:
db.query({
query: `
for city in cities
let malls=(
for mall in malls
filter mall.cityId==city._key
return mall
)
return merge(city, {malls})`,
bindVars: { },
});
Результирующий объект будет содержать поле malls типа array или значение null. Как Вы можете заметить, есть отличие от LEFT JOIN в стандартном SQL — это то, что количество объектов в результирующей коллекции будет равно количеству объектов в коллекции city, и не будет повторяться для каждого значение mall. Вместо этого mall представлено массивом. Я бы сказал, что такой вариант даже более удобен для работы. Получить же "классический" результат, как в SQL, также можно, но запрос будет более сложный.
Я привел самые простые запросы из тех, что есть в реальном приложении. Но, как выяснилось, на базе вышеперечисленных средств можно строить самые сложные запросы, которые не менее, а может быть и более выразительны, чем запросы SQL. При этом умолчу о других документо-ориентированных NoSQL базах данных, где аналогичные запросы просто невозможны.
Графы
Для реализации граф-ориентированных возможностей в ArangoDB применяются коллекции ребер графа. Документы в этой коллекции отличаются от документов в простых коллекциях наличием двух служебных полей: _from и _to. Работать с коллекциями ребер графом можно теми же средствами, что и с коллекциями документов. В дополнение существует несколько специальных средств для обходя графов.
Я планировал реализовать на граф-ориентированных возможностях дерево категорий товаров. Однако, реализация операций update оказалась неожиданно сложной. Поэтому я отказался от этой идеи. Возможно, просто не нашел еще ключ к этим возможностям.
Нечеткий поиск
Есть такая часто встречающаяся задача: искать текст, если в исходной строке есть опечатки или ошибки. Как правило, для этого используется база данных Elacticsearch. У такого решения есть два недостатка. Во-первых, нужно согласовывать в режиме реального времени значения в основной базе данных и в Elasticsearch. Это непросто, часто эти значения расходятся, и тогда приходится принудительно переиндексировать базу данных. И, во-вторых, Elasticsearch требовательна по ресурсам, что также не всегда приемлемо из соображений финансового порядка.
В последних версиях ArangoDB можно создать SEARCH VIEW в котором можно искать значения с неполным совпадением:
await db.createAnalyzer('fuzzy_brand_search_bigram', {
type: 'ngram',
properties: { min: 2, max: 2, preserveOriginal: true },
features: ['position', 'frequency', 'norm'],
});
await db.createView('brandSearch', {
links: {
brands: {
includeAllFields: true,
analyzers: ['fuzzy_brand_search_bigram'],
},
},
});
Сам запрос выглядит так:
db.query({
query: `
for brand in brandSearch
search NGRAM_MATCH(
brand.name,
@brandName,
0.4,
'fuzzy_brand_search_bigram'
)
filter brand.mallId == @mallId
return brand `,
bindVars: { mallId, brandName },
});
Без ODM?
В своей статье я показал, что по статистике, MongoDB в половине случаев используется без ODM. То есть, это достаточно распространенная практика.
Действительно, сделать запрос, как это было показано выше, гораздо проще средствами AQL, чем определять схему с разными видами связей. Во всяком случае, не было еще ни одного проекта на Sequelize (ORM для реляционных баз данных), где не пришлось бы сделать один-два RAW запроса.
Однако, я, тем не менее, сторонник использования ODM. В своей статье я описал, что я хотел бы от ODM для ArangoDB. ODM не обязательно должна заниматься генерацией запросов в базу данных. Я бы хотел, чтобы ODM обеспечивала сохранение в базу данных только нужных полей, и следила за наличием обязательных полей. А при получении объекта из базы данных типизировала его, добавляла вычислимые поля, фильтровала набор полей для разных групп запросов, и обеспечивала локализацию значений полей.
В настоящее время я нашел всего один фреймвёрк, который очень близок к тому, что я хочу получить. Но мне в нем не хватает двух возможностей. Во-первых для методов типа PATCH входной объект, как правило, содержит не все, а только изменяемые поля. Для таких запросов нужно отключать полные правила валидации. И, во-вторых, там невозможно сделать локализацию значений. Я незамедлительно создал два issue в этом репозитарии. К чести автора, он ответил почти мгновенно, но ответ меня далеко не устроил. По первому вопросу он рекомендовал сначала забирать полный объект из базы данных, а затем мерджить его с объектом с неполным набором полей. По второму порекомендовал локализацию делать на фронтенде.
В своей статье я описал и реализовал свою библиотеку. Её и использовал в реальном проекте. Конечно, были моменты стресса, когда выходило, что возможностей этой библиотеки недостаточно. Но их в основном удалось разрешить. Так что по-прежнему приглашаю к сотрудничеству желающих продвигать технологию ArangoDB.
apapacy@gmail.com
15 марта 2021 года
debagger
Язык запросов чем-то напоминает LINQ дотнетовский.