客户端应用程序

本节介绍如何在应用程序中管理数据库连接。

驱动程序对象

Neo4j 客户端应用程序需要一个驱动程序对象,从数据访问的角度来看,它构成了应用程序的主干。所有 Neo4j 交互都通过此对象进行,因此它应该可供应用程序中所有需要数据访问的部分使用。

在存在线程安全问题的语言中,驱动程序对象可以被认为是线程安全的

关于生命周期的说明

应用程序通常在启动时构造一个驱动程序对象,并在退出时销毁它。

销毁驱动程序对象将通过关闭关联的连接池,立即关闭之前通过该驱动程序对象打开的任何连接。

这将导致任何未完成的事务回滚,并关闭任何未消费的结果。

要构造驱动程序实例,必须提供连接 URI认证信息

如果需要,可以提供额外的配置详细信息。配置详细信息在驱动程序对象的生命周期内是不可变的。因此,如果需要多种配置(例如在与多个数据库用户协作时),则必须使用多个驱动程序对象。

示例 1. 驱动程序生命周期
        public class DriverLifecycleExample : IDisposable
        {
            public DriverLifecycleExample(string uri, string user, string password)
            {
                Driver = GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
            }

            public IDriver Driver { get; }

            public void Dispose()
            {
                Driver?.Dispose();
            }
        }
    }

    public class HelloWorldExampleTest : BaseExample
    {
        public HelloWorldExampleTest(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        [RequireServerFact]
        public void TestHelloWorldExample()
        {
            // Given
            using var example = new HelloWorldExample(Uri, User, Password);
            // When & Then
            example.PrintGreeting("Hello, world");
        }

        public class HelloWorldExample : IDisposable
        {
            private readonly IDriver _driver;

            public HelloWorldExample(string uri, string user, string password)
            {
                _driver = GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
            }

            public void Dispose()
            {
                _driver?.Dispose();
            }

            public void PrintGreeting(string message)
            {
                using var session = _driver.Session();
                var greeting = session.ExecuteWrite(
                    tx =>
                    {
                        var result = tx.Run(
                            "CREATE (a:Greeting) " +
                            "SET a.message = $message " +
                            "RETURN a.message + ', from node ' + id(a)",
                            new { message });

                        return result.Single()[0].As<string>();
                    });

                Console.WriteLine(greeting);
            }

            public static void Main()
            {
                using var greeter = new HelloWorldExample("bolt://:7687", "neo4j", "password");

                greeter.PrintGreeting("hello, world");
            }
        }
    }

    public class ReadWriteTransactionExample : BaseExample
    {
        public ReadWriteTransactionExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        [RequireServerFact]
        public void TestReadWriteTransactionExample()
        {
            // When & Then
            AddPerson("Alice").Should().BeGreaterOrEqualTo(0L);
        }

        public long AddPerson(string name)
        {
            using var session = Driver.Session();
            session.ExecuteWrite(tx => CreatePersonNode(tx, name));
            return session.ExecuteRead(tx => MatchPersonNode(tx, name));
        }

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

        private static long MatchPersonNode(IQueryRunner tx, string name)
        {
            var result = tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", new { name });
            return result.Single()[0].As<long>();
        }

    }

    public class ResultConsumeExample : BaseExample
    {
        public ResultConsumeExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public List<string> GetPeople()
        {
            using var session = Driver.Session();
            return session.ExecuteRead(
                tx =>
                {
                    var result = tx.Run("MATCH (a:Person) RETURN a.name ORDER BY a.name");
                    return result.Select(record => record[0].As<string>()).ToList();
                });
        }

        [RequireServerFact]
        public void TestResultConsumeExample()
        {
            // Given
            Write("CREATE (a:Person {name: 'Alice'})");
            Write("CREATE (a:Person {name: 'Bob'})");
            // When & Then
            GetPeople().Should().Contain(new[] { "Alice", "Bob" });
        }
    }

    public class ResultRetainExample : BaseExample
    {
        public ResultRetainExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public int AddEmployees(string companyName)
        {
            using var session = Driver.Session();
            var persons = session.ExecuteRead(tx => tx.Run("MATCH (a:Person) RETURN a.name AS name").ToList());
            return persons.Sum(
                person => session.ExecuteWrite(
                    tx =>
                    {
                        var result = tx.Run(
                            "MATCH (emp:Person {name: $person_name}) " +
                            "MERGE (com:Company {name: $company_name}) " +
                            "MERGE (emp)-[:WORKS_FOR]->(com)",
                            new { person_name = person["name"].As<string>(), company_name = companyName });

                        result.Consume();
                        return 1;
                    }));
        }

        [RequireServerFact]
        public void TestResultConsumeExample()
        {
            // Given
            Write("CREATE (a:Person {name: 'Alice'})");
            Write("CREATE (a:Person {name: 'Bob'})");
            // When & Then
            AddEmployees("Example").Should().Be(2);
            Read("MATCH (emp:Person)-[WORKS_FOR]->(com:Company) WHERE com.name = 'Example' RETURN count(emp)")
                .Single()[0]
                .As<int>()
                .Should()
                .Be(2);
        }
    }

    public class ServiceUnavailableExample : BaseExample
    {
        private readonly IDriver _baseDriver;

        public ServiceUnavailableExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
            _baseDriver = Driver;
            Driver = GraphDatabase.Driver(
                "bolt://:8080",
                AuthTokens.Basic(User, Password),
                o => o.WithMaxTransactionRetryTime(TimeSpan.FromSeconds(3)));
        }

        protected override void Dispose(bool isDisposing)
        {
            if (!isDisposing)
            {
                return;
            }

            Driver = _baseDriver;
            base.Dispose(true);
        }

        public bool AddItem()
        {
            try
            {
                using var session = Driver.Session();
                return session.ExecuteWrite(
                    tx =>
                    {
                        tx.Run("CREATE (a:Item)").Consume();
                        return true;
                    });
            }
            catch (ServiceUnavailableException)
            {
                return false;
            }
        }

        [RequireServerFact]
        public void TestServiceUnavailableExample()
        {
            AddItem().Should().BeFalse();
        }
    }

    [SuppressMessage("ReSharper", "xUnit1013")]
    public class SessionExample : BaseExample
    {
        public SessionExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public void AddPerson(string name)
        {
            using var session = Driver.Session();
            session.Run("CREATE (a:Person {name: $name})", new { name });
        }

        [RequireServerFact]
        public void TestSessionExample()
        {
            // Given & When
            AddPerson("Alice");
            // Then
            CountPerson("Alice").Should().Be(1);
        }
    }

    [SuppressMessage("ReSharper", "xUnit1013")]
    public class TransactionFunctionExample : BaseExample
    {
        public TransactionFunctionExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public void AddPerson(string name)
        {
            using var session = Driver.Session();
            session.ExecuteWrite(tx => tx.Run("CREATE (a:Person {name: $name})", new { name }).Consume());
        }

        [RequireServerFact]
        public void TestTransactionFunctionExample()
        {
            // Given & When
            AddPerson("Alice");
            // Then
            CountPerson("Alice").Should().Be(1);
        }
    }

    [SuppressMessage("ReSharper", "xUnit1013")]
    public class TransactionTimeoutConfigExample : BaseExample
    {
        public TransactionTimeoutConfigExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public void AddPerson(string name)
        {
            using var session = Driver.Session();
            session.ExecuteWrite(
                tx => tx.Run("CREATE (a:Person {name: $name})", new { name }).Consume(),
                txConfig => txConfig.WithTimeout(TimeSpan.FromSeconds(5)));
        }

        [RequireServerFact]
        public void TestTransactionTimeoutConfigExample()
        {
            // Given & When
            AddPerson("Alice");
            // Then
            CountPerson("Alice").Should().Be(1);
        }
    }

    [SuppressMessage("ReSharper", "xUnit1013")]
    public class TransactionMetadataConfigExample : BaseExample
    {
        public TransactionMetadataConfigExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        public void AddPerson(string name)
        {
            using var session = Driver.Session();
            var txMetadata = new Dictionary<string, object> { { "applicationId", "123" } };

            session.ExecuteWrite(
                tx => tx.Run("CREATE (a:Person {name: $name})", new { name }).Consume(),
                txConfig => txConfig.WithMetadata(txMetadata));
        }

        [RequireServerFact]
        public void TestTransactionMetadataConfigExample()
        {
            // Given & When
            AddPerson("Alice");
            // Then
            CountPerson("Alice").Should().Be(1);
        }
    }

    public class DatabaseSelectionExampleTest : BaseExample
    {
        public DatabaseSelectionExampleTest(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        [RequireEnterpriseEdition("4.0.0", "5.0.0", VersionComparison.Between)]
        public async void TestUseAnotherDatabaseExample()
        {
            try
            {
                await DropDatabase(Driver, "examples");
            }
            catch (FatalDiscoveryException)
            {
                // Its a new server instance, the database didn't exist yet
            }

            await CreateDatabase(Driver, "examples");

            // Given
            using var example = new DatabaseSelectionExample(Uri, User, Password);
            // When
            example.UseAnotherDatabaseExample();

            // Then
            var greetingCount = ReadInt("examples", "MATCH (a:Greeting) RETURN count(a)");
            greetingCount.Should().Be(1);
        }

        [RequireEnterpriseEdition("5.0.0", VersionComparison.GreaterThanOrEqualTo)]
        public async void TestUseAnotherDatabaseExampleAsync()
        {
            try
            {
                await DropDatabase(Driver, "examples", true);
            }
            catch (FatalDiscoveryException)
            {
                // Its a new server instance, the database didn't exist yet
            }

            await CreateDatabase(Driver, "examples", true);

            // Given
            using var example = new DatabaseSelectionExample(Uri, User, Password);
            // When
            example.UseAnotherDatabaseExample();

            // Then
            var greetingCount = ReadInt("examples", "MATCH (a:Greeting) RETURN count(a)");
            greetingCount.Should().Be(1);
        }

        private int ReadInt(string database, string query)
        {
            using var session = Driver.Session(SessionConfigBuilder.ForDatabase(database));
            return session.Run(query).Single()[0].As<int>();
        }

        private class DatabaseSelectionExample : IDisposable
        {
            private readonly IDriver _driver;
            private bool _disposed;

            public DatabaseSelectionExample(string uri, string user, string password)
            {
                _driver = GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            ~DatabaseSelectionExample()
            {
                Dispose(false);
            }

            public void UseAnotherDatabaseExample()
            {
                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);
                }
            }

            private void Dispose(bool disposing)
            {
                if (_disposed)
                {
                    return;
                }

                if (disposing)
                {
                    _driver?.Dispose();
                }

                _disposed = true;
            }
        }
    }

    [SuppressMessage("ReSharper", "xUnit1013")]
    public class PassBookmarksExample : BaseExample
    {
        public PassBookmarksExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
            : base(output, fixture)
        {
        }

        // Create a company node
        private IResultSummary AddCompany(IQueryRunner tx, string name)
        {
            return tx.Run("CREATE (a:Company {name: $name})", new { name }).Consume();
        }

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

        // Create an employment relationship to a pre-existing company node.
        // This relies on the person first having been created.
        private IResultSummary Employ(IQueryRunner 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 })
                .Consume();
        }

        // Create a friendship between two people.
        private IResultSummary MakeFriends(IQueryRunner 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 })
                .Consume();
        }

        // Match and display all friendships.
        private int PrintFriendships(IQueryRunner 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<Bookmarks>();

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

                savedBookmarks.Add(session1.LastBookmarks);
            }

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

                savedBookmarks.Add(session2.LastBookmarks);
            }

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

                session3.ExecuteRead(PrintFriendships);
            }
        }


        [RequireServerFact]
        public void TestPassBookmarksExample()
        {
            // Given & When
            AddEmployAndMakeFriends();

            // Then
            CountNodes("Person", "name", "Alice").Should().Be(1);
            CountNodes("Person", "name", "Bob").Should().Be(1);
            CountNodes("Company", "name", "Wayne Enterprises").Should().Be(1);
            CountNodes("Company", "name", "LexCorp").Should().Be(1);

            var works1 = Read(
                "MATCH (a:Person {name: $person})-[:WORKS_FOR]->(b:Company {name: $company}) RETURN count(a)",
                new { person = "Alice", company = "Wayne Enterprises" });

            works1.Count().Should().Be(1);

            var works2 = Read(
                "MATCH (a:Person {name: $person})-[:WORKS_FOR]->(b:Company {name: $company}) RETURN count(a)",
                new { person = "Bob", company = "LexCorp" });

            works2.Count().Should().Be(1);

            var friends = Read(
                "MATCH (a:Person {name: $person1})-[:KNOWS]->(b:Person {name: $person2}) RETURN count(a)",
                new { person1 = "Alice", person2 = "Bob" });

            friends.Count().Should().Be(1);
        }
    }
}

[Collection(SaIntegrationCollection.CollectionName)]
public abstract class BaseExample : IDisposable
{
    private bool _disposed;
    protected string Password = DefaultInstallation.Password;
    protected string Uri = DefaultInstallation.BoltUri;
    protected string User = DefaultInstallation.User;

    protected BaseExample(ITestOutputHelper output, StandAloneIntegrationTestFixture fixture)
    {
        Output = output;
        Driver = fixture.StandAloneSharedInstance.Driver;
    }

    protected ITestOutputHelper Output { get; }
    protected IDriver Driver { set; get; }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~BaseExample()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            using (var session = Driver.Session())
            {
                session.Run("MATCH (n) DETACH DELETE n").Consume();
            }
        }

        _disposed = true;
    }

    protected int CountNodes(string label, string property, string value)
    {
        using var session = Driver.Session();
        return session.ExecuteRead(
            tx => tx.Run(
                    $"MATCH (a:{label} {{{property}: $value}}) RETURN count(a)",
                    new { value })
                .Single()[0]
                .As<int>());
    }

    protected int CountPerson(string name)
    {
        return CountNodes("Person", "name", name);
    }

    protected void Write(string query, object parameters = null)
    {
        using var session = Driver.Session();
        session.ExecuteWrite(
            tx =>
                tx.Run(query, parameters).Consume());
    }

    protected List<IRecord> Read(string query, object parameters = null)
    {
        using var session = Driver.Session();
        return session.ExecuteRead(tx => tx.Run(query, parameters).ToList());
    }
}

连接 URI

连接 URI 标识图数据库以及如何连接到它。

加密信任设置提供了该连接应如何安全化的详细信息。

Neo4j 4.0 开始,客户端-服务器通信默认只使用未加密的本地连接。这与之前的版本不同,之前的版本默认开启加密,但会生成自签名证书。

当安装了完整证书并在驱动程序上启用加密时,会执行完整的证书检查(请参阅操作手册 → SSL 框架)。完整证书比自签名证书提供更好的整体安全性,因为它们包含一个完整的信任链,可追溯到根证书颁发机构。

Neo4j Aura 是一项由根证书颁发机构签名的完整证书支持的安全托管服务

要连接到 Neo4j Aura,驱动程序用户必须启用加密和完整的证书检查(后者默认启用)。

有关更多信息,请参阅下面的示例

初始地址解析

neo4j:// URI 中提供的地址仅用于初始和回退通信。

此通信发生在引导路由表时,所有后续通信都通过该路由表进行。当驱动程序无法联系路由表中保存的任何地址时,会发生回退。初始地址再次被重用来引导系统。

有多种选项可用于提供这种初始的逻辑到物理主机解析。这些选项包括常规 DNS自定义中间件(如负载均衡器)以及驱动程序对象解析器函数,所有这些都将在以下部分中进行描述。

DNS 解析

DNS 解析是默认且始终可用的选项。由于可以将 DNS 配置为将单个主机名解析为多个 IP 地址,因此这可用于在单个主机名下公开所有核心服务器 IP 地址。

dns resolution
图 1. 通过 DNS 进行的初始地址解析

自定义中间件

中间件(例如负载均衡器)可用于将核心服务器分组在一个公共地址下。

custom middleware
图 2. 使用自定义中间件进行初始地址解析

解析器函数

Neo4j 驱动程序还提供了一个名为解析器函数的地址解析拦截钩子。

这以回调函数的形式出现,接受一个输入地址并返回多个输出地址。该函数可以硬编码输出地址,也可以根据需要从其他配置源中获取它们。

resolver function
图 3. 使用解析器函数进行初始地址解析

以下示例展示了如何将单个地址扩展为多个(硬编码的)输出地址

示例 2. 自定义地址解析器
private IDriver CreateDriverWithCustomResolver(
    string virtualUri,
    IAuthToken token,
    params ServerAddress[] addresses)
{
    return GraphDatabase.Driver(
        virtualUri,
        token,
        o => o.WithResolver(new ListAddressResolver(addresses)).WithEncryptionLevel(EncryptionLevel.None));
}

public void AddPerson(string name)
{
    using var driver = CreateDriverWithCustomResolver(
        "neo4j://x.example.com",
        AuthTokens.Basic(Username, Password),
        ServerAddress.From("a.example.com", 7687),
        ServerAddress.From("b.example.com", 7877),
        ServerAddress.From("c.example.com", 9092));

    using var session = driver.Session();
    session.Run("CREATE (a:Person {name: $name})", new { name });
}

路由表

路由表充当驱动程序连接层和数据库表面之间的粘合剂。该表包含一个服务器地址列表,分组为读取器写入器,并由驱动程序根据需要自动刷新。

驱动程序不公开任何直接与路由表交互的 API,但有时在系统故障排除时探索它会很有用。

负载均衡策略

policy 参数可以包含在 neo4j:// URI 的查询字符串中,以自定义路由表并利用多数据中心路由设置

使用驱动程序进行负载均衡策略的前提是数据库在集群上运行,并且设置了一些服务器策略

示例

连接 URI 通常按照以下模式形成

neo4j://<HOST>:<PORT>[?policy=<POLICY-NAME>]

这针对一个路由的 Neo4j 服务,该服务可以由集群或单个实例提供。HOSTPORT 值包含一个逻辑主机名和端口号,指向 Neo4j 服务的入口点(例如 neo4j://graph.example.com:7687)。

在集群环境中,URI 地址将解析为一个或多个核心成员;对于独立安装,这将简单地指向该服务器地址。policy 参数允许自定义路由表,并在负载均衡策略中进行了更详细的讨论。

当需要单个点对点连接时,可以使用另一种 URI 形式,即使用 bolt URI 方案(例如 bolt://graph.example.com:7687)。此变体适用于需要了解单个服务器的客户端应用程序子集(例如管理工具),而不是那些需要高可用性数据库服务的应用程序。

bolt://<HOST>:<PORT>

neo4jbolt URI 方案都允许包含额外加密和信任信息的变体。+s 变体启用带完整证书检查的加密,而 +ssc 变体启用加密但没有证书检查。后一种变体专门设计用于自签名证书。

表 1. 可用 URI 方案
URI 方案 路由 描述

neo4j

不安全

neo4j+s

使用完整证书进行安全连接

neo4j+ssc

使用自签名证书进行安全连接

bolt

不安全

bolt+s

使用完整证书进行安全连接

bolt+ssc

使用自签名证书进行安全连接

Neo4j 3.x 在单实例模式下不提供路由表,因此如果目标是较旧的非集群服务器,您应该使用 bolt:// URI。

下表提供了针对不同部署配置的示例代码片段。每个片段都假定已预先定义了 auth 变量,其中包含该连接的认证详细信息。

连接到服务

下表展示了如何连接到带有路由的服务示例

表 2. Neo4j Aura 或 Neo4j >= 4.x,使用完整证书进行安全连接

产品

Neo4j Aura, Neo4j >= 4.x

安全性

使用完整证书进行安全连接

代码片段

GraphDatabase.Driver("neo4j+s://graph.example.com:7687", auth)

注释

这是 Neo4j Aura 的默认(也是唯一)选项。

表 3. Neo4j >= 4.x,不安全连接

产品

Neo4j >= 4.x

安全性

不安全

代码片段

GraphDatabase.Driver("neo4j://graph.example.com:7687", auth);

注释

这是 Neo4j 4.x 及更高版本的默认设置

表 4. Neo4j >= 4.x,使用自签名证书进行安全连接

产品

Neo4j >= 4.x

安全性

使用自签名证书进行安全连接

代码片段

GraphDatabase.Driver("neo4j+ssc://graph.example.com:7687", auth)
要连接到没有路由的服务,可以将 neo4j 替换为 bolt

认证

认证详细信息以认证令牌的形式提供,其中包含访问数据库所需的用户、密码或其他凭据。Neo4j 支持多种认证标准,但默认使用基本认证

基本认证

基本认证方案由存储在服务器中的密码文件支持,要求应用程序提供用户名和密码。为此,请使用基本认证助手

示例 3. 基本认证
public IDriver CreateDriverWithBasicAuth(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
}

基本认证方案也可以用于针对 LDAP 服务器进行认证。

Kerberos 认证

Kerberos 认证方案提供了一种简单的方法,可以使用 base64 编码的服务器认证票据创建 Kerberos 认证令牌。创建 Kerberos 认证令牌的最佳方法如下所示

示例 4. Kerberos 认证
public IDriver CreateDriverWithKerberosAuth(string uri, string ticket)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Kerberos(ticket),
        o => o.WithEncryptionLevel(EncryptionLevel.None));
}

只有在服务器安装了Kerberos 附加组件的情况下,服务器才能理解 Kerberos 认证令牌。

自定义认证

对于已构建自定义安全提供程序的高级部署,可以使用自定义认证助手。

示例 5. 自定义认证
public IDriver CreateDriverWithCustomizedAuth(
    string uri,
    string principal,
    string credentials,
    string realm,
    string scheme,
    Dictionary<string, object> parameters)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Custom(principal, credentials, realm, scheme, parameters),
        o => o.WithEncryptionLevel(EncryptionLevel.None));
}

配置

ConnectionAcquisitionTimeout

会话从连接池请求连接时将等待的最长时间。对于所有连接当前正在使用且已达到 MaxConnectionPoolSize 限制的连接池,会话将等待此持续时间以使连接可用。由于获取连接的过程可能涉及创建新连接,请确保此配置的值高于已配置的 ConnectionTimeout

设置一个较低的值将允许事务在池中所有连接都被其他事务占用时快速失败。设置一个较高的值将导致这些事务排队,增加最终获取连接的机会,但代价是接收失败反馈的时间更长。寻找最佳值可能需要进行一些实验,同时考虑到应用程序中预期的并行度以及 MaxConnectionPoolSize

默认值:60 秒

示例 6. 配置连接池
public IDriver CreateDriverWithCustomizedConnectionPool(string uri, string user, string password)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Basic(user, password),
        o => o.WithMaxConnectionLifetime(TimeSpan.FromMinutes(30))
            .WithMaxConnectionPoolSize(50)
            .WithConnectionAcquisitionTimeout(TimeSpan.FromMinutes(2)));
}
ConnectionTimeout

等待建立 TCP 连接的最长时间。只有当会话需要连接时才会创建连接,除非连接池中有可用连接。驱动程序维护一个开放连接池,当有可用连接时可以借给会话。如果没有可用连接,则会尝试使用此配置选项创建新连接(前提是未达到 MaxConnectionPoolSize 限制),此选项提供了等待连接建立的最长时间。

在高延迟和高连接超时发生率的环境中,建议配置较高的值。对于较低延迟的环境和更快地获得潜在网络问题的反馈,请配置较低的值。

默认值:30 秒

示例 7. 配置连接超时
public IDriver CreateDriverWithCustomizedConnectionTimeout(string uri, string user, string password)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Basic(user, password),
        o => o.WithConnectionTimeout(TimeSpan.FromSeconds(15)));
}
CustomResolver

指定路由驱动程序使用的自定义服务器地址解析器,以解析用于创建驱动程序的初始地址。有关更多详细信息,请参阅解析器函数

Encryption

指定是否在驱动程序和服务器之间使用加密连接。

默认值:False

示例 8. 未加密配置
public IDriver CreateDriverWithCustomizedSecurityStrategy(string uri, string user, string password)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Basic(user, password),
        o => o.WithEncryptionLevel(EncryptionLevel.None));
}
MaxConnectionLifetime

驱动程序在将连接从池中移除之前保持连接的最长时间。请注意,虽然驱动程序会遵守此值,但网络环境可能会在此生命周期内关闭连接。这超出了驱动程序的控制范围。对连接生命周期的检查发生在会话需要连接时。如果可用连接的生命周期超过此限制,则会关闭该连接并创建新连接,将其添加到池中并返回给请求会话。在用户无法完全控制网络环境并希望主动确保所有连接都已就绪的环境中,更改此配置值会很有用。

将此选项设置为低值将导致高连接流失率,并可能导致性能下降。建议选择一个小于周围系统基础设施(如操作系统、路由器、负载均衡器、代理和防火墙)所暴露的最大生命周期的值。负值表示不检查生命周期。

默认值:1 小时(3600 秒)

MaxConnectionPoolSize

每个主机(即集群节点)允许连接池管理的最大连接总数。换句话说,对于使用 bolt:// 方案的直接驱动程序,这设置了指向单个数据库服务器的最大连接数。对于使用 neo4j:// 方案连接到集群的驱动程序,这设置了每个集群成员的最大连接数。如果会话或事务在连接池已满时尝试获取连接,它必须等待池中出现可用连接,或者获取新连接的请求超时。连接获取超时通过 ConnectionAcquisitionTimeout 进行配置。

此配置选项允许您管理驱动程序使用的内存和 I/O 资源,调整此选项取决于这些因素,以及集群成员的数量。

默认值:100 个连接

MaxTransactionRetryTime

托管事务在失败前将重试的最长时间。在托管事务中执行的查询在发生瞬时错误时会受益于重试。在这种情况下,事务会多次重试,直到达到 MaxTransactionRetryTime

在高延迟环境或执行许多大型事务(这可能会限制其重试次数并因此限制其成功机会)的情况下,将此选项配置得更高。在低延迟环境以及您的工作负载主要由许多小型事务组成的情况下,配置得更低。更快地使事务失败可能有助于突出瞬时错误背后的原因,从而更容易修复底层问题。

默认值:30 秒

示例 9. 配置最大重试时间
public IDriver CreateDriverWithCustomizedMaxRetryTime(string uri, string user, string password)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Basic(user, password),
        o => o.WithMaxTransactionRetryTime(TimeSpan.FromSeconds(15)));
}
TrustedCertificates

指定如何确定您正在连接的 Neo4j 实例提供的加密证书的真实性。如果禁用加密,此选项无效。

可能的值有

  • TrustManager.CreateChainTrust() - [默认] 接受任何可根据系统存储进行验证的证书。

  • TrustManager.CreateCertTrust(new []{"/path/ca1.crt", "/path/ca2.crt"}) - 接受指定路径的证书。

  • TrustManager.CreateInsecure() - 接受任何证书,包括自签名证书。不建议用于生产环境。

示例 10. 配置受信任证书
public IDriver CreateDriverWithCustomizedTrustStrategy(string uri, string user, string password)
{
    return GraphDatabase.Driver(
        uri,
        AuthTokens.Basic(user, password),
        o => o.WithTrustManager(TrustManager.CreateInsecure()));
}
KeepAlive

指定是否应启用 TCP 保持活动。为了确保驱动程序和服务器之间的连接仍然正常运行,TCP 层可以定期发送消息来检查连接。

默认值:True

日志记录

所有官方 Neo4j 驱动程序都会将信息记录到标准日志通道。这通常可以通过特定于生态系统的方式访问。

以下代码片段演示了如何将日志消息重定向到标准输出

#Please note that you will have to provide your own console logger implementing the ILogger interface.
IDriver driver = GraphDatabase.Driver(..., o => o.WithLogger(logger));
© . All rights reserved.