Если вы столкнулись с задачей хранения сильно связанных данных, то отличным вариантом будет использовать графовую модель данных. Мы в Текфорс сделали именно так. Почему - разберем в этой статье, где я: 

  • приведу общую информацию о том, где применяются графовые БД;

  • расскажу про Neo4j как один из примеров такой БД;

  • покажу на примере как использовать Neo4j через Spring Data.

Статья будет полезна тем, кто:

  • хочет расширить кругозор в плане графовых БД;

  • сомневается в правильности выбора типа БД;

  • ищет вводный материал по работе с Spring Data Neo4J.

Введение

Долгое время стандартом хранения данных были реляционные базы данных. Со временем разнообразных по структуре данных становилось все больше и стандартные способы хранения стали неудобными. В начале 2010-х стали появляться альтернативные варианты хранения, так называемые NoSql базы данных. Каждая имеет свои особенности: скорость выполнения операций, возможность хранить огромные объемы данных, линейная масштабируемость, отказоустойчивость. 

Так,  документо-ориентированная база данных MongoDB предназначена для хранения слабосвязанных данных, поступающих в виде документов; колоночная база данных ClickHouse быстро работает на вставку и получение данных, но не подразумевает их удаление и изменение. 

При выборе базы данных могут возникнуть трудности
При выборе базы данных могут возникнуть трудности

Применение графовых БД

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

  • связей так много, что таблицы связей больше таблиц данных,

  • запросы получения данных сложны и состоят из множества  join-ов, 

  • есть необходимость в частом получении данных или глубина поиска достаточно большая (>3)? 

Например, в системе по типу социальной сети нужно проверить теорию шести рукопожатий.

Такие операции в реляционных базах стоят очень дорого. И если на небольших наборах данных проблема с производительностью будет не столь очевидна, то с ростом их объема время выполнения запроса существенно возрастет. 

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

Даже  типичное изображение проблемы на доске выглядит как граф
Даже  типичное изображение проблемы на доске выглядит как граф

Есть много систем применения  графовой структуры данных.  Я поделюсь несколькими типичными примерами использования графовых баз данных: 

1. Система рекомендаций. В качестве исходных данных используются данные о продуктах, брендах, а также связи между людьми (например, детьми и родителями) и купленными ими продуктами. Имея граф таких данных можно давать пользователю рекомендации по бренду и товарам, а также строить комплексные сложные запросы и рекомендации. 

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

Также графовые БД могут использоваться в системах контроля доступа,  анализа влияния (чтобы понять, как бизнес отреагирует на сбои или для выбора оптимального сценария) и других системах.

Больше информации можно найти, например, в книге “Learning Neo4j” (Rik Van Bruggen), где подробно расписаны кейсы использования графовых БД.

Надеюсь, этот краткий обзор помог определиться, нужны ли вам графовые БД. В следующем разделе я расскажу про устройство графовых БД, СУБД Neo4j и работу с ней через Spring.

Графовая модель данных

Формально граф – это система объектов произвольной природы (вершин) и связок (ребер), соединяющих некоторые пары этих объектов. 

Графовая модель данных имеет следующие особенности:

  • состоит из набора вершин (узлов) и ребер;

  • каждая вершина имеет идентификатор, список ребер и список свойств (пары ключ-значение, где ключ - это строка);

  • каждое ребро также имеет идентификатор, ссылку на начальную и конечную вершину (стрелки из ниоткуда или вникуда быть не может) и список свойств;

  • вершины и ребра в графовой модели данных необязательно должны быть одного типа - они могут представлять различные объекты.

Графическая модель данных (Источник:  https://neo4j.com/)
Графическая модель данных (Источник:  https://neo4j.com/)

В качестве примера создадим  модель данных для системы рекомендаций, которая хранит информацию о людях и фильмах, которые они посмотрели. Каждый фильм также может относиться к нескольким жанрам.

Простая модель графа в виде схемы
Простая модель графа в виде схемы

По этой модели легко определить, что Вова - друг Маши, Маша смотрела фильм Титаник, который является драмой.

В графовой БД нет join-ов: при создании узлов и ребер вы задаете им свойства, что позволяет построить граф из данных, которые связаны непосредственно с данными. Для выполнения запроса нужно только найти стартовый узел и от него перемещаться по ребрам. Для любой вершины можно найти как ее входящие, так и исходящие ребра и таким образом выполнить обход графа, что также упрощает написание запросов на выборку данных. Таким образом, учет взаимосвязей на больших объемах данных не ухудшает производительность графовых баз данных, поскольку запросы локализуются в определенной части графа.

Neo4j является популярной графовой базой данных с открытым исходным кодом. Посмотрим, как с помощью нее реализовать описанную выше структуру данных.

Описание Neo4j

После установки Neo4j вам будет доступен Neo4j-browser (по дефолту http://localhost:7474/browser/) – инструмент для выполнения CRUD-операций в БД Neo4j. Он имеет богатый пользовательский интерфейс для визуализации данных в виде графиков. 

Граф в браузере Neo4j после выполнения команды Cypher
Граф в браузере Neo4j после выполнения команды Cypher

Для базы данных Neo4j был создан декларативный язык запросов Cypher, обеспечивающий эффективное чтение и запись данных в Neo4j.  Пользоваться им очень удобно, потому что он выразительный и компактный. 

Рассмотрим некоторые примеры запросов для описанной выше модели данных.

Вот как выглядит запрос операции добавления узлов и связей для нашего примера (добавим людей и пару фильмов с жанрами):

CREATE
(igor: Person {name:'Igor'}),
(misha: Person {name:'Misha'}),
(olya: Person {name:'Olya'}),
(am_pie: Movie {name:'American Pie'}),
(saw: Movie {name:'Saw'}),
(home_alone: Movie {name:'Home Alone'}),
(comedy: Genre {name:'Comedy'}),
(horror: Genre {name:'Horror'}),
(olya) -[:WATCHED]-> (am_pie) -[:HAS_GENRE]-> (comedy),
(olya) -[:WATCHED]-> (saw),
(olya) -[:WATCHED]-> (home_alone),
(misha) -[:WATCHED]-> (saw),
(igor) -[:WATCHED]-> (am_pie),
(home_alone) -[:HAS_GENRE]-> (comedy),
(igor) -[:IS_FRIEND]-> (misha),
(misha) -[:IS_FRIEND]-> (olya)

С помощью запроса MATCH (n) RETURN n можно получить все имеющиеся данные. В Neo4j-browser они будут представлены так:

Добавление новых типов узлов или связей не затронет существующую модель и запросы, что позволяет безопасно расширять данные при необходимости. Это является неоспоримым преимуществом графовых баз данных.

Теперь напишем запрос, который получает все фильмы-комедии, которые смотрели друзья друзей пользователя, но которые он не смотрел сам.

MATCH (igor:Person{name:'Igor'})-[:IS_FRIEND*2]->()-[:WATCHED*0..1]->
            (m:Movie)-[]->(comedy: Genre {name:'Comedy'})
WHERE not (igor)-[:WATCHED]->(m)
RETURN m

Запрос получился достаточно компактным и простым для понимания.

Подобный запрос для реляционной базы данных выглядел бы громоздким:

select m.*
from person p
left join person_friend pF1 on p.id = pF1.person_id
left join person_friend pF2 on pF1.friend_id = pF2.person_id
      left join watched watched on pF2.friend_id = watched.person_id
left join movie_genre mG on watched.movie_id = mG.movie_id
      left join genre genre on mG.genre_id = genre.id
      left join movie m on m.id = mG.movie_id
where p.name = 'Igor' and p.id<> pF2.friend_id and genre.name='Comedy'
except select m2.* from watched w2
     left join person p2 on w2.person_id = p2.id
     left join movie m2 on m2.id = w2.movie_id
    where p2.name = 'Igor'

Для обхода графа в глубину в реляционной базе пришлось бы применять рекурсивные функции, которые усложняют синтаксис и увеличивают время выполнения запроса, тогда как в языке Cypher существует компактная запись  ()-[*0..]->(), которая означает 0 или более ребер до узла.

Далее я расскажу  о работе с БД Neo4j через Spring Data.

Использование Spring data neo4j

Spring Data Neo4j является частью Spring Data и предлагает настройку объектов на основе аннотаций, а затем сопоставляет их с узлами и отношениями в базе данных Neo4j. Не так давно вышла новая версия Spring Data Neo4j 6, которая содержит принципиальные изменения. Рассмотрим отличия этой версии.

Spring Data Neo4j 5 (SDN 5) и ранние версии использовали Neo4j-OGM под капотом. 

Neo4j-OGM (Object Graph Mapper) сопоставляет узлы и отношения в графе с объектами и ссылками в доменной модели. Экземпляры объектов сопоставляются с узлами, а ссылки на объекты сопоставляются с помощью отношений или сериализуются  в свойства.

Spring Data Neo4j 6 является standalone решением без использования Neo4j-OGM. 

В новой версии мы по прежнему можем работать со связями (relationships), которые имеют свойства (properties), но не напрямую через @RelationshipEntity, а через сущности (@Node), определяющие отношения (@Relationship) для их загрузки, изменения и сохранения.

Вот пример использования Spring Data Neo4j 6  для созданного  ранее запроса.  

  1. Создаем класс для каждого типа узла:

Node("Person")
public class PersonNeo4jEntity {

   @Id
   @GeneratedValue(GeneratedValue.UUIDGenerator.class)
   private UUID id;

   @Property("name")
   private UUID name;

   @Relationship(type = "IS_FRIEND", direction = Relationship.Direction.OUTGOING)
   private List<EdgeFriendNeo4j> friends;
}
  1. Описываем каждый тип связи  в отдельном классе, помеченном аннотацией @RelationshipProperties:

@RelationshipProperties
public class EdgeFriendNeo4j {

   @Id
   @GeneratedValue
   private Long id;

   @TargetNode
   private PersonNeo4jEntity friend;

   private String property1;
   private String property2;
}
  1. Для работы с базой создадим репозиторий, который наследуется от одного из интерфейсов Spring Data, например от CrudRepository. 

public interface PersonNeo4jRepository extends CrudRepository<PersonNeo4jEntity, UUID> {}

Это позволяет нам выполнять основные CRUD-запросы к Neo4j, а также дописывать дополнительные методы запросов в терминах Spring Data.

  1. При необходимости создаем  методы получения узла:

Optional<PersonNeo4jEntity> findOneByPersonName(String name);

Результатом выполнения такого метода будет узел с загрузкой всех связей.

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

@Query("match (n:Person)-[r]->(m) \n" +
        "where n.person_name=$name\n" +
        "return n, COLLECT(r), COLLECT(m)")
List<PersonNeo4jEntity> getNodeByName(String name);

Если нужен только сам узел без связей, подойдет такой метод:

@Query("match (n:Person {person_name: $name })-[r]->()\n" +
       " return n")
PersonNeo4jEntity getNodeByName(String name);

Пара важных рабочих моментов: 

  1. Только стрелки с помощью SDN вернуть нельзя, мы должны вернуть связанный объект-узел, а из него уже получить связи. 

  2. В Cypher мы можем написать такой рабочий запрос, который  вернет только связи:

match (n)-[r]-(m) return r

Таким нехитрым образом можно работать с базой данных Neo4j через Spring Data Neo4j, что позволяет добиться единообразия в приложении, использующем Spring.

Заключение

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

Мы в Текфорс использовали базу данных Neo4j для хранения данных, необходимых для отчетной подсистемы. Они хранились в реляционной базе и по мере наполнения необходимые для отчета данные складывались в Neo4j. При запросе отчета оставалось только обратиться к заранее подготовленным данным из Neo4j. Использование специализированной базы данных позволило избежать ограничений конкретного хранилища данных и эффективно реализовать разнородные запросы. Мартин Фаулер называет такой подход polyglot persistence.

Любая модель данных будет показывать хорошие результаты, если разумно применять ее для подходящий задачи. Надеюсь, что моя статья дала вам понимание того, когда следует использовать графовые базы, а также как работать с БД Neo4j, используя Spring Data Neo4j. 

Подведем итоги: 

  • Графовые базы подойдут, если в вашем приложении данные связаны иерархически, Neo4j - пример такой БД;

  • Neo4j-browser - удобный инструмент для выполнения операций над данными и визуализации;

  • БД Neo4j имеет собственный язык запросов Cypher. Запросы на нем выглядят лаконичнее и понятнее, чем аналогичные запросы для реляционной БД;

  • Spring Data Neo4j как часть Spring Data позволяет работать с Neo4j из java-кода привычным способом.

Спасибо за внимание!

Комментарии (9)


  1. Maxmyd
    19.01.2023 17:58

    Как база данных он неплох, но браузер в нём - это что-то с чем-то.


  1. UnclShura
    19.01.2023 18:13

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


  1. fuCtor
    19.01.2023 20:38
    +1

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


    1. barloc
      20.01.2023 13:27

      Китайцы возьмут, но ценник у них был на уровне, а то и выше neo4j.

      Правда опен сурс версии за глаза.


  1. sparrowhawk
    20.01.2023 11:20

    А почему выбор остановился именно на neo4j а не на arangodb, к примеру?


  1. NickNal
    20.01.2023 11:25
    +3

    БД Neo4j имеет собственный язык запросов Cypher. Запросы на нем выглядят лаконичнее и понятнее, чем аналогичные запросы для реляционной БД;

    Чуть больше года занимался проектом на Neo4j и категорически не согласен с этим утверждением. Когда впервые увидел, что они всерьёз рекламируют Cypher как "overcoming SQL pain", просто всплакнул кровавыми слезами))

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

    Уже после опыта с Neo4j довелось поработать с графами в pgrouting, это было несказанное облегчение по всем аспектам)

    Я бы выбрал Neo4j только как вспомогательный инструмент для каких-то простых задач по поиску связей (но, правда, практически с неограниченным масштабом, это большой плюс). Если в приоритете не масштаб, а сложная логика поиска/сложный ETL на графах - полно инструментов гораздо лучше.


    1. DeepHill
      22.01.2023 04:19

      полно инструментов гораздо лучше

      А каких именно? Просто интересно)


  1. andreyverbin
    20.01.2023 14:42
    +1

    >Такие операции в реляционных базах стоят очень дорого. 

    В каждом обзоре графовых баз мне не хватает ответа на вопрос «почему?» Если нужно найти друзей друзей с какими-то условиями, то реляционная БД

    1. Index only scan чтобы найти друзей

    2. Index scan чтобы найти их друзей

    3. Читаем таблицу и фильтруем результат.

    Каким образом графовая БД сделает меньше чтений диска? А что будет когда мы начнём обновлять данные? А если нужно бродить подданным, то чем хеш индекс хуже чем структуры данных графовой БД?


  1. TheKnight
    20.01.2023 23:34

    Вместо тысячи join-ов…

    ...две тысячи джойнов...

    (не в упрек Neo4J, просто шутки ради)