3.1 CREATE 与 MERGE:让图谱安全生长 本节摘要:写入侧的第一道分水岭是 CREATE 与 MERGE 的语义差异:前者无条件新建,后者"确保存在"。本节给出两者各自的使用场景,重点拆解 MERGE 的锚定规则——锚错属性是重复节点的头号来源——并示范 ON CREATE/ON MATCH 分支与约束兜底的组合拳。读完后你的写入语句将具备幂等性。 第 2 章的图只读不长。本节开始让图生长,先立一条纪律:生产环境的写入默认用 MERGE,CREATE 只用于确定"这一定是新实体"的场合。理由在本节的案例里会变得很具体。
本节摘要:写入侧的第一道分水岭是 CREATE 与 MERGE 的语义差异:前者无条件新建,后者"确保存在"。本节给出两者各自的使用场景,重点拆解 MERGE 的锚定规则——锚错属性是重复节点的头号来源——并示范 ON CREATE/ON MATCH 分支与约束兜底的组合拳。读完后你的写入语句将具备幂等性。
第 2 章的图只读不长。本节开始让图生长,先立一条纪律:生产环境的写入默认用 MERGE,CREATE 只用于确定"这一定是新实体"的场合。理由在本节的案例里会变得很具体。
// 每执行一次,图里就多一个节点——哪怕内容一模一样 CREATE (p:Person {name: 'Alice', age: 30}) RETURN p
执行两次后: MATCH (p:Person {name: 'Alice'}) RETURN count(p) → 2 -- 两个独立的 Alice,没有唯一 id,人工无从区分
CREATE 同时也可以建关系,但要求两个端点已经匹配到:
// 两段式:先锚定两端,再连边 MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) CREATE (a)-[:KNOWS {since: 2023}]->(b)
MERGE 的语义是"这个模式要么已存在,要么现在创建":
// 幂等:跑一百遍,图里也只有一个 Alice MERGE (p:Person {name: 'Alice'}) ON CREATE SET p.created = datetime() ON MATCH SET p.lastSeen = datetime() RETURN p
第一次执行走 ON CREATE 分支打上创建时间;之后再跑走 ON MATCH 分支刷新 lastSeen。这两个分支让"新建"与"重逢"可以走完全不同的业务逻辑,是同步类任务的主干模式。
MERGE 括号里写的全部内容构成查找键。写多写少一个属性,语义天差地别:
// 锚定过窄:每次带着不同 age 来,都会新建节点 MERGE (p:Person {name: 'Alice', age: 30}) -- 来了个 age: 31 的 Alice? -- 查不到 → 又建一个
// 锚定过宽:只锚标签 MERGE (p:Person) -- 全库只有一个裸 Person 节点
正确姿势是用业务唯一键锚定节点,其余属性用 SET 补写:
MERGE (p:Person {email: 'alice@example.com'}) // 邮箱是业务唯一键 ON CREATE SET p.name = 'Alice', p.age = 30 SET p.updated = datetime()
关系上的 MERGE 同理:两端点 + 关系类型(+ 有业务意义的关系属性)共同构成锚:
// 锚定"这条边"本身:同一对人的 KNOWS 边只建一次 MATCH (a:Person {email: 'alice@example.com'}), (b:Person {email: 'bob@example.com'}) MERGE (a)-[k:KNOWS]->(b) ON CREATE SET k.since = 2024

MERGE 的正确性依赖"锚定属性确实唯一",这件事不该靠自觉,交给唯一性约束:
// 唯一性约束:建约束会自动附带同名索引 CREATE CONSTRAINT person_email_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.email IS UNIQUE
约束生效后: CREATE (p:Person {email: 'alice@example.com'}) -- 第二次插入 → ConstraintValidationFailed: 节点已存在,写入被拒绝
约束的完整清单(存在性、范围、类型等)随版本扩张,社区版唯一性约束最常用。铁律:先建约束,再上 MERGE——顺序反了,重复数据已经入库,还得先清洗。
UNWIND 把列表摊成行,配合 MERGE 做批量幂等同步——这是第 4 章驱动编程的预告:
// 参数 $people 是 [{"email": "...", "name": "..."}, ...] UNWIND $people AS row MERGE (p:Person {email: row.email}) ON CREATE SET p.name = row.name, p.created = datetime() SET p.updated = datetime()
100 行输入 → 100 次幂等写入,1 个事务 重复执行 → 0 个新节点,100 次 lastSeen 刷新
💡 幂等是同步任务的生命线:消息队列的重投递、调度器的重跑、人工的误触发,全都打不乱一个幂等的 MERGE。
写入代码合并前的四个必查项,对应四类线上事故:
1. 锚定属性是否业务唯一键? —— 防重复节点 2. 是否先有约束再上线? —— 防脏数据入库 3. 批量写入是否 UNWIND 分批? —— 防事务超长 4. ON CREATE / ON MATCH 分支的逻辑是否都验证过? —— 防首次与再次执行行为不一致
第四条最常被漏测:本地库是空的,永远只走 ON CREATE;生产库里有存量数据,ON MATCH 分支一跑就暴露问题。用"跑两遍"作为写入语句的最小测试用例——第二遍必须零新增。
写入要"对",还要"稳"。下一节看事务与 ACID 如何守住后者。