SET и REMOVE: меняем свойства и метки
Как изменять уже существующие узлы и связи: свойства, метки, точечно и пачкой.
SET присваивает или меняет свойства и добавляет метки; REMOVE убирает свойство или метку. Менять стоит то, что сперва найдено через MATCH.
Меняем свойство
Сначала находим узел, потом присваиваем:
MATCH (p:Person {name:'Алиса'})
SET p.born = 1991, p.city = 'Москва'
RETURN pЕсли свойства не было — оно добавится; если было — перезапишется. SET одинаково создаёт и обновляет. Это важная черта: в графе у узла нет фиксированной «схемы колонок», поэтому добавить новое свойство одному узлу — нормальная и дешёвая операция, не требующая ALTER TABLE, как в реляционной базе. Два узла :Person вполне могут иметь разный набор свойств.
Порядок «сначала найти, потом менять» — не формальность, а суть модели. SET работает с тем, что приехало из MATCH: на каждую найденную строку он применяет присваивание. Нет совпадений в MATCH — нечего и менять, SET просто ничего не сделает (и не выдаст ошибки). Поэтому если апдейт «не сработал», первым делом проверяют, что MATCH вообще что-то находит.
Вычисляемые присваивания
Справа от SET может стоять любое выражение, в том числе использующее текущее значение свойства. «Поднять рейтинг всех фильмов студии на 1»:
MATCH (m:Movie)
WHERE m.studio = 'A24'
SET m.rating = coalesce(m.rating, 0) + 1
RETURN m.title, m.ratingФункция coalesce подставляет 0 там, где рейтинга ещё не было, иначе null + 1 дал бы null и испортил бы данные. Такой приём — счётчики, инкременты, накопление — частая причина выбрать SET с выражением вместо константы.
Слияние свойств через +=
Чтобы накатить пачку свойств из объекта, не трогая остальные, есть +=:
MATCH (p:Person {name:'Боб'})
SET p += {city:'Казань', verified:true}
RETURN pА вот простое = с объектом заменяет все свойства целиком: SET p = {name:'Боб'} сотрёт всё, кроме name. Разница между = и += здесь критична.
Добавляем и снимаем метки
Метку добавляют тем же SET, через двоеточие; снимают через REMOVE:
MATCH (p:Person {name:'Вера'})
SET p:Director // теперь :Person:Director
REMOVE p:Director // снова только :PersonТак удобно помечать состояние: добавить :Premium при оплате, снять при истечении. Метки в Neo4j — это лёгкие теги, и узел может нести их сразу несколько (:Person:Director:Premium). Помечать ими статус выгодно ещё и потому, что по метке есть быстрый поиск: «все премиум-пользователи» — это MATCH (p:Premium), дешёвый label scan, а не фильтр по булеву свойству.
Динамическое навешивание/снятие меток — типичный приём моделирования жизненного цикла сущности: :Lead → :Customer → :Churned. Вместо колонки status со строковыми значениями вы оперируете метками, и запросы по состоянию становятся короче и быстрее.
Удаляем свойство
REMOVE убирает свойство (это не то же самое, что выставить null, хотя эффект похож):
MATCH (p:Person {name:'Боб'})
REMOVE p.verified
RETURN pТонкость, которую полезно знать: в Neo4j установить свойству значение null и удалить свойство — это одно и то же действие. SET p.verified = null физически убирает ключ verified у узла, ровно как REMOVE p.verified. В графе попросту не хранится «свойство со значением null»: либо свойство есть, либо его нет. Поэтому проверка WHERE p.verified IS NULL истинна и для тех, у кого свойства никогда не было, и для тех, кому его обнулили.
Массовые апдейты
SET применяется ко всем найденным узлам сразу. «Проставить всем фильмам без года значение по умолчанию»:
MATCH (m:Movie)
WHERE m.released IS NULL
SET m.released = 0Один MATCH нашёл все подходящие фильмы, один SET поправил их все — никакого цикла писать не нужно, Cypher по своей природе работает над множеством строк. Это и сила, и ловушка: широкий MATCH поправит больше, чем вы ожидали. Перед массовым SET полезно сначала прогнать тот же MATCH ... RETURN count(*) и убедиться, что под фильтр попадает ровно столько узлов, сколько вы держите в голове.
Как работает под капотом
SET и REMOVE — операции записи внутри транзакции. Neo4j берёт блокировку на изменяемые узлы/связи, применяет изменения и пишет их в журнал (write-ahead log) для устойчивости. Если запрос затрагивает миллионы узлов, всё это копится в одной транзакции и грузит память — поэтому большие апдейты разбивают на батчи (через CALL { ... } IN TRANSACTIONS или подобное), а не делают одной командой.
Почему именно «копится»? Транзакция в Neo4j атомарна: либо все изменения применяются вместе, либо при ошибке откатываются целиком. Чтобы гарантировать откат, движок держит в памяти состояние всех затронутых записей до самого commit. Миллион изменённых узлов — это миллион записей в памяти транзакции плюс журнал. Батчинг через CALL { ... } IN TRANSACTIONS OF 10000 ROWS разрезает работу на множество мелких транзакций, каждая из которых фиксируется и освобождает память:
MATCH (m:Movie)
WHERE m.released IS NULL
CALL { WITH m SET m.released = 0 } IN TRANSACTIONS OF 10000 ROWSПлатой за это становится потеря «всё-или-ничего»: если упадёт пятый батч, первые четыре уже зафиксированы. Для разовой миграции данных это обычно приемлемый компромисс, но помнить о нём нужно.
Частые ошибки
- Путать
=и+=с объектом.SET p = {...}заменяет ВСЕ свойства;SET p += {...}сливает. Это самая болезненная ошибка. - SET без MATCH. Менять можно только найденное; забудете MATCH — менять нечего.
- Гигантский апдейт одной транзакцией. Миллионы изменений сразу могут упасть по памяти — батчируйте.
Итоги
SETприсваивает свойства и добавляет метки; работает как «создать или обновить».+=сливает свойства, а=с объектом — заменяет все; не перепутайте.REMOVEубирает свойство или метку.- Массовые апдейты батчируйте — одна гигантская транзакция бьёт по памяти.