知识库

理解 MERGE 的工作原理

MERGE 是什么,它是如何工作的?

MERGE 子句确保图中存在某个模式。该模式要么已完整存在,要么需要完整创建。

通过这种方式,可以将 MERGE 理解为尝试对模式进行 MATCH,如果未找到匹配项,则对该模式进行 CREATE 操作。

当指定的模式不存在且需要创建时,先前绑定到现有图元素的所有变量将在模式中重用。模式的所有其他元素都将被创建。

了解哪些模式元素将使用现有图元素,哪些将代替现有图元素被创建,这一点很重要。

对于以下示例,我们将使用一个非常简单的图,包含 :Student、:Class、:ReportCard 和 :Term 节点,如下所示。

(:Student)-[:ENROLLED_IN]->(:Class)
(:Student)-[:EARNED]->(:ReportCard)
(:Class)-[:FOR_TERM]->(:Term)

本文中将引用“绑定变量”,这指的是从早期子句(通常是 MATCH 或 MERGE)中绑定到图中现有元素并在 MERGE 模式中重用的变量。这意味着在 MERGE 模式中,它指的是先前找到并当前存在于图中的元素。

相反,模式中的“新变量”指的是首次在模式中引入的变量。因此,它尚未引用图中当前存在的任何元素,但作为 MERGE(匹配现有元素或创建新元素)的结果,它将绑定到图元素。

模式中任何未与绑定变量关联的元素(包括使用新变量的元素和根本没有变量的元素),如果 MERGE 必须创建整个模式,都将在图中生成新创建的元素。

没有绑定变量的 MERGE 可能会创建重复元素

最常见的 MERGE 错误是,当您想使用现有图元素时,却尝试 MERGE 一个没有绑定变量的模式。

例如,尝试将现有 student 注册到现有 class 中。

MERGE (student:Student{id:123})-[:ENROLLED_IN]->(class:Class{name:'Cypher101'})

在上述查询中,studentclass 是新变量,它们之前没有绑定到图中的任何节点,这是它们在查询中的首次使用。

如果整个模式已经存在(给定的学生已经注册到给定的班级中),这些变量将按预期绑定到图中现有节点。

然而,如果模式尚不存在,则模式的所有新元素都将被创建。在这种情况下,所有元素都将被创建;一个新的 :Student 节点将以给定的 ID 创建,一个新的 :Class 节点将以给定的名称创建,并且这些全新的节点之间将创建一个新的 :ENROLLED_IN 关系。

如果此类学生或此类班级已经存在,这可能会导致创建重复节点。如果学生或班级节点针对给定属性存在唯一约束,则会抛出错误。否则,将创建重复节点,这可能不会被注意到,特别是对于新手用户而言。

带有绑定变量的 MERGE 重用现有图元素

要在图中使用现有节点和关系,请首先对节点或关系进行 MATCH 或 MERGE,然后使用绑定变量在模式中进行 MERGE。

上述注册查询的正确版本将首先对 studentclass 进行 MATCH,然后 MERGE 关系。

MATCH (student:Student{id:123})
MATCH (class:Class{name:'Cypher101'})
MERGE (student)-[:ENROLLED_IN]->(class)

类似地,您可以在 MERGE 学生和班级之后再对它们之间的关系进行 MERGE。

MERGE (student:Student{id:123})
MERGE (class:Class{name:'Cypher101'})
MERGE (student)-[:ENROLLED_IN]->(class)

这确保了学生和班级节点存在(如果它们尚不存在,则会创建它们),然后它们之间的关系被合并。

根据不同用例,使用绑定元素和新元素的组合进行 MERGE

虽然上述方法适用于该特定用例,但并非适用于所有用例的正确方法。我们可能需要在 MERGE 中使用绑定元素和新元素的组合以获得正确的行为。

考虑一个为学生创建成绩单的查询。

如果我们重用上述方法,查询可能如下所示。

MATCH (student:Student{id:123})
MERGE (reportCard:ReportCard{term:'Spring2017'})
MERGE (student)-[:EARNED]->(reportCard)

此查询中的问题是,所有学生都重用了相同的 :ReportCard 节点。如果查询还需要向 :ReportCard 添加成绩,则后续的每个条目都会覆盖之前添加的内容。如果未发现,这种方法最终会导致所有学生拥有完全相同的成绩单节点,从而拥有完全相同的成绩,即由最后处理的学生输入的成绩。

我们真正需要的是每个学生一个单独的 :ReportCard。我们可以通过绑定 :Student 节点而不是 :ReportCard 节点来实现这一点。

MATCH (student:Student{id:123})
MERGE (student)-[:EARNED]->(reportCard:ReportCard{term:'Spring2017'})

由于 student 变量绑定到某个节点,如果需要创建模式,则将使用该节点,并且将仅为该 student 创建 :ReportCard,而不是在所有学生之间共享。

请注意,如果我们完全省略 reportCard 变量,我们会得到完全相同的行为,因为当需要创建模式时,没有变量的元素和新引入变量的元素的处理方式完全相同。

MATCH (student:Student{id:123})
MERGE (student)-[:EARNED]->(:ReportCard{term:'Spring2017'})

记住也会创建新关系

上述示例对于节点来说应该很容易理解,但请记住它们也适用于关系。如果整个模式不存在,则模式中的新关系将被创建。当使用更大的模式时,最容易看到这一点。

考虑如果我们需要将一个 :Student 注册到一个特定 :Term 的 :Class 中。我们假设学生、学期和班级已经存在于图中。

MATCH (student:Student{id:123})
MATCH (spring:Term{name:'Spring2017'})
MATCH (class:Class{name:'Cypher101'})
MERGE (student)-[:ENROLLED_IN]->(class)-[:FOR_TERM]->(spring)

这看起来可能是正确的,并且当这些关系都不提前存在时,行为也可能正确,但如果这里存在任何不平衡,即只有模式的一半存在,那么最终将创建重复元素。如果查询是为多个学生运行而不是仅为一个学生运行,这将最为明显。

在第一次运行中,对于第一个学生,关系将按预期创建。

在下一次运行中,对于下一个学生,班级和学期之间存在一个 :FOR_TERM 关系,但学生尚未注册到该班级。由于整个模式不存在,整个模式将被创建(不包括绑定节点),因此学生将被注册到该班级,并且在班级和学期之间将创建一个新的(重复的):FOR_TERM 关系。

如果查询运行 30 次,为同一班级和学期注册 30 名学生,那么当我们完成时,班级和学期之间将有 30 个 :FOR_TERM 关系。

为了解决这个问题,如果已知每个 :Class 和其 :Term 之间已经存在 :FOR_TERM 关系,那么将模式的该部分移动到 MATCH 中。

MATCH (student:Student{id:123})
MATCH (class:Class{name:'Cypher101'})-[:FOR_TERM]->(spring:Term{name:'Spring2017'})
MERGE (student)-[:ENROLLED_IN]->(class)

如果我们不知道 :FOR_TERM 关系是否已经存在,那么我们就必须进一步分解,先对学生、班级和学期进行 MATCH,然后对 :FOR_TERM 进行 MERGE,最后对 :ENROLLED_IN 进行 MERGE。

这里的关键是,通常应避免 MERGE 较长的模式。如果模式的部分存在但整个模式不存在,您很可能会看到重复项,因此请考虑将较大模式的 MERGE 分解为对较小模式的单独 MERGE。

在 MERGE 之后使用 ON MERGE 和 ON CREATE 根据 MERGE 行为设置属性

通常在 MERGE 之后,我们需要在模式的元素上设置属性,但我们可能希望根据 MERGE 是匹配到现有模式还是必须创建它来有条件地设置这些属性。例如,如果我们在创建时有要设置的默认属性值。

ON MERGE 和 ON CREATE 子句提供了我们所需的控制。这也使得可以重新运行查询并确保我们不会用默认值覆盖现有属性。

以下是设置学生成绩单和成绩的示例,假设 {grades} 是一个映射参数,包含我们要在创建新的 :ReportCard 节点时设置的成绩。

MATCH (student:Student{id:123})
MERGE (student)-[:EARNED]->(reportCard:ReportCard{term:'Spring2017'})
ON CREATE SET reportCard += {grades}

当 MERGE 导致模式创建时,它会获取模式中节点和关系的锁

当 MERGE 未能找到现有模式时,它将在创建模式中缺失的元素之前,获取模式中所有绑定节点和关系的锁。

这是为了确保在 MERGE 执行期间,MERGE 或 CREATE 不会同时创建模式(或更改现有模式的属性使其与所需模式相同),从而导致模式重复。

在锁定绑定元素之后,MERGE 会对模式执行另一次 MATCH,以避免在 MERGE 确定模式不存在和获取锁之间的时间间隔内模式可能被创建的竞态条件。

对单个节点模式进行 MERGE 可能会创建重复项,除非存在唯一约束

当对尚不存在的单个节点模式执行 MERGE(且没有唯一约束)时,没有可锁定的对象来避免竞态条件,因此并发事务可能会导致重复节点。

例如

MERGE (student:Student{id:123})

如果没有唯一约束,则此查询(或任何可能涉及具有此 ID 的 :Student 节点的 MERGE 或 CREATE 的其他查询)的并发执行可能会导致同一学生存在多个节点。

通过在 :Student(id) 上添加唯一约束,模式锁将确保学生节点的唯一性,并且不会出现重复项。

不使用绑定节点对较大模式进行 MERGE 也可能创建重复项

在上述单个节点(当节点尚不存在时)的情况下,我们没有任何可用于锁定的绑定节点。类似地,如果 MERGE 是针对大于单个节点的模式,其中整个模式不存在,并且模式中未使用任何绑定节点,则没有可锁定的对象来避免竞态条件,因此它面临与并发写入相同的重复风险。

MERGE (class:Class{name:'Cypher101'})-[:FOR_TERM]->(spring:Term{name:'Spring2017'})

Neo4j < 3.0.9(针对 3.0.x 版本)和 < 3.1.2(针对 3.1.x 版本)的注意事项

受影响版本中成本规划器的一个错误阻止了 MERGE 上的双重检查锁定。这导致了竞态条件,可能导致并发 MERGE 操作或其他写入操作(导致之前不存在的模式变为存在)创建重复模式。

我们建议升级到包含此错误修复的版本。

© . All rights reserved.