Как организовать миграцию схемы Neo4j и импортировать в графовую базу данные из реляционных систем. Знакомимся с инструментами проекта Neo4j Labs: Neo4j-ETL и Neo4j-Migrations.
Как работает Neo4j-ETL
В рамках развития своих продуктов, таких как графовая СУБД Neo4j и экосистема элементов вокруг нее (Graph Data Science, Neo4j Bloom, Neo4j Browser и пр.), компания-разработчик реализует проект Labs – сборник новых идей и гипотез. После тестирования в Labs исходный код проекта становится общедоступным, а сам проект переводится в статус официального проекта Neo4j или объявляется устаревшим. Проекты Labs активно разрабатываются и поддерживаются онлайн-сообществом, но при нахождении в этом репозитории компания Neo4j не предоставляет для библиотеки или приложения никаких соглашений об уровне обслуживания или гарантий в отношении обратной совместимости и устаревания.
Впрочем, несмотря на отказ от ответственности, среди проектов Labs есть довольно интересные инструменты для работы с графами. Например, инструмент Neo4j-ETL, который упрощает загрузку данных из реляционных баз в графовую СУБД. Он позволяет получить графовую модель из реляционной метамодели, чтобы затем адаптировать ее под задачи анализа графов. Помимо такого преобразования, этот инструмент также выполняет фактический импорт данных. Для интерактивного моделирования и импорта CSV-файлов можно воспользоваться инструментом Neo4j Data Importer, о котором мы писали здесь.
Neo4j-ETL поддерживает управление несколькими соединениями РСУБД, может автоматически извлекать метаданные из реляционной базы данных, позволяет получить графовую модель, а также визуально редактировать метки, типы отношений, имена и типы свойств. Инструмент визуализирует текущую модель данных в виде графа, позволяет сохранить отображение в формате JSON, получить соответствующие данные в CSV-формате из реляционных баз данных и запустить массовый или онлайн-импорт. Средство поддерживает популярные реляционные СУБД MySQL и PostgreSQL, а также может использовать собственный JDBC-драйвер в версии Enterprise для подключения к другим хранилищам.
С Neo4j-ETL весь процесс конвертации реляционных данных в графовую модель сводится к следующим шагам:
- Настройка соединения с реляционной базой данных;
- Запуск импорта данных в Neo4j;
- Проверка сопоставления схемы и корректировка графовой модели данных.
Есть несколько различных способов, с помощью которых инструмент ETL может импортировать данные в Neo4j, в зависимости от состояния графовой базы данных. Если база данных активна, т.е. работает, то используется прямой доступ (Online Direct), который запускается через BOLT-соединение для импорта, превращая результаты SQL-запросов в параметры Cypher. Также можно запустить пакетный режим онлайн с использованием CSV-файлов для пакетного импорта через BOLT-соединение. Если база данных закрыта, т.е. не работает, быстрее всего будет массовый импорт – автономный загрузчик, запускаемый через утилиту neo4j-admin.
Рефакторинг схемы данных и миграция базы
Еще одним полезным инструментом из проектов Labs можно назвать Neo4j-Migrations, который упрощает миграцию схемы данных. Он предоставляет приложениям, командной строке и инструментам сборки единый способ отслеживания, управления и применения изменений в базе данных. Он во многом похож на Flyway – ПО с открытым исходным кодом для миграции баз данных, но поддерживает работу с Cypher-скриптами, активно используемыми в Neo4j. Neo4j-Migrations построен непосредственно на основе официального Neo4j-Java-Driver и поддерживает версии графовой СУБД от 3.5 до 4.4, включая корпоративные функции, такие как поддержка нескольких баз данных и олицетворение.
В Neo4j-Migrations все модули имеют паритет функций: команды в API отражаются в CLI, в Maven, а также в интеграции Spring Boot. Поскольку инструмент непосредственно основан на официальном драйвере Neo4j-Java (Bolt), JDBC-драйвер для подключения к базе данных не требуется. Средство хорошо работает с динамическими средами, включая облачную базу данных Neo4j Aura, за счет использования транзакционных функций во всем стеке. Можно использовать другую базу данных для хранения информации о миграции и для применения фактической миграции, отделяя одну базу данных от другой. Neo4j-Migrations поддерживает олицетворение Core Java API с гарантированным семантическим управлением версиями и предоставляет Spring Boot Starter, который подключается к официальной поддержке Spring Boot для Neo4j. Благодаря этому миграция становится проще, поскольку нужно предоставлять только сценарии этого процесса. Также инструмент предоставляет плагин Maven для запуска миграций во время сборки и имеет собственные инструменты CLI для Linux, macOS и Windows. Для выполнения миграции через собственный CLI даже не требуется Java. Впрочем, поскольку Java API позволяет выполнять индивидуальные миграции, написанные на Java, его полезно использовать для особых случаев, когда возможностей встроенного языка запросов Cypher недостаточно.
Единственными зависимостями в Neo4j-Migrations являются указанный драйвер и ClassGraph для поиска миграций в пути к классам. История примененных миграций хранится в виде подграфа в базе данных. В качестве основного формата представления данных инструмент использует XML, позволяя в XSD-документе определить особенности миграции с исходного экземпляра гафовой СУБД в целевой. XML выбран из-за встроенной поддержки этого формата непосредственно в JVM, благодаря чему нет необходимости вводить какие-либо дополнительные зависимости для анализа и проверки контента.
Например, полная XML-схема для миграции на основе каталога, когда каждая обнаруженная миграция будет записываться в каталог, известный в контексте экземпляра Migration, выглядит следующим образом:
<?xml version="1.0" encoding="UTF-8" ?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="https://your_graph_database/neo4j-migrations" xmlns="https://your_graph_database/neo4j-migrations" elementFormDefault="qualified"> <xs:element name="migration" type="migration"/> <xs:complexType name="migration"> <xs:sequence> <xs:element name="catalog" minOccurs="0" type="catalog"/> <xs:element name="verify" minOccurs="0" type="verifyOperation" /> <xs:choice> <xs:choice maxOccurs="unbounded"> <xs:element name="refactor" minOccurs="0" maxOccurs="unbounded" type="refactoring"/> <xs:choice maxOccurs="unbounded"> <xs:element name="create" minOccurs="0" maxOccurs="unbounded" type="createOperation"/> <xs:element name="drop" minOccurs="0" maxOccurs="unbounded" type="dropOperation"/> </xs:choice> </xs:choice> <xs:element name="apply" minOccurs="0" type="applyOperation"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="refactoring"> <xs:sequence minOccurs="0"> <xs:element name="parameters"> <xs:complexType> <xs:sequence maxOccurs="unbounded"> <xs:any processContents="lax"/> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="type"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="merge.nodes"/> <xs:enumeration value="migrate.createFutureIndexes"/> <xs:enumeration value="migrate.replaceBTreeIndexes"/> <xs:enumeration value="normalize.asBoolean"/> <xs:enumeration value="rename.label"/> <xs:enumeration value="rename.type"/> <xs:enumeration value="rename.nodeProperty"/> <xs:enumeration value="rename.relationshipProperty"/> <xs:enumeration value="addSurrogateKeyTo.nodes"/> <xs:enumeration value="addSurrogateKeyTo.relationships"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> <xs:complexType name="catalog"> <xs:all> <xs:element name="constraints" minOccurs="0"> <xs:complexType> <xs:sequence> <xs:element type="constraint" name="constraint" maxOccurs="unbounded" minOccurs="0"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="indexes" minOccurs="0"> <xs:complexType> <xs:sequence> <xs:element type="index" name="index" maxOccurs="unbounded" minOccurs="0"/> </xs:sequence> </xs:complexType> </xs:element> </xs:all> <xs:attribute name="reset" type="xs:boolean" default="false"/> </xs:complexType> <xs:complexType name="operation" /> <xs:complexType name="applyOperation"> <xs:complexContent> <xs:extension base="operation" /> </xs:complexContent> </xs:complexType> <xs:complexType name="verifyOperation"> <xs:complexContent> <xs:extension base="operation" > <xs:attribute name="useCurrent" type="xs:boolean" default="false"/> <xs:attribute name="allowEquivalent" type="xs:boolean" default="true"/> <xs:attribute name="includeOptions" type="xs:boolean" default="false"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="itemOperation"> <xs:complexContent> <xs:extension base="operation"> <xs:sequence> <xs:choice minOccurs="0"> <xs:element name="constraint" type="constraint"/> <xs:element name="index" type="index"/> </xs:choice> </xs:sequence> <xs:attribute name="item" type="xs:string"/> <xs:attribute name="ref" type="xs:IDREF"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="createOperation"> <xs:complexContent> <xs:extension base="itemOperation"> <xs:attribute name="ifNotExists" type="xs:boolean" default="true"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="dropOperation"> <xs:complexContent> <xs:extension base="itemOperation"> <xs:attribute name="ifExists" type="xs:boolean" default="true"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="properties"> <xs:sequence> <xs:element type="xs:string" name="property" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="catalogItem"> <xs:attribute name="name" use="required" type="xs:ID"/> </xs:complexType> <xs:complexType name="constraint"> <xs:complexContent> <xs:extension base="catalogItem"> <xs:sequence> <xs:choice> <xs:element name="label" type="xs:string"/> <xs:element name="type" type="xs:string"/> </xs:choice> <xs:element type="properties" name="properties"/> <xs:element type="xs:string" name="options" minOccurs="0"/> </xs:sequence> <xs:attribute name="type" use="required"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="unique"/> <xs:enumeration value="exists"/> <xs:enumeration value="key"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="index"> <xs:complexContent> <xs:extension base="catalogItem"> <xs:sequence> <xs:choice> <xs:element name="label" type="xs:string"/> <xs:element name="type" type="xs:string"/> </xs:choice> <xs:element type="properties" name="properties"/> <xs:element type="xs:string" name="options" minOccurs="0"/> </xs:sequence> <xs:attribute name="type"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="property" /> <xs:enumeration value="fulltext"/> <xs:enumeration value="text"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:complexContent> </xs:complexType> </xs:schema>
Внутри Neo4j-Migrations реализована концепция каталога, который содержит объекты того же типа, что и схема. При миграции можно выбирать элементы из каталога для определения окончательной схемы. Элементы могут находиться в каталоге несколько раз, что определяется их идентификатором и версией миграции, в которой они были определены. Это сделано для того, чтобы, например, операция удаления могла ссылаться на последнюю версию объекта, примененную к схеме, а не на последнюю версию, в которой свойства или параметры могут быть изменены. Рефакторинги существуют как общая концепция в каталоге, их не нужно определять, а просто объявлять как операцию, которую необходимо выполнить. Каталог определяется в двух вариантах: удаленный и локальный. Удаленный каталог, определенный схемой базы данных, доступен только для чтения всех элементов, содержащихся в схеме, включая ограничения и индексы. Именно ограничения и индексы по сути представляют собой схему графовой базы данных Neo4j, которая, как и многие NoSQL-решения позиционируется как безсхемное хранилище. Однако, на практике это все же не так: любые данные имеют свою структуру, в т.ч. те, которые представляют собой вершины и ребра графа, поскольку у них есть имена, метки и свойства.
Удаленный каталог в Migrations можно получить по требованию в любое время. Локальный каталог строится итеративно при обнаружении миграций. Миграции на основе каталога считываются в порядке версий. Таким образом, Migrations — это набор инструментов, которые максимально упрощают миграцию схемы графовой базы данных, предоставляя приложениям, командной строке и инструментам сборки единый способ отслеживания, управления и применения изменений в базе данных. Это особенно полезно для рефакторинга, когда нужно улучшить схему графа.
Узнайте больше про использование графовых алгоритмов и средств работы с ними для практического применения в реальных проектах аналитики больших данных на специализированных курсах нашего лицензированного учебного центра обучения и повышения квалификации для разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве:
Источники