Зачем проверять подключение к Neo4j, какую URI-схему выбрать, чем плохи транзакции с автофиксацией и как передавать переменные в Cypher-запросы: рекомендации по использованию драйверов графовой СУБД в реальных приложениях аналитики больших данных.
Драйверы и особенности подключения к базе данных
Напомним, драйвер – это сущность, которая реализует определённые API-интерфейсы для взаимодействия с сервером базы данных. По сути, драйвер базы данных является прослойкой между пользовательским кодом и базой данных, обеспечивая переносимость приложения на разные базы без привязки к движку СУБД. Для драйверов на основе СУБД обычно требуется сложный набор сведений о соединении, например, сетевой адрес, протокол, имя базы данных и пр.
Как и в любой СУБД, драйверы позволяют интегрировать Neo4j с нужным языком программирования. Neo4j предоставляет драйверы, которые позволяют разработчику подключаться к базе данных, создавая приложения, которые записывают, считывают, обновляют и удаляют информацию из графа. Neo4j официально поддерживает драйверы для .Net, Java, JavaScript, Go и Python для бинарного протокола Bolt. Драйверы представляют собой тяжеловесные объекты, которые предполагается многократное повторное использование. После создания экземпляра драйвера следует проверить подключение, например:
from neo4j import GraphDatabase uri = "neo4j+s://my-aura-instance.databases.neo4j.io" driver = GraphDatabase.driver(uri, auth=("neo4j", "password")) driver.verify_connectivity()
Обычно драйвер Neo4j создает соединения по мере необходимости и управляет ими в пуле. Проверка подключения в самом начале работы, заставляет драйвер создать подключение в этот момент. Таким образом можно предупредить ошибки, связанные с неверным адресом или учетными записями. Для версий Neo4j после 4.0 лучше всего подойдет URI-схема подключения neo4j+s://. Она работает с защищенными базами данных с одним экземпляром, а также с кластерами, выполняет проверку сертификата и обеспечивает безопасное соединение. Схема подключения Bolt:// подключается только к одному компьютеру и не очень подходит для кластерного режима.
Объекты-драйверы в Neo4j содержат пулы соединений, и их создание может длиться долго, порядка нескольких секунд, т.к. помимо самого подключения, производится установка всех необходимых соединений. В результате приложение должно создать только один экземпляр драйвера для каждой СУБД Neo4j, сохранить его и использовать для всего. Это особенно важно в таких средах, как AWS Lambda и других типах бессерверных облачных функциях, где создание экземпляра драйвера каждый раз при запуске кода снижает производительность.
Cypher-транзакции в Neo4j
Напомним, в Neo4j транзакции — это атомарные единицы работы, состоящие из одного или нескольких исполнений оператора языка запросов Cypher. Транзакция выполняется в рамках сеанса. Для выполнения оператора Cypher требуются две части информации: шаблон оператора и набор параметров с ключом. Шаблон представляет собой строку, которая наполняется значениями параметров во время выполнения. Несмотря на то, что можно запускать Cypher-код без параметров, хорошей практикой считается использование параметров в операторах. Это позволяет кэшировать операторы в движке Cypher, что полезно для производительности. API драйвера Neo4j предусматривает три формы транзакций:
- транзакции с автоматической фиксацией, которые отправляются в сеть и немедленно подтверждаются. Несколько транзакций не могут совместно использовать сетевые пакеты, что снижает эффективность использования сети. Auto-commit не предназначены для применения в production, когда важно обеспечить производительность или отказоустойчивость приложения. Однако, автофиксация транзакций — единственный способ выполнить Cypher-оператор USING PERIODIC COMMIT, который может предотвратить нехватку памяти при импорте больших объемов данных с помощью функции LOAD CSV, нарушив изоляцию транзакций.
- транзакционные функции (Transaction functions), которые могут автоматически воспроизводится после сбоя, требуют минимум шаблонного кода, а также позволяют четко разделить запросы к базе данных и логику приложения. Функции транзакций также могут обрабатывать проблемы с подключением и временные ошибки, используя механизм автоматического повтора, который можно настроить в конструкции драйвера. Любые результаты запроса, полученные внутри функции транзакции, должны использоваться внутри нее. Функции транзакций могут возвращать значения, но это должны быть производные значения, а не необработанные результаты. Именно эту форму рекомендуется использовать в реальных проектах, т.е. production-среде.
- явные транзакции (Explicit transactions) — сокращенная форма функций транзакций, обеспечивающая доступ к явным операциям BEGIN, COMMIT и ROLLBACK.
Про тонкости обработки транзакций с помощью механизма закладок мы рассказываем в следующей статье, а пока рассмотрим, почему форма Transaction functions считается лучшей практикой. Драйверы Neo4j не анализируют пользовательский Cypher-запрос, а кластеры графовой СУБД используют маршрутизируемые драйверы. Поэтому в случае транзакций с автоматической фиксацией все запросы на чтение и на запись всегда будут отправляться лидеру кластера. Таким образом, к примеру, 2/3 узлов кластера из 3 машин будут простаивать, поскольку все запросы направились на один и тот же компьютер. Другой недостаток использования транзакций session.run и Auto-commit заключается в потере контроля над поведением фиксации: становится невозможным запустить несколько Cypher-запросов, которые все фиксируются один за другим по отдельности, а не одновременно.
Таким образом, вместо следующего кода с авто-фиксацией транзакции:
with driver.session() as session: session.run("CREATE (p:Person { name: $name })", name="Ann")
рекомендуется использовать транзакционную функцию:
def add_person(self, name): with driver.session() as session: session.write_transaction(create_person_node, name) def create_person_node(tx, name): return tx.run("CREATE (a:Person {name: $name})", name=name)
В этом участке кода задание Cypher инкапсулирован в create_person_node. А вызов session.write_transaction сообщает драйверу, что это транзакция, содержащая операции записи. При использовании session.read_transaction для чтения драйвер может распределить работу между узлами кластера.
Этот же участок кода демонстрирует другую лучшую практику применения параметров с именем $name для передачи переменной в запрос Cypher. Рекомендуется использовать эти параметры везде, где данные подставляются в запросы без всякой конкатенации строк. Использование параметров запроса дает следующие преимущества:
- повышение безопасности, включая защиту от атак с внедрением (Cypher Injection Attacks);
- уменьшение форм запросов, позволяя базе данных выполнять их быстрее, вместо компиляции нового запроса в виде ранее неизвестной строки.
Больше реальных примеров применения Neo4j и других инструментов графовой аналитики больших данных для практических бизнес-задач вы узнаете на специализированных курсах в нашем лицензированном учебном центре обучения и повышения квалификации для разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков больших данных в Москве: