CREATE: создаём узлы и связи

Наполняем граф с нуля: узлы, их свойства и связи между ними одной командой CREATE.

CREATE — команда Cypher, которая всегда создаёт новые узлы и/или связи по заданному паттерну, даже если похожие уже есть.

В прошлом уроке мы научились читать паттерны. Хорошая новость: для записи используется тот же самый синтаксис скобок и стрелок. Разница лишь в глаголе перед паттерном. Если поставить MATCH — паттерн будет искаться в существующих данных. Если поставить CREATE — тот же паттерн будет материализован, то есть превращён в реальные узлы и связи на диске. Это очень удобно: один раз освоив язык рисунков, вы умеете и читать, и писать граф.

Создаём узлы

Самый простой CREATE добавляет узел с меткой и свойствами:

CREATE (p:Person {name:'Алиса', born:1990})
RETURN p

За один запрос можно создать несколько узлов через запятую:

CREATE
  (alice:Person {name:'Алиса'}),
  (bob:Person   {name:'Боб'}),
  (matrix:Movie {title:'Матрица', released:1999})

Несколько слов про свойства. Их типы знакомы по любому языку: строки ('Алиса'), числа (1999), булевы (true/false), а также списки простых значений, например genres:['sci-fi','action']. Чего у свойства нет — так это вложенных объектов: значением свойства не может быть другой узел. Если хочется «вложенности», в графе это выражается не свойством, а отдельным узлом и связью к нему. Это важная смена мышления для тех, кто пришёл из мира JSON и документных баз: связь — полноценный гражданин, а не поле внутри документа.

Создаём связи

Связь создаётся тем же паттерном со стрелкой. Чтобы соединить два узла, их обычно сначала находят через MATCH, а потом создают ребро:

MATCH (a:Person {name:'Алиса'}), (b:Person {name:'Боб'})
CREATE (a)-[:ЗНАЕТ {since:2020}]->(b)

Свойство {since:2020} повисло на связи — это атрибут именно знакомства. Обратите на это особое внимание: свойства живут не только на узлах, но и на рёбрах. Год знакомства, дата подписки, гонорар актёра за роль, вес ребра в графе дорог — всё это естественно ложится на связь, а не на один из узлов. В реляционной модели для таких атрибутов пришлось бы заводить отдельную таблицу-связку со своими колонками; в графе они просто крепятся к ребру.

Можно создать узлы и связь сразу в одном CREATE, если узлов ещё нет:

CREATE (a:Person {name:'Вера'})-[:РАБОТАЕТ_В]->(c:Company {name:'CodeChick'})

Здесь одной командой родились два узла и связь между ними. Удобно для первичного наполнения базы, когда вы точно знаете, что таких сущностей ещё нет. А вот для дозаписи в уже населённую базу этот приём опасен — почему, разберём прямо сейчас.

Опасность дублей

Главное, что нужно усвоить про CREATE: он не проверяет существование. Если выполнить такой запрос дважды:

CREATE (p:Person {name:'Алиса'})

в базе окажутся две Алисы — два разных узла с одинаковыми свойствами. Для данных, которые должны быть уникальны (пользователь, товар), это ловушка. Решение — MERGE из следующего урока и ограничения уникальности из раздела про индексы.

Почему так задумано, а не «исправлено»? Потому что у CREATE есть законные применения, где дубли — это норма. Каждое событие, лог, транзакция, лайк — отдельная сущность, даже если у двух лайков совпали все свойства. CREATE честно говорит: «я создаю новый факт». Ответственность за уникальность там, где она нужна, лежит на вас: либо вы используете MERGE (поиск-или-создание), либо ставите ограничение уникальности на ключевое свойство, и тогда повторный CREATE упадёт с ошибкой вместо тихого дубля. Безопасный по умолчанию вариант для справочных сущностей — именно MERGE.

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

CREATE — это чистая запись: движок выделяет новые записи узлов/связей в store-файлах, проставляет свойства и связывает рёбра в списки смежности. Никаких проверок и поисков существующего он не делает, поэтому CREATE — самая быстрая операция вставки. Вся работа происходит в транзакции: либо весь CREATE применился, либо ничего (об ACID — отдельный урок).

Чуть подробнее про «связывает рёбра в списки смежности». Когда создаётся связь между Алисой и Бобом, движок не просто кладёт где-то запись «ребро #42». Он дописывает ссылку на это ребро в список связей обоих узлов — и Алисы, и Боба. Благодаря этому позже, при чтении, чтобы получить соседей узла, не нужно сканировать всю базу: достаточно пройти по короткому списку, физически прикреплённому к узлу. Цена этого — запись связи чуть дороже, чем «просто строчка в таблице»: обновляются структуры у двух узлов. Но это разовая плата при вставке, окупающаяся быстрыми обходами при каждом чтении.

Полезно помнить и про порядок частей в одном CREATE: переменные, объявленные раньше в команде, видны дальше. Поэтому CREATE (a:Person {name:'Вера'})-[:РАБОТАЕТ_В]->(c:Company) работает — к моменту создания связи a уже определена. А вот сослаться в CREATE на узел, которого нет ни в базе, ни выше в этой же команде, нельзя: его сперва надо либо найти через MATCH, либо создать.

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

  • CREATE вместо MERGE для уникальных сущностей. Повторный запуск наплодит дубли. Для «создать, если нет» нужен MERGE.
  • Создавать связь, не найдя узлы. Если узлов ещё нет, MATCH ... CREATE ничего не свяжет (MATCH вернёт пусто). Тогда либо создавайте узлы в том же CREATE, либо используйте MERGE.
  • Забыть RETURN при отладке. Без RETURN запрос отработает, но вы не увидите, что именно создалось.

Итоги

  • CREATE всегда добавляет новые узлы/связи по паттерну, без проверки на существование.
  • Несколько узлов и связей создаются за один запрос через запятую.
  • Чтобы связать существующие узлы — сперва MATCH, затем CREATE ребра.
  • Для уникальных данных CREATE опасен дублями — используйте MERGE и ограничения.
Проверьте себя
1. Что произойдёт, если дважды выполнить CREATE (p:Person {name:'Алиса'})?
AПоявится одна Алиса
BПоявятся два разных узла-Алисы (дубли)
CБудет ошибка уникальности
DВторой запрос ничего не сделает
2. Как правильно создать связь между двумя уже существующими узлами?
ACREATE сразу обоих узлов заново
BСначала MATCH найти узлы, потом CREATE связь между ними
CСвязи нельзя добавлять после создания узлов
DТолько через DELETE
3. Почему CREATE — самая быстрая операция вставки?
AОн сжимает данные
BОн не делает проверок существования, а только пишет новые записи
CОн работает вне транзакции
DОн не пишет на диск