Pull to refresh

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

Reading time 8 min
Views 12K

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

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

  • расскажу про 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-кода привычным способом.

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

Tags:
Hubs:
+16
Comments 17
Comments Comments 17

Articles