Работа из приложения: драйверы Python и JS

Как ваше приложение разговаривает с Neo4j: официальные драйверы, сессии и параметры запросов.

Драйвер Neo4j — официальная библиотека (Python, JavaScript, Java, Go, .NET), подключающаяся к серверу по протоколу Bolt и исполняющая Cypher из кода.

До сих пор мы писали Cypher в интерактивной консоли. Но в реальном продукте запросы исходят не от человека за консолью, а от кода: веб-сервер обрабатывает запрос пользователя, лезет в граф за рекомендациями и возвращает JSON. Мост между вашим приложением и Neo4j — это официальный драйвер. Он берёт на себя сетевое соединение, пул подключений, передачу параметров и разбор ответов, оставляя вам только текст Cypher и значения. Драйверы для всех языков устроены по одной логике, поэтому, поняв один, вы понимаете все. И есть пара правил — про параметры и про жизненный цикл драйвера, — которые отделяют рабочий код от дырявого и медленного.

Подключение по Bolt

Все драйверы устроены похоже: создаём драйвер с адресом bolt:// и учёткой, открываем сессию, выполняем Cypher. Пример на Python (библиотека neo4j):

from neo4j import GraphDatabase

driver = GraphDatabase.driver(
    "bolt://localhost:7687",
    auth=("neo4j", "secretpass"),
)

with driver.session() as session:
    result = session.run(
        "MATCH (p:Person {name:$name})-[:ЗНАЕТ]->(f) RETURN f.name AS friend",
        name="Алиса",
    )
    for record in result:
        print(record["friend"])

driver.close()

Этот код помечен как language-text намеренно: ему нужна библиотека и живой сервер — в браузере он не исполнится. Обратите внимание на $name — это параметр.

Разберём скелет по шагам, потому что он одинаков во всех языках. GraphDatabase.driver(url, auth) создаёт объект драйвера: это «тяжёлый» долгоживущий объект, который сразу поднимает пул сетевых соединений к серверу. Префикс bolt:// в адресе — это не HTTP, а специальный бинарный протокол Neo4j (о нём ниже). Дальше driver.session() открывает лёгкую сессию — рабочий контекст под конкретную задачу; её мы оборачиваем в with, чтобы она гарантированно закрылась. Внутри session.run(cypher, params) отправляет запрос и возвращает поток записей, по которому мы итерируемся. Наконец driver.close() аккуратно гасит пул соединений при завершении приложения. Запомните пропорцию: драйвер — один на всё приложение, сессии — много и недолго.

Параметры — не конкатенация

Главное правило безопасности: никогда не склеивайте запрос строками. Передавайте значения как параметры ($name). Это защищает от инъекций и позволяет серверу кэшировать план запроса (одна и та же форма Cypher с разными значениями переиспользует скомпилированный план).

Чтобы прочувствовать опасность, представьте соблазн написать запрос конкатенацией: взять имя из формы и вклеить прямо в строку Cypher. Если пользователь введёт в поле что-то хитрое с кавычками и завершающими конструкциями, он сможет «дописать» ваш запрос — это та же инъекция, что и SQL-инъекция, только в графе. Параметр $name закрывает дыру в корне: значение едет на сервер отдельным каналом, рядом с текстом запроса, а не внутри него. Сервер никогда не пытается «исполнить» содержимое параметра как Cypher — для него это просто данные. Поэтому экранировать кавычки вручную не нужно: параметр не может сломать синтаксис в принципе.

Второй, менее очевидный выигрыш — производительность. Сервер кэширует план исполнения по тексту запроса. Запрос MATCH ... {name:$name} имеет один и тот же текст для всех имён, поэтому план компилируется один раз и переиспользуется. А вот склейка строк рождает уникальный текст на каждое имя — кэш промахивается, и сервер тратит время на перекомпиляцию плана при каждом вызове. Так что параметры — это и про безопасность, и про скорость одновременно.

// JavaScript (пакет neo4j-driver)
const neo4j = require('neo4j-driver');
const driver = neo4j.driver('bolt://localhost:7687',
  neo4j.auth.basic('neo4j', 'secretpass'));

const session = driver.session();
const res = await session.run(
  'MATCH (m:Movie) WHERE m.released > $year RETURN m.title AS t',
  { year: 2000 }
);
res.records.forEach(r => console.log(r.get('t')));
await session.close();
await driver.close();

Сессии и транзакционные функции

Сессия — это логический контекст работы. Для надёжности драйверы предлагают транзакционные функции (execute_read / execute_write), которые сами повторяют запрос при временных сбоях (например, при переключении лидера в кластере). В продакшене пишут именно через них, а не через сырой session.run.

def get_friends(tx, name):
    result = tx.run(
        "MATCH (p:Person {name:$name})-[:ЗНАЕТ]->(f) RETURN f.name AS friend",
        name=name,
    )
    return [r["friend"] for r in result]

with driver.session() as session:
    friends = session.execute_read(get_friends, "Алиса")

Здесь логика запроса вынесена в функцию, а execute_read её исполняет. Зачем эта обёртка? Она встроенно повторяет вызов при временных сбоях — например, в кластере лидер мог переключиться ровно в момент запроса. Сырой session.run в такой ситуации просто упал бы с ошибкой, а транзакционная функция тихо повторит попытку и вернёт результат. Поэтому важное требование: функцию-тело пишут идемпотентной и без побочных эффектов вне транзакции (никаких отправок писем внутри), ведь её могут вызвать дважды. Для чтения берут execute_read, для записи — execute_write; в кластере это ещё и подсказка драйверу, куда маршрутизировать запрос.

Как работает под капотом

Драйвер открывает Bolt-соединение (бинарный протокол поверх TCP/WebSocket), держит пул соединений и мультиплексирует по ним сессии. Cypher отправляется на сервер, тот компилирует/берёт из кэша план, исполняет и стримит записи обратно. Параметры передаются отдельно от текста запроса — поэтому они не могут «сломать» синтаксис и не требуют экранирования. В кластере драйвер ещё и маршрутизирует чтение на реплики, а запись — на лидера (об этом — урок о масштабировании).

Частые ошибки

  • Склеивать значения в строку запроса. Это дыра для инъекций и убийца кэша планов; используйте $параметры.
  • Не закрывать драйвер/сессию. Утечки соединений; используйте контекст-менеджеры (with) или finally.
  • Создавать драйвер на каждый запрос. Драйвер тяжёлый и держит пул — он создаётся один раз на приложение, сессии — дёшевы.
  • Игнорировать транзакционные функции. В кластере без ретраев запросы будут падать на переключениях.

Итоги

  • Драйверы (Python/JS/…) подключаются по bolt:// и исполняют Cypher из кода.
  • Значения передавайте параметрами ($name) — безопасность и кэш планов.
  • Драйвер создаётся один раз (пул соединений), сессии — лёгкие и краткоживущие.
  • В продакшене используйте транзакционные функции с автоповтором (execute_read/write).
Проверьте себя
1. Почему значения в Cypher передают параметрами ($name), а не склеивают в строку?
AТак короче
BЗащита от инъекций и переиспользование кэшированного плана запроса
CПараметры обязательны синтаксически
DИначе Bolt не работает
2. Сколько раз за жизнь приложения обычно создают объект драйвера Neo4j?
AНа каждый запрос
BОдин раз на приложение — он держит пул соединений; сессии дёшевы
CНа каждую сессию
DНикогда, он создаётся сам
3. Зачем нужны транзакционные функции (execute_read/execute_write)?
AОни быстрее на 50%
BОни автоматически повторяют запрос при временных сбоях, например переключении лидера в кластере
CОни шифруют запрос
DБез них нельзя читать