客户端应用程序

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

驱动程序对象

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 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 void Dispose()
            {
                _driver?.Dispose();
            }

            public static void Main()
            {
                using var greeter = new HelloWorldExample("bolt://localhost: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://localhost: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 来直接使用路由表,但有时在对系统进行故障排除时探索路由表可能很有用。

负载均衡策略

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

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

示例

连接 URI 通常根据以下模式形成

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

这针对由集群或单个实例满足的路由 Neo4j 服务。HOSTPORT 值包含针对 Neo4j 服务入口点的逻辑主机名和端口号(例如 neo4j://graph.example.com:7687)。

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

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

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

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

将此选项设置为较低的值会导致连接 churn 率很高,并可能导致性能下降。建议选择的值小于周围系统基础设施(如操作系统、路由器、负载均衡器、代理和防火墙)公开的最大生命周期。负值会导致生命周期不被检查。

默认: 1 小时(3600 秒)

MaxConnectionPoolSize

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

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

默认: 500 个连接

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 keep-alive。为了确保驱动程序和服务器之间的连接仍然有效,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));