Cypher 工作流程

概述

Neo4j 驱动程序公开了一个 Cypher 通道,可以通过该通道执行数据库工作(有关 Cypher 查询语言的更多信息,请参见Cypher 手册)。

工作本身按以下方式组织成 **会话**、**事务** 和 **查询**

sessions queries transactions
图 1. 会话、查询和事务

会话始终绑定到 **单个事务上下文**,通常是单个数据库。

使用书签机制,会话还提供对正确事务顺序的保证,即使事务发生在多个集群成员上。此效果称为 因果链

会话

会话是 **用于因果链事务序列的轻量级容器**(参见操作手册 → 因果一致性)。它们本质上为以书签形式存储事务顺序信息提供了上下文。

当事务开始时,包含该事务的会话从驱动程序连接池中获取连接。在事务提交(或回滚)时,会话会再次释放该连接。这意味着只有当会话执行工作时,它才会占用连接资源。空闲时,不会使用任何此类资源。

由于会话保证了排序,因此会话一次只能托管 **一个事务**。对于并行执行,应使用多个会话。在 线程安全 存在问题的语言中,**会话不应被视为线程安全的**。

关闭会话会强制任何打开的事务回滚,并将其关联的连接相应地释放回池中。

会话绑定到单个事务上下文,该上下文在构造时指定。**Neo4j** 在其自己的上下文中公开每个数据库,从而通过设计禁止跨数据库事务(或会话)。同样,**绑定到不同数据库的会话可能无法通过在它们之间传播书签来因果链**。

各个语言驱动程序提供了多个会话类,每个类都面向特定的编程风格。每个会话类都提供类似的功能集,但为客户端应用程序提供根据应用程序结构以及是否使用任何框架(如果有)进行选择的选项。

会话类在 会话 API 中进行了描述。有关更多详细信息,请参阅您语言的 API 文档

事务

事务是包含一个或多个 **Cypher 查询** 的 **原子工作单元**。事务可能包含读或写工作,通常会路由到适当的服务器以执行,它们将在其中完整执行。如果事务失败,则需要从头开始重试事务。这是 事务管理器 的责任。

Neo4j 驱动程序通过 事务函数 机制提供 **事务管理**。此机制通过会话对象上的方法公开,这些方法接受一个函数对象,该函数对象可以多次针对不同的服务器播放,直到它成功或达到超时为止。此方法推荐用于大多数客户端应用程序。

一个方便的 **简短形式的替代方法** 是 自动提交 事务机制。这为单查询事务提供了一种有限形式的事务管理,作为略小的代码开销的权衡。这种形式的事务对于 **快速脚本和不需要高可用性保证的环境** 很有用。它也是运行 PERIODIC COMMIT 查询所需的交易形式,它们是唯一管理自身交易的 Cypher 查询类型。

还提供了一个更底层的 **非管理事务 API**,用于高级用例。当客户端应用了替代事务管理层,并且需要以自定义方式管理错误处理和重试时,这很有用。要了解有关如何使用非管理事务的更多信息,请参阅您语言的 API 文档

查询和结果

查询由向服务器发送的执行 Cypher 语句的请求以及返回给客户端的响应组成。结果以 **记录流** 形式传输,以及标头和页脚元数据,并且可以由客户端应用程序增量使用。使用 反应式功能,可以通过允许 Cypher 结果在中途暂停或取消来增强记录流的语义。

要执行 Cypher 查询,需要 **查询文本** 以及可选的 **命名参数** 集。文本可以包含在运行时用相应的值替换的 **参数占位符**。虽然可以运行非参数化的 Cypher 查询,但 **良好的编程实践是在 Cypher 查询中尽可能使用参数**。这允许在 Cypher 引擎中缓存查询,这对于性能有利。参数值应遵守 /docs/cypher-manual/4.1/syntax/values

通常还提供结果摘要。这包含与查询执行和结果内容相关的其他信息。对于 EXPLAINPROFILE 查询,这是返回查询计划的地方。有关这些查询的更多信息,请参见Cypher 手册 → 分析查询

因果链和书签

在使用 **因果集群** 时,可以通过会话对事务进行链接,以确保 **因果一致性**。这意味着对于任何两个事务,都保证第二个事务只有在第一个事务成功提交后才会开始。即使事务是在不同的物理集群成员上执行的,也是如此。有关因果集群的更多信息,请参阅操作手册 → 集群

在内部,因果链通过在事务之间传递书签来执行。每个书签记录特定数据库的事务历史中的一个或多个点,可用于通知集群成员以特定顺序执行工作单元。在收到书签后,服务器将在赶上相关事务时间点之前阻塞。

在开始新事务时,从客户端到服务器发送初始书签,并在成功完成时返回最终书签。请注意,这适用于读事务和写事务。

书签传播在会话中自动执行,不需要应用程序的任何显式信号或设置。为了避免不相关工作单元的这种小延迟开销,应用程序可以使用多个会话。这会避免因果链的小延迟开销。

可以通过从会话中提取最后一个书签并将其传递到另一个会话的构造函数中,在会话之间传递书签。如果事务具有多个逻辑前身,则还可以组合多个书签。请注意,只有在跨会话进行链接时,应用程序才需要直接处理书签。

driver passing bookmarks
图 2. 传递书签
示例 1. 传递书签
传递书签
// Create a company node
private IResult AddCompany(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Company {name: $name})", new {name});
}

// Create a person node
private IResult AddPerson(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Person {name: $name})", new {name});
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private IResult Employ(ITransaction tx, string personName, string companyName)
{
    return tx.Run(@"MATCH (person:Person {name: $personName}) 
             MATCH (company:Company {name: $companyName}) 
             CREATE (person)-[:WORKS_FOR]->(company)", new {personName, companyName});
}

// Create a friendship between two people.
private IResult MakeFriends(ITransaction tx, string name1, string name2)
{
    return tx.Run(@"MATCH (a:Person {name: $name1}) 
             MATCH (b:Person {name: $name2})
             MERGE (a)-[:KNOWS]->(b)", new {name1, name2});
}

// Match and display all friendships.
private int PrintFriendships(ITransaction tx)
{
    var result = tx.Run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name");

    var count = 0;
    foreach (var record in result)
    {
        count++;
        Console.WriteLine($"{record["a.name"]} knows {record["b.name"]}");
    }

    return count;
}

public void AddEmployAndMakeFriends()
{
    // To collect the session bookmarks
    var savedBookmarks = new List<Bookmark>();

    // Create the first person and employment relationship.
    using (var session1 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
    {
        session1.WriteTransaction(tx => AddCompany(tx, "Wayne Enterprises"));
        session1.WriteTransaction(tx => AddPerson(tx, "Alice"));
        session1.WriteTransaction(tx => Employ(tx, "Alice", "Wayne Enterprises"));

        savedBookmarks.Add(session1.LastBookmark);
    }

    // Create the second person and employment relationship.
    using (var session2 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
    {
        session2.WriteTransaction(tx => AddCompany(tx, "LexCorp"));
        session2.WriteTransaction(tx => AddPerson(tx, "Bob"));
        session2.WriteTransaction(tx => Employ(tx, "Bob", "LexCorp"));

        savedBookmarks.Add(session2.LastBookmark);
    }

    // Create a friendship between the two people created above.
    using (var session3 = Driver.Session(o =>
        o.WithDefaultAccessMode(AccessMode.Write).WithBookmarks(savedBookmarks.ToArray())))
    {
        session3.WriteTransaction(tx => MakeFriends(tx, "Alice", "Bob"));

        session3.ReadTransaction(PrintFriendships);
    }
}
func addCompanyTxFunc(name string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		return tx.Run("CREATE (a:Company {name: $name})", map[string]interface{}{"name": name})
	}
}

func addPersonTxFunc(name string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		return tx.Run("CREATE (a:Person {name: $name})", map[string]interface{}{"name": name})
	}
}

func employTxFunc(person string, company string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		return tx.Run(
			"MATCH (person:Person {name: $personName}) "+
				"MATCH (company:Company {name: $companyName}) "+
				"CREATE (person)-[:WORKS_FOR]->(company)", map[string]interface{}{"personName": person, "companyName": company})
	}
}

func makeFriendTxFunc(person1 string, person2 string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		return tx.Run(
			"MATCH (a:Person {name: $name1}) "+
				"MATCH (b:Person {name: $name2}) "+
				"MERGE (a)-[:KNOWS]->(b)", map[string]interface{}{"name1": person1, "name2": person2})
	}
}

func printFriendsTxFunc() neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name", nil)
		if err != nil {
			return nil, err
		}

		for result.Next() {
			fmt.Printf("%s knows %s\n", result.Record().Values[0], result.Record().Values[1])
		}

		return result.Consume()
	}
}

func addAndEmploy(driver neo4j.Driver, person string, company string) (string, error) {
	session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
	defer session.Close()

	if _, err := session.WriteTransaction(addCompanyTxFunc(company)); err != nil {
		return "", err
	}
	if _, err := session.WriteTransaction(addPersonTxFunc(person)); err != nil {
		return "", err
	}
	if _, err := session.WriteTransaction(employTxFunc(person, company)); err != nil {
		return "", err
	}

	return session.LastBookmark(), nil
}

func makeFriend(driver neo4j.Driver, person1 string, person2 string, bookmarks ...string) (string, error) {
	session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite, Bookmarks: bookmarks})
	defer session.Close()

	if _, err := session.WriteTransaction(makeFriendTxFunc(person1, person2)); err != nil {
		return "", err
	}

	return session.LastBookmark(), nil
}

func addEmployAndMakeFriends(driver neo4j.Driver) error {
	var bookmark1, bookmark2, bookmark3 string
	var err error

	if bookmark1, err = addAndEmploy(driver, "Alice", "Wayne Enterprises"); err != nil {
		return err
	}

	if bookmark2, err = addAndEmploy(driver, "Bob", "LexCorp"); err != nil {
		return err
	}

	if bookmark3, err = makeFriend(driver, "Bob", "Alice", bookmark1, bookmark2); err != nil {
		return err
	}

	session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead, Bookmarks: []string{bookmark1, bookmark2, bookmark3}})
	defer session.Close()

	if _, err = session.ReadTransaction(printFriendsTxFunc()); err != nil {
		return err
	}

	return nil
}
导入书签
import java.util.ArrayList;
import java.util.List;

import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.Bookmark;

import static org.neo4j.driver.Values.parameters;
import static org.neo4j.driver.SessionConfig.builder;
传递书签
// Create a company node
private Result addCompany(final Transaction tx, final String name )
{
    return tx.run( "CREATE (:Company {name: $name})", parameters( "name", name ) );
}

// Create a person node
private Result addPerson(final Transaction tx, final String name )
{
    return tx.run( "CREATE (:Person {name: $name})", parameters( "name", name ) );
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private Result employ(final Transaction tx, final String person, final String company )
{
    return tx.run( "MATCH (person:Person {name: $person_name}) " +
                    "MATCH (company:Company {name: $company_name}) " +
                    "CREATE (person)-[:WORKS_FOR]->(company)",
            parameters( "person_name", person, "company_name", company ) );
}

// Create a friendship between two people.
private Result makeFriends(final Transaction tx, final String person1, final String person2 )
{
    return tx.run( "MATCH (a:Person {name: $person_1}) " +
                    "MATCH (b:Person {name: $person_2}) " +
                    "MERGE (a)-[:KNOWS]->(b)",
            parameters( "person_1", person1, "person_2", person2 ) );
}

// Match and display all friendships.
private Result printFriends(final Transaction tx )
{
    Result result = tx.run( "MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name" );
    while ( result.hasNext() )
    {
        Record record = result.next();
        System.out.println( String.format( "%s knows %s", record.get( "a.name" ).asString(), record.get( "b.name" ).toString() ) );
    }
    return result;
}

public void addEmployAndMakeFriends()
{
    // To collect the session bookmarks
    List<Bookmark> savedBookmarks = new ArrayList<>();

    // Create the first person and employment relationship.
    try ( Session session1 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
    {
        session1.writeTransaction( tx -> addCompany( tx, "Wayne Enterprises" ) );
        session1.writeTransaction( tx -> addPerson( tx, "Alice" ) );
        session1.writeTransaction( tx -> employ( tx, "Alice", "Wayne Enterprises" ) );

        savedBookmarks.add( session1.lastBookmark() );
    }

    // Create the second person and employment relationship.
    try ( Session session2 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
    {
        session2.writeTransaction( tx -> addCompany( tx, "LexCorp" ) );
        session2.writeTransaction( tx -> addPerson( tx, "Bob" ) );
        session2.writeTransaction( tx -> employ( tx, "Bob", "LexCorp" ) );

        savedBookmarks.add( session2.lastBookmark() );
    }

    // Create a friendship between the two people created above.
    try ( Session session3 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( savedBookmarks ).build() ) )
    {
        session3.writeTransaction( tx -> makeFriends( tx, "Alice", "Bob" ) );

        session3.readTransaction( this::printFriends );
    }
}
传递书签
// Create a company node
function addCompany (tx, name) {
  return tx.run('CREATE (a:Company {name: $name})', { name: name })
}

// Create a person node
function addPerson (tx, name) {
  return tx.run('CREATE (a:Person {name: $name})', { name: name })
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
function addEmployee (tx, personName, companyName) {
  return tx.run(
    'MATCH (person:Person {name: $personName}) ' +
      'MATCH (company:Company {name: $companyName}) ' +
      'CREATE (person)-[:WORKS_FOR]->(company)',
    { personName: personName, companyName: companyName }
  )
}

// Create a friendship between two people.
function makeFriends (tx, name1, name2) {
  return tx.run(
    'MATCH (a:Person {name: $name1}) ' +
      'MATCH (b:Person {name: $name2}) ' +
      'MERGE (a)-[:KNOWS]->(b)',
    { name1: name1, name2: name2 }
  )
}

// To collect friend relationships
const friends = []

// Match and display all friendships.
function findFriendships (tx) {
  const result = tx.run('MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name')

  result.subscribe({
    onNext: record => {
      const name1 = record.get(0)
      const name2 = record.get(1)

      friends.push({ name1: name1, name2: name2 })
    }
  })
}

// To collect the session bookmarks
const savedBookmarks = []

// Create the first person and employment relationship.
const session1 = driver.session({ defaultAccessMode: neo4j.WRITE })
const first = session1
  .writeTransaction(tx => addCompany(tx, 'Wayne Enterprises'))
  .then(() => session1.writeTransaction(tx => addPerson(tx, 'Alice')))
  .then(() =>
    session1.writeTransaction(tx =>
      addEmployee(tx, 'Alice', 'Wayne Enterprises')
    )
  )
  .then(() => {
    savedBookmarks.push(session1.lastBookmark())
  })
  .then(() => session1.close())

// Create the second person and employment relationship.
const session2 = driver.session({ defaultAccessMode: neo4j.WRITE })
const second = session2
  .writeTransaction(tx => addCompany(tx, 'LexCorp'))
  .then(() => session2.writeTransaction(tx => addPerson(tx, 'Bob')))
  .then(() =>
    session2.writeTransaction(tx => addEmployee(tx, 'Bob', 'LexCorp'))
  )
  .then(() => {
    savedBookmarks.push(session2.lastBookmark())
  })
  .then(() => session2.close())

// Create a friendship between the two people created above.
const last = Promise.all([first, second]).then(() => {
  const session3 = driver.session({
    defaultAccessMode: neo4j.WRITE,
    bookmarks: savedBookmarks
  })

  return session3
    .writeTransaction(tx => makeFriends(tx, 'Alice', 'Bob'))
    .then(() =>
      session3.readTransaction(findFriendships).then(() => session3.close())
    )
})
from neo4j import GraphDatabase
class BookmarksExample:

    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth)

    def close(self):
        self.driver.close()

    # Create a person node.
    @classmethod
    def create_person(cls, tx, name):
        tx.run("CREATE (:Person {name: $name})", name=name)

    # Create an employment relationship to a pre-existing company node.
    # This relies on the person first having been created.
    @classmethod
    def employ(cls, tx, person_name, company_name):
        tx.run("MATCH (person:Person {name: $person_name}) "
               "MATCH (company:Company {name: $company_name}) "
               "CREATE (person)-[:WORKS_FOR]->(company)",
               person_name=person_name, company_name=company_name)

    # Create a friendship between two people.
    @classmethod
    def create_friendship(cls, tx, name_a, name_b):
        tx.run("MATCH (a:Person {name: $name_a}) "
               "MATCH (b:Person {name: $name_b}) "
               "MERGE (a)-[:KNOWS]->(b)",
               name_a=name_a, name_b=name_b)

    # Match and display all friendships.
    @classmethod
    def print_friendships(cls, tx):
        result = tx.run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
        for record in result:
            print("{} knows {}".format(record["a.name"], record["b.name"]))

    def main(self):
        saved_bookmarks = []  # To collect the session bookmarks

        # Create the first person and employment relationship.
        with self.driver.session() as session_a:
            session_a.write_transaction(self.create_person, "Alice")
            session_a.write_transaction(self.employ, "Alice", "Wayne Enterprises")
            saved_bookmarks.append(session_a.last_bookmark())

        # Create the second person and employment relationship.
        with self.driver.session() as session_b:
            session_b.write_transaction(self.create_person, "Bob")
            session_b.write_transaction(self.employ, "Bob", "LexCorp")
            saved_bookmarks.append(session_b.last_bookmark())

        # Create a friendship between the two people created above.
        with self.driver.session(bookmarks=saved_bookmarks) as session_c:
            session_c.write_transaction(self.create_friendship, "Alice", "Bob")
            session_c.read_transaction(self.print_friendships)

使用访问模式路由事务

事务可以以 **** 或 **** 模式执行;这被称为 访问模式。在因果集群中,每个事务都将根据模式路由到适当的服务器。在使用单个实例时,所有事务都将传递到该单个服务器。

通过识别读写操作来路由 Cypher 可以提高可用集群资源的利用率。由于读服务器通常比写服务器多,因此将读流量定向到读服务器而不是写服务器是有益的。这样做有助于使写服务器可用于写事务。

访问模式通常由用于调用事务函数的方法指定。 会话类提供了一种用于调用读操作的方法,另一种用于写操作。

作为自动提交非托管事务的回退,还可以在会话级别提供默认访问模式。这仅在无法以其他方式指定访问模式的情况下使用。如果在该会话中使用事务函数,则默认访问模式将被覆盖

驱动程序不解析Cypher,因此无法自动确定事务是否旨在执行读操作或写操作。因此,标记为读操作的写事务仍将发送到读服务器,但在执行时会失败。
示例 2. 读写事务
读写事务
public long AddPerson(string name)
{
    using (var session = Driver.Session())
    {
        session.WriteTransaction(tx => CreatePersonNode(tx, name));
        return session.ReadTransaction(tx => MatchPersonNode(tx, name));
    }
}

private static IResult CreatePersonNode(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Person {name: $name})", new {name});
}

private static long MatchPersonNode(ITransaction tx, string name)
{
    var result = tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", new {name});
    return result.Single()[0].As<long>();
}
func addPersonNodeTxFunc(name string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run("CREATE (a:Person {name: $name})", map[string]interface{}{"name": name})
		if err != nil {
			return nil, err
		}

		return result.Consume()
	}
}

func matchPersonNodeTxFunc(name string) neo4j.TransactionWork {
	return func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", map[string]interface{}{"name": name})
		if err != nil {
			return nil, err
		}

		if result.Next() {
			return result.Record().Values[0], nil
		}

		return nil, errors.New("one record was expected")
	}
}

func addPersonNode(driver neo4j.Driver, name string) (int64, error) {
	session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
	defer session.Close()

	if _, err := session.WriteTransaction(addPersonNodeTxFunc(name)); err != nil {
		return -1, err
	}

	var id interface{}
	var err error
	if id, err = session.ReadTransaction(matchPersonNodeTxFunc(name)); err != nil {
		return -1, err
	}

	return id.(int64), nil
}
导入读写事务
import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.TransactionWork;

import static org.neo4j.driver.Values.parameters;
读写事务
public long addPerson( final String name )
{
    try ( Session session = driver.session() )
    {
        session.writeTransaction( new TransactionWork<Void>()
        {
            @Override
            public Void execute( Transaction tx )
            {
                return createPersonNode( tx, name );
            }
        } );
        return session.readTransaction( new TransactionWork<Long>()
        {
            @Override
            public Long execute( Transaction tx )
            {
                return matchPersonNode( tx, name );
            }
        } );
    }
}

private static Void createPersonNode( Transaction tx, String name )
{
    tx.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) );
    return null;
}

private static long matchPersonNode( Transaction tx, String name )
{
    Result result = tx.run( "MATCH (a:Person {name: $name}) RETURN id(a)", parameters( "name", name ) );
    return result.single().get( 0 ).asLong();
}
读写事务
const session = driver.session()

try {
  await session.writeTransaction(tx =>
    tx.run('CREATE (a:Person {name: $name})', { name: personName })
  )

  const result = await session.readTransaction(tx =>
    tx.run('MATCH (a:Person {name: $name}) RETURN id(a)', {
      name: personName
    })
  )

  const singleRecord = result.records[0]
  const createdNodeId = singleRecord.get(0)

  console.log('Matched created node with id: ' + createdNodeId)
} finally {
  await session.close()
}
def create_person_node(tx, name):
    tx.run("CREATE (a:Person {name: $name})", name=name)

def match_person_node(tx, name):
    result = tx.run("MATCH (a:Person {name: $name}) RETURN count(a)", name=name)
    return result.single()[0]

def add_person(name):
    with driver.session() as session:
        session.write_transaction(create_person_node, name)
        persons = session.read_transaction(match_person_node, name)
        return persons

数据库和执行上下文

Neo4j 提供了在同一个 DBMS 中使用多个数据库的功能。

对于社区版,这仅限于一个用户数据库,以及system数据库。

从驱动程序 API 的角度来看,会话具有 DBMS 范围,并且可以在会话构建时选择会话的默认数据库。默认数据库用作对未在USE子句中明确指定数据库的查询的目标。有关USE子句的详细信息,请参见Cypher 手册 → USE

在多数据库环境中,服务器会将一个数据库标记为默认数据库。在没有命名特定数据库为默认数据库的情况下创建会话时,会选择此数据库。在单数据库环境中,该数据库始终是默认数据库。

有关在同一个 DBMS 中管理多个数据库的更多信息,请参考Cypher 手册 → Neo4j 数据库和图,其中包含Neo4j 数据存储层次结构的完整细分。

以下示例说明了如何使用数据库

var session = driver.session(SessionConfig.forDatabase( "foo" ))
// lists nodes from database foo
session.run("MATCH (n) RETURN n").list()

// lists nodes from database bar
session.run("USE bar MATCH (n) RETURN n").list()

// creates an index in foo
session.run("CREATE INDEX foo_idx FOR (n:Person) ON n.id").consume()

// creates an index in bar
session.run("USE bar CREATE INDEX bar_idx FOR (n:Person) ON n.id").consume()

// targets System database
session.run("SHOW USERS")

数据库选择

客户端的数据库选择发生在会话 API上。

在会话创建期间,将数据库的名称传递给驱动程序。如果未指定名称,则使用默认数据库。数据库名称不能为null,也不能为空字符串。

仅当驱动程序连接到 Neo4j 企业版时才可以选择数据库。在 Neo4j 社区版中更改为任何其他数据库(而不是默认数据库)会导致运行时错误。

以下示例说明了 DBMS Cypher 手册 → 事务的概念,并展示了如何在一次驱动程序事务中发出对多个数据库的查询。它带有注释,说明每个操作如何影响数据库事务。

var session = driver.session(SessionConfig.forDatabase( "foo" ))
// a DBMS-level transaction is started
var transaction = session.begin()

// a transaction on database "foo" is started automatically with the first query targeting foo
transaction.run("MATCH (n) RETURN n").list()

// a transaction on database "bar" is started
transaction.run("USE bar MATCH (n) RETURN n").list()

// executes in the transaction on database "foo"
transaction.run("RETURN 1").consume()

// executes in the transaction on database "bar"
transaction.run("USE bar RETURN 1").consume()

// commits the DBMS-level transaction which commits the transactions on databases "foo" and
// "bar"
transaction.commit()

请注意,所请求的数据库必须存在。

示例 3. 在会话创建时进行数据库选择
在会话创建时进行数据库选择
using (var session = _driver.Session(SessionConfigBuilder.ForDatabase("examples")))
{
    session.Run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").Consume();
}

void SessionConfig(SessionConfigBuilder configBuilder) =>
    configBuilder.WithDatabase("examples")
        .WithDefaultAccessMode(AccessMode.Read)
        .Build();

using (var session = _driver.Session(SessionConfig))
{
    var result = session.Run("MATCH (a:Greeting) RETURN a.message as msg");
    var msg = result.Single()[0].As<string>();
    Console.WriteLine(msg);
}
在会话创建时进行数据库选择
session := driver.NewSession(neo4j.SessionConfig{DatabaseName: "example"})
导入在会话创建时进行数据库选择
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
在会话创建时进行数据库选择
try ( Session session = driver.session( SessionConfig.forDatabase( "examples" ) ) )
{
    session.run( "CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a" ).consume();
}

SessionConfig sessionConfig = SessionConfig.builder()
        .withDatabase( "examples" )
        .withDefaultAccessMode( AccessMode.READ )
        .build();
try ( Session session = driver.session( sessionConfig ) )
{
    String msg = session.run( "MATCH (a:Greeting) RETURN a.message as msg" ).single().get( "msg" ).asString();
    System.out.println(msg);
}
在会话创建时进行数据库选择
const session = driver.session({ database: 'examples' })
try {
  const result = await session.writeTransaction(tx =>
    tx.run(
      'CREATE (a:Greeting {message: "Hello, Example-Database"}) RETURN a.message'
    )
  )

  const singleRecord = result.records[0]
  const greeting = singleRecord.get(0)

  console.log(greeting)
} finally {
  await session.close()
}

const readSession = driver.session({
  database: 'examples',
  defaultAccessMode: neo4j.READ
})
try {
  const result = await readSession.writeTransaction(tx =>
    tx.run('MATCH (a:Greeting) RETURN a.message')
  )

  const singleRecord = result.records[0]
  const greeting = singleRecord.get(0)

  console.log(greeting)
} finally {
  await readSession.close()
}
在会话创建时进行数据库选择
from neo4j import READ_ACCESS
with driver.session(database="example") as session:
    session.run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").consume()

with driver.session(database="example", default_access_mode=READ_ACCESS) as session:
    message = session.run("MATCH (a:Greeting) RETURN a.message as msg").single().get("msg")
    print(message)

类型映射

驱动程序在应用程序语言类型Cypher 类型之间进行转换。

为了传递参数和处理结果,了解 Cypher 如何处理类型以及如何映射 Cypher 类型到驱动程序中非常重要。

下表显示了可用数据类型。所有类型都可能在结果中找到,但并非所有类型都可以用作参数。

Cypher 类型 参数 结果

null*

列表

映射

布尔值

整数

浮点数

字符串

字节数组

日期

时间

本地时间

日期时间

本地日期时间

持续时间

节点**

关系**

路径**

* 空标记不是类型,而是值不存在的占位符。有关如何在 Cypher 中使用空的详细信息,请参考Cypher 手册 → 使用null

** 节点、关系和路径作为原始图实体的快照传递到结果中。虽然这些快照中包含原始实体 ID,但不会保留到基础服务器端实体的永久链接,这些实体可能会独立于客户端副本被删除或以其他方式更改。图结构不能用作参数,因为这取决于应用程序上下文,此类参数是按引用传递还是按值传递,而 Cypher 没有机制来表示这一点。等效的功能可以通过简单地传递 ID(按引用传递)或提取的属性映射(按值传递)来实现。**

Neo4j 驱动程序将 Cypher 类型映射到和从本机语言类型,如下表所示。自定义类型(在语言或标准库中不可用)以粗体突出显示。

示例 4. 将 Neo4j 类型映射到本机语言类型
Neo4j Cypher 类型 .NET 类型

null

null

列表

IList<object>

映射

IDictionary<string, object>

布尔值

bool

整数

long

浮点数

double

字符串

string

字节数组

byte[]

日期

LocalDate

时间

OffsetTime

本地时间

本地时间

日期时间*

ZonedDateTime

本地日期时间

本地日期时间

持续时间

持续时间

节点

INode

关系

IRelationship

路径

IPath

* 时区名称符合IANA 系统,而不是Windows 系统。入站转换使用扩展 Windows-Olson zid 映射进行,该映射由 Unicode CLDR 定义。

Neo4j 类型 Go 类型

null

nil

列表

[]interface{}

映射

map[string]interface{}

布尔值

bool

整数

int64

浮点数

float64

字符串

string

字节数组

[]byte

日期

neo4j.Date

时间

neo4j.OffsetTime

本地时间

neo4j.LocalTime

日期时间

time.Time*

本地日期时间

neo4j.LocalDateTime

持续时间

neo4j.Duration

neo4j.Point

节点

neo4j.Node

关系

neo4j.Relationship

路径

neo4j.Path

* 当time.Time值通过驱动程序发送/接收时,如果其Zone()返回Offset的名称,则该值将使用其偏移值而不是其时区名称存储。

Neo4j Cypher 类型 Java 类型

null

null

列表

List<Object>

映射

Map<String, Object>

布尔值

boolean

整数

long

浮点数

double

字符串

字符串

字节数组

byte[]

日期

LocalDate

时间

OffsetTime

本地时间

本地时间

日期时间

ZonedDateTime

本地日期时间

本地日期时间

持续时间

IsoDuration*

节点

节点

关系

关系

路径

路径

* 作为参数传递的DurationPeriod将始终隐式转换为IsoDuration

Neo4j Cypher 类型 JavaScript 类型

null

null

列表

数组

映射

对象

布尔值

布尔值

整数

整数*

浮点数

数字

字符串

字符串

字节数组

Int8Array

日期

日期

时间

时间

本地时间

本地时间

日期时间

日期时间

本地日期时间

本地日期时间

持续时间

持续时间

节点

节点

关系

关系

路径

路径

* JavaScript没有本机整数类型,因此提供了一种自定义类型。为了方便起见,可以通过配置禁用此功能,以便使用本机 Number 类型。请注意,这会导致精度损失。

Neo4j 类型 Python 3 类型

null

None

列表

list

映射

dict

布尔值

bool

整数

int

浮点数

float

字符串

str

字节数组

bytearray

日期

neo4j.time.Date

时间

neo4j.time.Time

本地时间

neo4j.time.Time††

日期时间

neo4j.time.DateTime

本地日期时间

neo4j.time.DateTime††

持续时间

neo4j.time.Duration*

neo4j.spatial.Point

节点

neo4j.graph.Node

关系

neo4j.graph.Relationship

路径

neo4j.graph.Path

* 作为参数传递的datetime.timedelta对象将始终隐式转换为neo4j.time.Duration

† 其中tzinfo不为 None

†† 其中tzinfo None

异常和错误处理

在使用驱动程序执行Cypher或执行其他操作时,可能会出现某些异常和错误情况。服务器生成的异常都与状态代码相关联,该代码描述了问题的性质,以及提供更多详细信息的消息。

下表列出了分类。

表 1. 服务器状态代码分类
分类 描述

客户端错误

客户端应用程序导致错误。应用程序应修改并重试操作。

数据库错误

服务器导致错误。重试操作通常会失败。

瞬时错误

发生了临时错误。应用程序应重试操作。

服务不可用

当驱动程序无法再与服务器建立通信时,即使在重试后也是如此,将发出服务不可用异常。

遇到此情况通常表明存在基本网络问题或数据库问题。

虽然驱动程序可以通过某些缓解措施来避免此问题,但总会有无法避免的情况。因此,强烈建议确保客户端应用程序包含在客户端无法再与服务器通信时可以遵循的代码路径。

瞬时错误

瞬时错误是由服务器生成的,并被标记为可以安全重试而无需修改原始请求。此类错误的示例包括死锁和内存问题。

使用事务函数时,驱动程序通常能够在发生瞬时故障时自动重试。