SQL 到 Cypher 的转换

简介

将 SQL 查询转换为 Cypher® 是该驱动程序的一项可选功能,它由两部分组成:

  • 转换器 SPI(服务提供者接口),位于模块 org.neo4j:neo4j-jdbc-translator-spi 中。它由两个接口组成:SqlTranslatorFactory 和实际的 SqlTranslator

  • 该 SPI 的具体实现,以 org.neo4j:neo4j-jdbc-translator-impl 的形式发布。

后者将在“使用默认转换器”一节中介绍,并包含在“完整包”中,详情请参阅 可用包。提供前者的原因有两个:它允许我们在分发驱动程序时选择是否包含默认转换器,并允许您运行自定义转换器。

转换器可以进行链式调用,类路径中可以有任意数量的转换器。它们的优先级是可以配置的,我们的默认实现优先级最低。因此,您可以拥有一个处理特定查询集的自定义转换器,如果它收到无法转换的查询,它会将其传递给我们的实现。

将任意 SQL 查询转换为 Cypher 是一项主观性很强的任务,因为没有一种“正确”的方法将表名映射到图对象:表名可以直接用作标签,也可以转换为单数形式等。映射关系则更为棘手:关系类型应该派生自连接表、连接列(如果是这种情况,是哪一列?)还是外键?

我们认为我们的假设适用于各种用例,并且与其提供配置来迎合所有场景,我们提供了编写您自己的转换层的可能性。驱动程序将使用标准的 Java 服务加载机制(Service Loader)在模块路径或类路径上查找 SPI 的实现。

某些工具(如 Tableau)使用的类加载器不支持标准的 Java 服务加载机制。对于这些情况,我们提供了一个名为 translatorFactory 的附加配置属性。将其设置为 DEFAULT 可直接加载我们的默认实现,或将其设置为任何其他工厂的完全限定类名。请注意,无论是我们的默认实现还是您的自定义实现,都必须在类路径中。

将 SQL 转换为 Cypher

启用 SQL 到 Cypher 转换只有一个要求:您必须在类路径中有一个实现该 SPI 的模块。如果您使用完整包 (org.neo4j:neo4j-jdbc-full-bundle),则自动满足此条件。在这种情况下,您不需要添加任何其他依赖项。如果您使用单独的分发版或“轻量级”包 org.neo4j:neo4j-jdbc-bundle,则必须添加 artifact org.neo4j:neo4j-jdbc-translator-impl

该实现将被自动加载。如果您按需使用转换,它将被懒加载(即不会触及或加载额外的类到内存中)。如果您为所有语句配置了自动转换,则该实现将被预先加载。关于实现加载,没有其他配置选项。

按需转换(按情况)

可以通过 java.sql.Connection 类中的官方 JDBC API nativeSQL 按需使用转换器。通过以下导入:

import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Logger;

您只需将 SQL 语句传递给 nativeSQL,即可获得对应的 Cypher。

try (var connection = DriverManager.getConnection(url, username, password)) {
    var sql = connection.nativeSQL("SELECT * FROM Movie n");
    assert """
            MATCH (n:Movie)
            RETURN *""".equals(sql);
}

针对所有查询

如果您在打开 Neo4j 实例连接时使用 enableSQLTranslation=true(作为 URL 参数或配置属性),则所有语句都将从 SQL 转换为 Cypher。如果以这种方式配置驱动程序,转换器将被预先加载。

var url = "jdbc:neo4j://:7687?enableSQLTranslation=true";
try (var connection = DriverManager.getConnection(url, username, password);
        var stmnt = connection.createStatement();
        var result = stmnt.executeQuery("SELECT n.title FROM Movie n")) {
    while (result.next()) {
        LOGGER.info(result.getString("n.title"));
    }
}

有时您可能需要对某些语句回退到 Cypher,要么是为了使用 SQL 无法表达的结构,要么是因为我们的默认转换器无法处理您的查询。我们提供了一条特殊的注释,您可以将其用作语句中的提示,以停止自动转换:/*+ NEO4J FORCE_CYPHER */

var url = "jdbc:neo4j://:7687?enableSQLTranslation=true";
var query = """
        /*+ NEO4J FORCE_CYPHER */
        MATCH (:Station { name: 'Denmark Hill' })<-[:CALLS_AT]-(d:Stop)
            ((:Stop)-[:NEXT]->(:Stop)){1,3}
            (a:Stop)-[:CALLS_AT]->(:Station { name: 'Clapham Junction' })
        RETURN localtime(d.departs) AS departureTime,
            localtime(a.arrives) AS arrivalTime
        """;
try (var connection = DriverManager.getConnection(url, username, password);
        var stmnt = connection.createStatement();
        var result = stmnt.executeQuery(query)) {
    while (result.next()) {
        LOGGER.info(result.getTime("departureTime").toString());
    }
}

可能的错误场景

当没有可用的 SQL 转 Cypher 转换器实现,且您使用了 java.sql.Connection.nativeSQL 或启用了自动转换时,会抛出消息为 No SQL translators availableNoSuchElementException。该异常将在访问该方法时抛出,或者在后者情况下,在打开连接时预先抛出。

使用默认转换器

支持的 SQL 方言

我们的默认转换器使用来自 jOOQ 的 OSS 解析器,它已经支持广泛的 SQL 方言。我们选择了 jOOQ 的通用默认方言作为我们的默认方言,但您可以在 SQL 到 Cypher 配置中使用参数 s2c.sqlDialect 将其覆盖,参数值请参考下文中的 配置列表。对于多种集成,POSTGRES 可能是一个不错的选择。

请记住,转换中的任何缺陷可能不是由于解析器不足,而是由于缺乏明显、语义上等价的 Cypher 结构。这意味着我们可能能够解析特定的 SQL 片段,但在没有额外上下文信息的情况下,无法将其翻译成 Neo4j 能理解的有意义的内容。

配置

默认实现提供了许多配置设置。它们必须在 URL 或配置选项中以 s2c 为前缀:

名称 含义 默认

parseNameCase

是否按原样解析表名。

true

tableToLabelMappings

表名到标签的映射表。

空映射

joinColumnsToTypeMappings

列名到关系类型的映射表。

空映射

prettyPrint

是否格式化生成的 Cypher。

true

alwaysEscapeNames

是否始终转义名称。

除非在启用 pretty printing 时明确配置为 false,否则默认为 true

sqlDialect

解析时使用的方言。支持的值为 POSTGRES, SQLITE, MYSQL, H2, HSQLDB, DERBYDEFAULT

DEFAULT

relationshipPattern

用于自动推断表名关系模式。1

匹配 ALabel_RELATIONSHIP_TYPE_AnotherLabel 的模式(即关系类型必须大写,且可以包含下划线)

1 该模式必须是具有三个捕获组的有效正则表达式:第一个组将被解释为起点或左侧节点,第二个组被解释为关系类型,第三个组为终点或右侧节点。也支持命名组 startreltypeendnull 或空白模式将禁用自动推断。

接下来的几个示例使用 properties 配置,以避免文档中出现过长的 URL,但所有属性也可以通过 URL 指定。

清单 1. 禁用 pretty printing;仅在必要时转义;配置专用表映射
var properties = new Properties();
properties.put("username", "neo4j");
properties.put("password", "verysecret");
properties.put("enableSQLTranslation", "true");
properties.put("s2c.prettyPrint", "false");
properties.put("s2c.alwaysEscapeNames", "false");
properties.put("s2c.tableToLabelMappings", "people:Person;movies:Movie;movie_actors:ACTED_IN");

var url = "jdbc:neo4j://:7687";
var query = """
        SELECT p.name, m.title
        FROM people p
        JOIN movie_actors r ON r.person_id = p.id
        JOIN movies m ON m.id = r.person_id""";
try (var connection = DriverManager.getConnection(url, properties)) {
    var sql = connection.nativeSQL(query);
    assert "MATCH (p:Person)-[r:ACTED_IN]->(m:Movie) RETURN p.name, m.title".equals(sql);
}
清单 2. 将表名解析为大写
var properties = new Properties();
properties.put("username", "neo4j");
properties.put("password", "verysecret");
properties.put("enableSQLTranslation", "true");
properties.put("s2c.parseNameCase", "UPPER");

var url = "jdbc:neo4j://:7687";
var query = "SELECT * FROM people";
try (var connection = DriverManager.getConnection(url, properties)) {
    var sql = connection.nativeSQL(query);
    assert """
            MATCH (people:PEOPLE)
            RETURN *""".equals(sql);
}

SQL 解析器中的命名参数语法默认为 :name(如 Oracle, JPA, Spring 所支持的,即冒号后跟名称)。以下示例将此前缀更改为 $(与 Cypher 使用的前缀相同):

清单 3. 更改参数前缀并添加连接列的映射
var properties = new Properties();
properties.put("username", "neo4j");
properties.put("password", "verysecret");
properties.put("enableSQLTranslation", "true");
properties.put("s2c.parseNamedParamPrefix", "$");
properties.put("s2c.joinColumnsToTypeMappings", "people.movie_id:DIRECTED");

var url = "jdbc:neo4j://:7687";
var query = """
        SELECT *
        FROM people p
        JOIN movies m ON m.id = p.movie_id
        WHERE p.name = $1
        """;
try (var connection = DriverManager.getConnection(url, properties)) {
    var sql = connection.nativeSQL(query);
    assert """
            MATCH (p:people)-[:DIRECTED]->(m:movies)
            WHERE p.name = $1
            RETURN *""".equals(sql);
}

当工具生成此类名称且不允许自定义时,此功能非常有用。

支持的语句

以下语句均经过测试,描述了您可以从默认转换层获得的结果

转换概念

表名到标签

最简单的 SELECT 语句是无需 FROM 子句的语句,例如:

SELECT 1

它等同于 Cypher 的 RETURN

RETURN 1

没有 JOIN 子句的 SELECT 语句非常容易转换。这里的挑战是如何将表名映射到标签。

  • 我们默认区分大小写地解析 SQL 语句

  • 表名被映射到节点标签

  • 表别名被用作可识别的符号名称

SELECT t.a, t.b
FROM My_Table (1)
  AS t (2)
WHERE t.a = 1
1 将原样用作匹配的标签,即 My_Table
2 表别名将成为节点别名

整个结构将被转换为

MATCH (t:My_Table)
WHERE t.a = 1
RETURN t.a, t.b

表别名是可选的;如果您省略它们,我们将从标签和类型中派生别名。如果您检查转换后的查询,建议使用别名,因为这会使查询更易读。

星号选择(Star-Selects)

星号或 * 选择有不同的形式

未限定的

SELECT * FROM table

限定的

SELECT t.* FROM table t

以及一种变体,选择关系本身:SELECT t FROM table t

我们利用这一点让您决定是希望将 Neo4j 节点和关系作为实体返回,还是作为映射返回,或者展平为单独的列。后者要求我们的转换器能够访问底层 Neo4j 数据库的架构。以下部分描述了这些用例。

投影单个属性

不要使用星号选择,而是列举属性

SELECT m.title FROM Movie m

表别名将被用作符号名称

MATCH (m:Movie)
RETURN m.title;

您可以省略表别名

SELECT title FROM Movie

小写的表名将成为符号名称;当只有一个标签匹配时,会为其添加别名,以便于通过该名称检索。

MATCH (movie:Movie)
RETURN movie.title AS title;

按名称访问 JDBC 列会导致难以维护的代码,因为列重命名也会影响代码。为避免这种情况,请给列起别名

SELECT title AS title FROM Movie

使其拥有一个稳定的、众所周知的名称

MATCH (movie:Movie)
RETURN movie.title AS title;
投影所有属性

SELECT * 语句的转换方式取决于是否可以连接到 Neo4j 数据库。

SELECT * FROM Person p

如果您处于离线状态,您将得到以下 Cypher 语句

MATCH (p:Person) RETURN *

上述查询将返回一列 (p),其中包含一个 Neo4j 节点。这通常不是您在关系型世界中所期望的。如果您在线运行转换,并且可以检索到 Neo4j 元数据,您将得到一个展平每个节点和关系及其元素 ID 的属性的语句

如果 Person 节点具有属性 bornname

SELECT * FROM Person p

您将得到此 Cypher 语句

MATCH (p:Person)
RETURN elementId(p) AS `v$id`,
       p.born AS born, p.name AS name

这也适用于多表情况(Movie 具有属性 titlereleased

SELECT * FROM Person p JOIN Movie m ON m.id = p.acted_in
MATCH (p:Person)-[acted_in:ACTED_IN]->(m:Movie)
RETURN elementId(p) AS `v$id`, p.born AS born, p.name AS name,
       elementId(m) AS `v$id1`, m.title AS title, m.released AS released

我们会为名称冲突的列名添加递增数字(例如,当 MoviePerson 中都有 nameremark 属性时)

SELECT * FROM Person p JOIN Movie m ON m.id = p.acted_in

注意每个重复名称后的递增数字

MATCH (p:Person)-[acted_in:ACTED_IN]->(m:Movie)
RETURN elementId(p) AS `v$id`,
       p.born AS born, p.name AS name, p.remark AS remark,
       elementId(m) AS `v$id1`,
       m.name AS name1, m.released AS released, m.remark AS remark1

以下示例使用连接表来访问关系(我们稍后会在本手册的讨论连接时解释这一点),属性展平在这里也有效

SELECT *
FROM people p
JOIN movie_actors r ON r.person_id = p.id
JOIN movies m ON m.id = r.person_id
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
RETURN elementId(p) AS `v$id`,
       p.born AS born, p.name AS name,
       elementId(p) AS `v$person_id`,
       elementId(r) AS `v$id1`, r.role AS role,
       elementId(m) AS `v$movie_id`,
       elementId(m) AS `v$id2`,
       m.title AS title, m.released AS released

清单 4. 不指定表别名的排序
SELECT * FROM Person p ORDER BY name ASC
MATCH (p:Person)
RETURN elementId(p) AS `v$id`,
       p.born AS born, p.name AS name
ORDER BY p.name

也可以使用限定的别名。如果没有 Neo4j 元数据可用,您将获得节点/关系的属性映射

SELECT m.*, p.*
FROM Person p
JOIN Movie m ON m.id = p.acted_in

相应的列必须在 JDBC 中向下转型为映射

MATCH (p:Person)-[acted_in:ACTED_IN]->(m:Movie)
RETURN m{.*} AS m, p{.*} AS p

如果我们添加更多数据(例如 Person 中的 bornname),限定星号将投影所有这些数据(注意我们还从 Movie 表中投影了一个已知列)

SELECT p.*, m.title AS title
FROM Person p
JOIN Movie m ON m.id = p.acted_in
MATCH (p:Person)-[acted_in:ACTED_IN]->(m:Movie)
RETURN elementId(p) AS `v$id`, p.born AS born, p.name AS name, m.title AS title
返回节点和关系

投影表别名的语句,例如

SELECT m FROM Movie m

将导致 Cypher 语句返回匹配的节点作为节点。

MATCH (m:Movie)
RETURN m;

节点也可以有别名

SELECT m AS node FROM Movie m
MATCH (m:Movie)
RETURN m AS node;

也可以使用未添加别名的表

SELECT movie FROM Movie
MATCH (movie:Movie)
RETURN movie;

也支持多个实体

SELECT p, r, m FROM Person p
JOIN ACTED_IN r ON r.person_id = p.id
JOIN Movie m ON m.id = r.movie_id
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie) RETURN p, r, m

比较 SQL 与 Cypher 示例

以下示例来源:比较 SQL 与 Cypher

查找所有产品
选择并返回记录

products 表中选择所有内容。

SELECT p.*
FROM products as p

类似地,在 Cypher 中,您只需 MATCH 一个简单的模式:所有带有 标签 Product 的节点并 RETURN 它们。

MATCH (p:Product)
RETURN p{.*} AS p

上述查询将投影匹配节点的所有属性。如果您想返回节点本身,请在选择时不使用星号

SELECT p
FROM products as p
MATCH (p:Product)
RETURN p
字段访问、排序和分页

只返回属性的一个子集会更有效率,例如 ProductNameUnitPrice。顺便说一句,我们按价格排序并只返回最贵的 10 个项目。(请记住,标签、关系类型和属性名在 Neo4j 中是 区分大小写 的。)

SELECT p.`productName`, p.`unitPrice`
FROM products as p
ORDER BY p.`unitPrice` DESC
LIMIT 10
MATCH (p:Product)
RETURN p.productName, p.unitPrice ORDER BY p.unitPrice DESC LIMIT 10

默认的排序方向将按原样转换

SELECT * FROM Movies m ORDER BY m.title
MATCH (m:Movies)
RETURN * ORDER BY m.title
DISTINCT 投影

DISTINCT 投影关键字已被处理

SELECT DISTINCT m.released FROM Movies m
MATCH (m:Movies)
RETURN DISTINCT m.released

它也适用于 * 投影

SELECT DISTINCT m.* FROM Movies m
MATCH (m:Movies)
RETURN DISTINCT m {.*} AS m

但是,由于限定星号在可用时会使用元数据,因此有数据库连接时的转换方式会有所不同

SELECT DISTINCT m.* FROM Movies m
MATCH (m:Movies)
RETURN DISTINCT elementId(m) AS `v$id`, m.title AS title, m.released AS released

请注意,每一行都包含 Neo4j 元素 ID,这使得每一行都是唯一的。综上所述,DISTINCT 子句在与星号一起使用时用途有限。

表达式

大多数 SQL 表达式都有相应的 Cypher 表达式,并且可以直接转换。

字面值

字面值是 1:1 转换的。

SELECT
    1, TRUE, FALSE, NULL, 'a'
RETURN 1, TRUE, FALSE, NULL, 'a'

算术表达式

算术表达式是 1:1 转换的。

SELECT
    1 + 2,
    1 - 2,
    1 * 2,
    1 / 2,
    square(2)

请注意,默认转换器的底层技术内部使用了 Cypher-DSL,它会用圆括号包裹算术(和逻辑)表达式

RETURN
    (1 + 2),
    (1 - 2),
    (1 * 2),
    (1 / 2),
    (2 * 2)

函数

数值函数

我们可以转换 Neo4j 的 Cypher 实现所支持的所有数值函数:数学函数 - 数值

SELECT
    abs(1),
    ceil(1),
    floor(1),
    round(1),
    round(1, 1),
    sign(1)

将被转换为

RETURN
    abs(1),
    ceil(1),
    floor(1),
    round(1),
    round(1, 1),
    sign(1)
对数函数

Neo4j 支持广泛的 对数函数

SELECT
    exp(1),
    ln(1),
    log(2, 1),
    log10(1),
    sqrt(1)

将被转换为

RETURN
    exp(1),
    log(1),
    (log(1) / log(2)),
    log10(1),
    sqrt(1)
三角函数

对三角函数的调用

SELECT
    acos(1),
    asin(1),
    atan(1),
    atan2(1, 2),
    cos(1),
    cot(1),
    degrees(1),
    pi(),
    radians(1),
    sin(1),
    tan(1)

将被转换为相应的 Neo4j 函数

RETURN
    acos(1),
    asin(1),
    atan(1),
    atan2(1, 2),
    cos(1),
    cot(1),
    degrees(1),
    pi(),
    radians(1),
    sin(1),
    tan(1)
字符串函数

以下字符串操作保证有效

SELECT
    lower('abc'),
    cast(3 as varchar),
    trim(' abc '),
    length('abc'),
    left('abc', 2),
    ltrim(' abc '),
    replace('abc', 'b'),
    replace('abc', 'b', 'x'),
    reverse('abc'),
    right('abc', 2),
    rtrim(' abc '),
    substring('abc', 2 - 1),
    substring('abc', 2 - 1, 2),
    upper('abc')

并将被转换为 Neo4j 版本

RETURN
    toLower('abc'),
    toString(3),
    trim(' abc '),
    size('abc'),
    left('abc', 2),
    ltrim(' abc '),
    replace('abc', 'b', NULL),
    replace('abc', 'b', 'x'),
    reverse('abc'),
    right('abc', 2),
    rtrim(' abc '),
    substring('abc', (2 - 1)),
    substring('abc', (2 - 1), 2),
    toUpper('abc')
标量函数

输入

SELECT
    coalesce(1, 2),
    coalesce(1, 2, 3),
    nvl(1, 2),
    cast('1' as boolean),
    cast(1 as float),
    cast(1 as double precision),
    cast(1 as real),
    cast(1 as tinyint),
    cast(1 as smallint),
    cast(1 as int),
    cast(1 as bigint)

将被转换为(参见 标量函数

RETURN
    coalesce(1, 2),
    coalesce(1, 2, 3),
    coalesce(1, 2),
    toBoolean('1'),
    toFloat(1),
    toFloat(1),
    toFloat(1),
    toInteger(1),
    toInteger(1),
    toInteger(1),
    toInteger(1)

查询表达式

也支持几种高级 SQL 表达式。

CASE 简单格式

简单 CASE 表达式

SELECT
    CASE 1 WHEN 2 THEN 3 END,
    CASE 1 WHEN 2 THEN 3 ELSE 4 END,
    CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 END,
    CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END
RETURN CASE 1 WHEN 2 THEN 3 END, CASE 1 WHEN 2 THEN 3 ELSE 4 END, CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 END, CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END

有关更多信息,请参阅 Cypher → 条件表达式 (CASE)

CASE 高级格式

以及使用搜索的 CASE 语句

SELECT
    CASE WHEN 1 = 2 THEN 3 END,
    CASE WHEN 1 = 2 THEN 3 ELSE 4 END,
    CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 END,
    CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 ELSE 7 END

将被转换为

RETURN
    CASE WHEN 1 = 2 THEN 3 END,
    CASE WHEN 1 = 2 THEN 3 ELSE 4 END,
    CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 END,
    CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 ELSE 7 END

有关更多信息,请参阅 Cypher → 条件表达式 (CASE)

CASE 缩写(非 COALESCENVL

输入

SELECT
    nullif(1, 2),
    nvl2(1, 2, 3)

将被转换为

RETURN
    CASE WHEN 1 = 2 THEN NULL ELSE 1 END,
    CASE WHEN 1 IS NOT NULL THEN 2 ELSE 3 END

谓词

与表达式一样,许多用作谓词的逻辑 SQL 表达式和条件可以直接转换为 Cypher 谓词。

合取和析取

逻辑合取和析取均受支持。

SELECT 1 FROM p WHERE 1 = 1 AND 2 = 2 OR 3 = 3
MATCH (p:p)
WHERE ((1 = 1
    AND 2 = 2)
  OR 3 = 3)
RETURN 1

输入

SELECT 1 FROM p WHERE NOT 1 = 1 XOR 2 = 2

将被转换为

MATCH (p:p)
WHERE (NOT (1 = 1)
  XOR 2 = 2)
RETURN 1

运算符

算术运算符

输入

SELECT 1 FROM p WHERE 1 = 1 AND 2 > 1 AND 1 < 2 AND 1 <= 2 AND 2 >= 1 AND 1 != 2

将被转换为

MATCH (p:p)
WHERE (1 = 1
  AND 2 > 1
  AND 1 < 2
  AND 1 <= 2
  AND 2 >= 1
  AND 1 <> 2)
RETURN 1
Between

SQL 中的 Between 是包含边界的

SELECT 1 FROM p WHERE 2 BETWEEN 1 AND 3

并将被转换为(由于底层生成器的限制,我们无法生成更短的形式 (1 ⇐ 2 ⇐ 3))

MATCH (p:p)
WHERE (1 <= 2) AND (2 <= 3)
RETURN 1

SQL 有一个用于 BETWEEN 子句的 SYMMETRIC 关键字,用于指示您不关心范围的哪个边界更大

SELECT 1 FROM p WHERE 2 BETWEEN SYMMETRIC 3 AND 1

我们将其转换为析取(OR 逻辑)

MATCH (p:p)
WHERE (3 <= 2) AND (2 <= 1) OR (1 <= 2) AND (2 <= 3)
RETURN 1

逻辑行值表达式

上面的示例基于标量表达式。行值表达式也将被转换

SELECT 1
FROM p
WHERE (1, 2) = (3, 4)
OR (1, 2) < (3, 4)
OR (1, 2) <= (3, 4)
OR (1, 2, 3) <> (4, 5, 6)
OR (1, 2, 3) > (4, 5, 6)
OR (1, 2, 3) >= (4, 5, 6)

导致语义等价的 Cypher

MATCH (p:p)
WHERE 1 = 3 AND 2 = 4
OR (1 < 3 OR 1 = 3 AND 2 < 4)
OR (1 < 3 OR 1 = 3 AND 2 <= 4)
OR (1 != 4 AND 2 != 5 AND 3 != 6)
OR (1 > 4 OR 1 = 4 AND (2 > 5 OR 2 = 5 AND 3 > 6))
OR (1 > 4 OR 1 = 4 AND (2 > 5 OR 2 = 5 AND 3 >= 6))
RETURN 1

Null 处理

对于标量表达式

输入

SELECT 1 FROM p WHERE 1 IS NULL AND 2 IS NOT NULL

将被转换为

MATCH (p:p)
WHERE (1 IS NULL
  AND 2 IS NOT NULL)
RETURN 1
对于行值表达式

输入

SELECT 1 FROM p WHERE (1, 2) IS NULL OR (3, 4) IS NOT NULL

将被转换为

MATCH (p:p)
WHERE
  (1 IS NULL AND 2 IS NULL)
  OR (3 IS NOT NULL AND 4 IS NOT NULL)
RETURN 1

LIKE 运算符

LIKE 运算符

SELECT * FROM movies m WHERE m.title LIKE '%Matrix%' OR m.title LIKE 'M_trix'

将被转换为正则表达式,将 % 替换为 .*

MATCH (m:`movies`) WHERE m.title CONTAINS 'Matrix' OR m.title =~ 'M.trix'
RETURN *

使用连接(Join)来映射关系

表面上看,连接是 SQL 中具体化的关系(外键不是)。遗憾的是,映射到 Cypher 并不那么简单。有几种实现方案:

  • 当通过某一列连接两个表时,取左侧表的列,将其名称用作关系类型,并将其视为从左到右的输出方向。

  • 当通过连接表(Intersection table)连接两个表时(这在 SQL 中通常用于带有属性的 m:n 关系),使用该连接表的名称作为关系类型。

我们实现了一些变体,但我们不保证它们在所有情况下都绝对有效。

不支持外连接(Outer joins)。

1:n 连接

自然连接 (Natural joins)

SQL NATURAL 连接是表示关系名称的最简单方法,无需进行任何映射。一跳 NATURAL JOIN 将转换为匿名、通配符关系。

SELECT p, m FROM Person p
NATURAL JOIN Movie m
MATCH (p:Person)-->(m:Movie) RETURN p, m

NATURAL 连接可以链式使用,且不需要连接表存在。这将被转换为 Neo4j 关系

SELECT p.name, r.roles, m.* FROM Person p
NATURAL JOIN ACTED_IN r
NATURAL JOIN Movie m
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
RETURN p.name, r.roles,
       elementId(m) AS `v$id`, m.title AS title, m.released AS released
简单连接

假设我们将转换器配置为使用以下表映射

  • people 映射到标签 People

  • movies 映射到标签 Movie

在此基础上,我们将

SELECT p.name, m.title
FROM people p
JOIN movies m ON m.id = p.directed

转换为

MATCH (p:Person)-[directed:DIRECTED]->(m:Movie)
RETURN p.name, m.title

DIRECTED 是左表 (p.directed) 中连接列的大写版本。

如果我们有不同的列名,可以以 people.movie_id:DIRECTED 的形式添加连接列映射

SELECT p.name, m.title
FROM people p
JOIN movies m ON m.id = p.movie_id

转换为

MATCH (p:Person)-[directed:DIRECTED]->(m:Movie)
RETURN p.name, m.title
使用 ON 子句

我们在这里为表名和列名使用了反引号,且没有使用映射。

SELECT p.name, m.title
FROM `Person` as p
JOIN `Movie` as m ON (m.id = p.`DIRECTED`)

转换与之前相同

MATCH (p:Person)-[directed:DIRECTED]->(m:Movie)
RETURN p.name, m.title

m:n 连接

连接表是包含对其他两个表的引用的表,其形式为至少两列。此结构在关系模型中通常用于创建 m:n 关系。这种辅助结构在 Neo4j 中是不必要的。我们可以从一个标签到另一个标签建模任意数量的出站和入站关系,它们也可以拥有属性。因此,我们可以将该结构用于我们的转换器。

以下示例使用了如下配置的映射:

  • people 映射到标签 People

  • movies 映射到标签 Movie

  • movie_actors 映射到 ACTED_IN

SELECT p.name, m.title
FROM people p (1)
JOIN movie_actors r ON r.person_id = p.id (2)
JOIN movies m ON m.id = r.person_id (3)
1 用于映射出站关系的表
2 在下一个 JOIN 子句中再次使用的连接表
3 最后的 JOIN 子句

我们不做语义分析:连接的顺序很重要,并将导致以下查询:

MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
RETURN p.name, m.title

多个连接将导致关系链

SELECT p.name AS actor, d.name AS director, m.title
FROM people p
 JOIN movie_actors r ON r.person_id = p.id
 JOIN movies m ON m.id = r.person_id
 JOIN movie_directors r2 ON r2.movie_id = m.id
 JOIN people d ON r2.person_id = d.id
MATCH (p:`Person`)-[r:`ACTED_IN`]->(m:`Movie`)<-[r2:`DIRECTED`]-(d:`Person`)
RETURN p.name AS actor, d.name AS director, m.title

请注意 DIRECTED 关系的方向是如何由连接列的顺序定义的。

DML 语句

本节列出了支持的数据操作语言 (DML) 语句。虽然 SELECT 语句在技术上也是 DML,但它已在 转换概念 中介绍。

删除节点

节点可以通过 SQL DELETE 语句删除。

例如,无条件删除所有 person 节点:

DELETE FROM person
MATCH (person:person)
DELETE person

可以添加 WHERE 子句来防止误删

DELETE FROM person
WHERE person.id = 1
MATCH (person:person)
WHERE person.id = 1
DELETE person

如果您想删除所有内容,但工具报错,只需添加一个始终为 true 的条件

DELETE FROM person
WHERE true
MATCH (person:person)
WHERE true
DELETE person

或者,条件也可以是始终计算为 false 的条件,从而不删除任何内容

DELETE FROM person
WHERE false
MATCH (person:person)
WHERE false
DELETE person

表可以有别名,别名也会在 Cypher 中使用

DELETE FROM person p
MATCH (p:person)
DELETE p

在配置了表名映射到的标签时,也支持表别名。使用相同的查询并配置 table_mappings=person:Person

DELETE FROM person p

将被转换为

MATCH (p:Person)
DELETE p

您可以使用 SQL TRUNCATEDETACH DELETE 节点

TRUNCATE TABLE people

将被转换为

MATCH (people:Person)
DETACH DELETE people

插入数据

具有显式列和常量值的单个值列表可以通过简单的 INSERT 语句插入

INSERT INTO People (first_name, last_name, born) VALUES ('Helge', 'Schneider', 1955)
CREATE (people:`Person` {first_name: 'Helge', last_name: 'Schneider', born: 1955})

支持所有表达式,包括参数。在 Cypher 中,参数名称将从 1 开始递增。

INSERT INTO People (first_name, last_name, born) VALUES (?, ?, ?)
CREATE (people:`Person` {first_name: $1, last_name: $2, born: $3})

如果您在插入目标上省略了列名,我们将自动生成名称

INSERT INTO People VALUES ('Helge', 'Schneider', 1955)

注意 unknown field xxx 属性名

CREATE (people:`Person` {`unknown field 0`: 'Helge', `unknown field 1`: 'Schneider', `unknown field 2`: 1955})

SQL VALUES 子句实际上支持值列表

INSERT INTO People (first_name, last_name, born) VALUES
    ('Helge', 'Schneider', 1955),
    ('Bela', 'B', 1962)

这些值将被转换为 Cypher 数组,以便在 Cypher 语句中进行 UNWIND。这是批量插入的绝佳解决方案

UNWIND [
  {first_name: 'Helge', last_name: 'Schneider', born: 1955},
  {first_name: 'Bela', last_name: 'B', born: 1962}]
AS properties
CREATE (people:`Person`)
SET people = properties

也支持 returning 子句

INSERT INTO People p (name) VALUES (?) RETURNING elementId(p)
CREATE (p:Person {name: $1}) RETURN elementId(p)

Upserts

我们通过非标准但非常常见的 ON DUPLICATEON CONFLICT SQL 子句支持有限范围的“upsert”。Upsert 被转换为 MERGE 语句。虽然它们在没有约束的情况下也可以工作,但您确实应该在合并的节点属性上具有唯一性约束,否则 Neo4j 可能会创建重复项(请参阅 理解 merge 的工作原理)。

所有列的 Upsert 可以通过 ON DUPLICATE KEY IGNOREON CONFLICT IGNORE 发生。虽然 ON DUPLICATE KEY 提供了升级选项,但它假定被违反的主键(或唯一键)是已知的。虽然在关系型系统中这几乎肯定是真的,但这个在没有数据库连接下运行的转换层并不知道这一点。

清单 5. 使用 ON DUPLICATE KEY IGNORE 进行 Upsert
INSERT INTO Movie(title, released) VALUES(?, ?) ON DUPLICATE KEY IGNORE
MERGE (movie:`Movie` {title: $1, released: $2})
清单 6. 使用 ON CONFLICT IGNORE 进行 Upsert
INSERT INTO actors(name, firstname) VALUES(?, ?) ON CONFLICT DO NOTHING
MERGE (actors:`Actor` {name: $1, firstname: $2})

如果您想定义一个操作,必须使用 ON CONFLICT 并指定您想要合并的键。

INSERT INTO tbl(i, j, k) VALUES (1, 40, 700)
ON CONFLICT (i) DO UPDATE SET j = 0, k = 2 * EXCLUDED.k

请注意如何使用特殊引用 EXCLUDED 来指代那些不属于键的列的值。它们将在 ON MATCH SET 子句中重用其值。

MERGE (tbl:`tbl` {i: 1})
ON CREATE SET tbl.j = 40, tbl.k = 700
ON MATCH SET tbl.j = 0, tbl.k = (2 * 700)

这也适用于参数

INSERT INTO tbl(i, j, k) VALUES (1, 2, ?)
ON CONFLICT (i) DO UPDATE SET j = EXCLUDED.k
MERGE (tbl:`tbl` {i: 1})
ON CREATE SET tbl.j = 2, tbl.k = $1
ON MATCH SET tbl.j = $1

也可以直接指定具体的合并列,而不是在所有列上进行合并。它将使用 ON CREATE 进行转换

INSERT INTO tbl(i, j, k) VALUES (1, 40, 700)
ON CONFLICT (i) DO NOTHING
MERGE (tbl:`tbl` {i: 1})
ON CREATE SET tbl.j = 40, tbl.k = 700

使用 ON CONFLICT 并指定键是使用 MERGE 语句插入多行的唯一方法

INSERT INTO People (first_name, last_name, born) VALUES
    ('Helge', 'Schneider', 1955),
    ('Bela', 'B', 1962)
ON CONFLICT(last_name) DO UPDATE SET born = EXCLUDED.born
UNWIND [{first_name: 'Helge', last_name: 'Schneider', born: 1955}, {first_name: 'Bela', last_name: 'B', born: 1962}] AS properties
MERGE (people:`People` {last_name: properties['last_name']})
ON CREATE SET
  people.first_name = properties.first_name,
  people.born = properties.born
ON MATCH SET people.born = properties['born']

操作关系

Neo4j 中的关系涉及三个实体:起点节点、关系本身和终点节点。使用 Cypher,这可以在一个语句中表达

CREATE (a:Person {name: 'Jaret Leto'})
       -[:ACTED_IN {role: 'Ares'}]->
       (m:Movie {title: 'TRON Ares'})
RETURN *;

这为您提供了这个图

movie rel

另一方面,在 SQL 中,我们通常至少使用两个,或者根据规范化程度使用三个表来存储它(想想 peoplemovies 和作为连接表的 acted_in,引用 peoplemovies 并存储任何属性)。

虽然您显然可以从您能想到的任意多个关系中 SELECT,但 SQL 中的 INSERT 子句不支持多个插入目标,只能支持单个表。没有这样的结构,SQL 到 Cypher 转换器无法直接将 SQL 语句转换为上述那样的模式。转换器只能尝试尽其所能推断意图。

根据现有数据从表中推断关系

假设执行了上述语句,JDBC 驱动程序实际上会将该关系显示为数据库元数据中的虚拟表

movie rel virtual table

拥有这样一个关系足以让 JDBC 驱动程序将其用作一种模板。JDBC 驱动程序将根据给定的表名查询关系虚拟表,如果匹配,则自动推断以下 SQL 语句的目标是关系

INSERT INTO Person_ACTED_IN_Movie(name, role, title)
VALUES('Jodie Turner-Smith', 'Athena', 'TRON Ares');

更新后的图如下所示

after first insert

值得注意的事情是:

  • 没有创建额外的 Movie 节点

  • name 属性已用于 Person 标签,title 用于 Movie

  • role 已用于关系上

匹配基于虚拟表。如果两个节点都有 name 属性,该属性将仅用于起点节点。不匹配节点属性也不匹配关系属性的列将被添加到关系中。

如果架构在节点和关系上发现了相同的属性名,则节点具有优先权。如果关系的两侧都出现了相同的属性,它将仅在第一次出现(起点节点)时使用。如果这对您无效,则必须限定目标属性。

INSERT 语句也没有像上面那样转换成简单的 CREATE … 语句。插入关系表达了创建关系的明确意图,因此,如果可能,JDBC 驱动程序将始终创建如下语句

MERGE (lhs:LabelLeft {propN: 'Value'})
MERGE (rhs:LabelRight {propM: 'Value'})
CREATE (lhs)-[:TYPE]->(rhs);

节点的所有属性都将用于合并。如果 JDBC 驱动程序无法将任何属性匹配到标签,它将为该标签发出 CREATE 语句。

如果可以,请确保为您将要插入关系的节点属性定义唯一约束,这既是为了性能,也是为了考虑 Neo4j 的 MERGE 行为。在并发更新下,MERGE 仅保证 MERGE 模式的存在,而不保证唯一性,请参阅 MERGE

这同样适用于多值。像以下这样的语句

INSERT INTO Person_ACTED_IN_Movie(name, role, title) VALUES
 ('Gillian Anderson', 'Elisabeth Dillinger', 'TRON Ares'),
 ('Arturo Castro', 'Seth Flores', 'TRON Ares');

或如下的批量 JDBC 变体

try (var statement = connection.prepareStatement("""
    INSERT INTO Person_ACTED_IN_Movie(name, role, title)
    VALUES (?, ?, ?)""")
) {
    statement.setString(1, "Jeff Bridges");
    statement.setString(2, "Kevin Flynn");
    statement.setString(3, "TRON Ares");
    statement.addBatch();
    statement.setString(1, "Greta Lee");
    statement.setString(2, "Eve Kim");
    statement.setString(3, "TRON Ares");
    statement.executeBatch();
}

将被转换为利用 UNWIND 的 Cypher 查询,从而非常适合批量加载关系。

根据表名推断关系

默认情况下,转换器的配置方式是:如果既没有匹配的现有关系,也没有具有相同标签的节点,它会尝试从要插入的表名中推断关系。

INSERT 子句的目标表是:Person_ACTED_IN_Movie

  • 存在关系 (:Person)-[:ACTED_IN]→(:Movie):此关系及其所有组成部分的属性将用作模板

  • 存在带有标签 Person_ACTED_IN_Movie 的节点:INSERT 子句将被转换为 CREATE (n:Person_ACTED_IN_Movie),不会尝试创建关系

  • 否则,由于 Person_ACTED_IN_Movie 符合默认模式,带有标签 PersonMovie 的节点将被合并或创建,并创建一个新的关系 ACTED_IN

推断关系的默认模式旨在检测混合大小写字母中的标签,这些标签可能包含由每个站点上的单个下划线与大写的关系类型分隔的下划线,即 Person_ACTED_IN_Movie 将被检测到,A_RELATES_TO_B 也是如此。后者将为节点使用标签 AB,并为关系类型使用 RELATES_TO

在没有现有模板的情况下,我们需要一种将属性映射到源节点、目标节点或关系的方法。默认情况下,当仅通过表名推断出关系时,所有属性都将放置在关系上。

然而,JDBC 驱动程序可以利用 SQL 的一项特性,允许您像这样限定列

INSERT INTO Person_ACTED_IN_Movie(Person.name, ACTED_IN.role, Movie.title)
VALUES ('Evan Peters', 'Julian Dillinger', 'TRON Ares');

现在,大多数 SQL 数据库不支持 INSERT 语句中的限定列,但它是有效的 SQL,并且我们可以解析它。上述限定列将被适当地分配到 PersonMovieACTED_IN 上。

删除关系

虽然 Samuel L. Jackson 演过很多电影,但他并没有在 TRON: Legacy 中扮演他自己,您可能想从图中删除该关系

tronwrong

通过普通的 DELETE FROM 语句可以实现这一点,在该语句上,关系推断的工作方式与上述相同。与创建时如果可能则合并起点和终点节点且不修改其他部分一致,删除将仅影响关系本身

DELETE FROM Person_ACTED_IN_Movie
WHERE title = 'TRON: Legacy'
  AND name = 'Samuel L. Jackson'
  AND role = 'himself'

谓词将被下推,以便它们在正确的目标上进行过滤(此处::Actor 节点的 name 属性,:Movietitle 和关系 :ACTED_INrole)。结果可以通过 Cypher 确认(JDBC 驱动程序不支持外连接)

/*+ NEO4J FORCE_CYPHER */
MATCH (p:Person {name: 'Samuel L. Jackson'})
RETURN p.name, COUNT {(p)-[:ACTED_IN]->(:Movie)} AS cnt

结果将是只有一行,说明 Samuel L. Jackson 在零部电影中演出,这显然是一个真实的谎言。

截断关系

TRUNCATE 关键字的工作方式相同,只是根据 SQL 标准,它将无条件删除所有关系实例

TRUNCATE Person_ACTED_IN_Movie

将删除所有 ACTED_IN 关系,但不会删除任何关联的节点。

更新关系

当您像这样创建图时

INSERT INTO Person_ACTED_IN_Movie(Person.name, ACTED_IN.role, Movie.title)
VALUES
    ('Jaret Leto', 'Ares', 'Morbius'),
    ('Greta Lee', 'Eve Kim', 'TRON Ares'),
    ('Jodie Turner-Smith', 'Elisha James', 'TRON Ares')

后来意识到 Jodie Turner-Smith 扮演的是 Athena,您可以通过 UPDATE 语句修复该关系

try (var stmt = con.prepareStatement(
    "UPDATE Person_ACTED_IN_Movie SET role = ? WHERE name = ?")
) {
    stmt.setString(1, "Athena");
    stmt.setString(2, "Jodie Turner-Smith");
    stmt.executeUpdate();
}

用于确定在插入期间将哪个列应用于起点节点、关系或终点节点属性的相同算法也应用于更新。这意味着您还可以更新节点的属性。但请注意,这些更新将仅通过 SET 应用,不会进行其他 MERGE 更新。这意味着修复上述图中的第二个错误——确保 Leto 在 TRON Ares 中扮演 Ares,而不是在 Morbius 中——会创建第二个名为 TRON Ares:Movie 节点

UPDATE Person_ACTED_IN_Movie
SET title = 'TRON Ares'
WHERE name = 'Jaret Leto'

将导致有两个名为 TRON Ares:Movie 节点(SELECT count(*) FROM Movie WHERE title = 'TRON Ares' 将返回 2)。