c# neo4jClient 在事务中添加两个具有新关系的新节点

c# neo4jClient adding two new nodes with a new relationship inside a transaction

我已经使用 neo4JClient 玩了几天,并且有一个工作场所,其中包含我想要建模的一小部分数据实体。你在这里看到的是一个孤立的例子,试图找出问题所在。

所提供的代码应该复制和粘贴,并且 运行 提供正确的参考资料;

该代码说明核心方法在事务外正常工作,但在第一次尝试在同一事务内的两个新节点之间创建新关系时失败。

我不知道我是否落入新手陷阱 a) 一般的 neo4j,neo4jClient 具体来说,或者交易处理存在真正的问题。我假设我错了,我的想法是缺乏的,但我无法在任何地方找到另一个相关的问题来给我一个线索。

简而言之,我的用例是这样的;

As a new user I want to register as an owner with my current identity and be able to add my list of related assets to my portfolio.

我知道这可能不是执行此建议的正确或最有效的方法,我们将不胜感激。

以下代码应说明有效的用例和失败的用例;

我得到的异常是;

System.InvalidOperationException was unhandled HResult=-2146233079
Message=Cannot be done inside a transaction scope.
Source=Neo4jClient StackTrace: at Neo4jClient.GraphClient.CheckTransactionEnvironmentWithPolicy(IExecutionPolicy policy) in D:\temp4a765\Neo4jClient\GraphClient.cs:line 797 at Neo4jClient.GraphClient.CreateRelationship[TSourceNode,TRelationship](NodeReference`1 sourceNodeReference, TRelationship relationship) in D:\temp4a765\Neo4jClient\GraphClient.cs:line 350 at ConsoleApplication1.Example.CreateOwnerNode(IGraphClient client, Owner owner, Identity identity) in . . . InnerException:

using System;
using System.Linq;
using System.Transactions;
using Neo4jClient;

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var example = new Example();
        }
    }

    public class Example
    {
        public Example()
        {
            var rootUri = new Uri("http://localhost:7474/db/data/");
            var username = "neo4j";
            var neo4jneo4j = "neo4j";
            IGraphClient client = new GraphClient(rootUri, username, neo4jneo4j);
            client.Connect();

            Node<Owner> ownerNode;
            Node<Identity> identityNode;

            // whole thing outside tranaction
            ownerNode = CreateOwnerNode(client, new Owner(), new Identity());


            // individually outside transaction
            ownerNode = CreateOwner(client, new Owner());
            identityNode = CreateIdentity(client, new Identity());
            GiveOwnerAnIdentity(client, ownerNode, identityNode);


            // individually inside a transaction
            using (var scope = new TransactionScope())
            {
                ownerNode = CreateOwner(client, new Owner());
                identityNode = CreateIdentity(client, new Identity());
                GiveOwnerAnIdentity(client, ownerNode, identityNode);
                scope.Complete();
            }

            // whole thing inside a transaction
            using (var scope = new TransactionScope())
            {
                ownerNode = CreateOwnerNode(client, new Owner(), new Identity());
                scope.Complete();
            }

            //TODO: Something else with ownerNode
        }

        public void GiveOwnerAnIdentity(IGraphClient client, Node<Owner> ownerNode, Node<Identity> identityNode)
        {
            client.CreateRelationship(ownerNode.Reference, new Has(identityNode.Reference));
        }

        public Node<Identity> CreateIdentity(IGraphClient client, Identity identity)
        {
            var identityKey = KeyFor<Identity>();

            return client.Cypher.Create(identityKey)
                .WithParams(new { identity })
                .Return(o => o.Node<Identity>())
                .Results
                .Single();
        }

        public Node<Owner> CreateOwner(IGraphClient client, Owner owner)
        {  var ownerKey = KeyFor<Owner>();
           return client.Cypher.Create(ownerKey)
                   .WithParams(new { owner })
                   .Return(o => o.Node<Owner>())
                   .Results.Single();

        }


        /// <summary>
        ///     Create a node for an owner along with its nominated identity, relate the owner as having an identity
        /// </summary>
        /// <param name="client">The <see cref="Neo4jClient" /> instance</param>
        /// <param name="owner">The <see cref="Identity" /> instance</param>
        /// <param name="identity">The <see cref="Identity" /> instance</param>
        /// <returns>The created <see cref="Owner" /> node instance for additional relationships</returns>
        public Node<Owner> CreateOwnerNode(IGraphClient client, Owner owner, Identity identity)
        {
            var ownerKey = KeyFor<Owner>();
            var identityKey = KeyFor<Identity>();

            var ownerNode =
                client.Cypher.Create(ownerKey)
                    .WithParams(new {owner})
                    .Return(o => o.Node<Owner>())
                    .Results.Single();

            var identityNode = client.Cypher.Create(identityKey)
                .WithParams(new {identity})
                .Return(o => o.Node<Identity>())
                .Results
                .Single();

            client.CreateRelationship(ownerNode.Reference, new Has(identityNode.Reference));
            return ownerNode;
        }

        /// <summary>
        ///     Conform a Cypher create text for a type
        /// </summary>
        /// <typeparam name="TObject">The type to handle</typeparam>
        /// <returns>A string like "{o:TObject {tobject})</returns>
        public string KeyFor<TObject>()
        {
            var name = typeof(TObject).Name;
            return $"(o:{name} {{{name.ToLower()}}})";
        }

        public abstract class Nodebase
        {
            public Guid Id { get; set; }

            public Nodebase()
            {
                Id = Guid.NewGuid(); // make sure each node is always uniquely identifiable
            }
        }
        /// <summary>
        ///     Owner node , properties to be added later
        /// </summary>
        public class Owner
        {
        }

        /// <summary>
        ///     Identity  node , properties to be added later
        /// </summary>
        public class Identity
        {
        }

        /// <summary>
        ///     The <see cref="Owner" /> Has an <see cref="Identity" />
        /// </summary>
        public class Has : Relationship,
            IRelationshipAllowingSourceNode<Owner>,
            IRelationshipAllowingTargetNode<Identity>
        {
            internal Has(NodeReference<Identity> targetNode)
                : base(targetNode)
            {
            }

            public override string RelationshipTypeKey => GetType().Name.ToUpper();
        }
    }
}

更新:

好的,还有更多信息,但还不是解决方案。

一如既往,如果我说错了树,请权衡一下。

关于为什么交易如此难以处理(到目前为止)的更多线索。

我根据 Chris Skardons 回复更正了我的代码,这是正确的,它解决了创建问题,但没有解决在事务中创建所需对象并取回节点引用的原则问题。然而,它确实创建了节点和关系。我认为 neo4jClient 某处存在错误。

请求实际上成功了,但客户端的处理失败了,可能是因为我正在请求一个节点引用以便稍后在我的代码中使用。

这个有希望的最终问题现在集中在 GraphClient 处理事务中的以下方法。

看起来,当您围绕 Cypher 查询包装交易时,它的所有步骤都在标准 Cypher API 方法处理之外,并通过交易 API.

处理所有内容

这导致了截然不同的反应,我认为这就是问题所在。

我下载了整个 Neo4jClient 代码库并将其直接连接到我的示例代码解决方案而不是 NuGet 包中,因此我可以在必要时逐步执行所有代码到 HttpClient。

我还连接了 fiddler 来查看 REST 消息。更多内容见下文。

这两个 fiddler 会话更详细地显示了正在发生的事情; (请原谅冗长的帖子,因为它的相关数据可以了解正在发生的事情。)

交易之外

POST http : //localhost:7474/db/data/cypher HTTP/1.1
Accept : application / json;
stream = true
    X - Stream : true
    User - Agent : Neo4jClient / 0.0.0.0
    Authorization : Basic bmVvNGo6bmVvNGpuZW80ag ==
    Content - Type : application / json;
charset = utf - 8
    Host : localhost : 7474
    Content - Length : 174
    Expect : 100 - continue
    {

    "query" : "CREATE (o:Owner {owner})\r\nCREATE (i:Identity {identity})\r\nCREATE (o)-[:HAS]->(i)\r\nRETURN o",


    "params" : {
        "owner" : {},
        "identity" : {}
    }
}
HTTP / 1.1 200 OK
Date : Wed, 18 May 2016 12 : 03 : 57 GMT

Content - Type : application / json;
charset = UTF - 8;
stream = true
    Access - Control - Allow - Origin :  *
    Content - Length : 1180
    Server : Jetty(9.2.9.v20150224)
    {
    "columns" : ["o"],
    "data" : [[{
                "extensions" : {},
                "metadata" : {
                    "id" : 53044,
                    "labels" : ["Owner"]
                },
                "paged_traverse" : "http://localhost:7474/db/data/node/53044/paged/traverse/{returnType}{?pageSize,leaseTime}",


                "outgoing_relationships" : "http://localhost:7474/db/data/node/53044/relationships/out",
                "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/out/{-list|&|types}",
                "create_relationship" : "http://localhost:7474/db/data/node/53044/relationships",

                "labels" : "http://localhost:7474/db/data/node/53044/labels",
                "traverse" : "http://localhost:7474/db/data/node/53044/traverse/{returnType}",
                "all_relationships" : "http://localhost:7474/db/data/node/53044/relationships/all",
                "all_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/all/{-list|&|types}",
                "property" : "http://localhost:7474/db/data/node/53044/properties/{key}",
                "self" : "http://localhost:7474/db/data/node/53044",
                "incoming_relationships" : "http://localhost:7474/db/data/node/53044/relationships/in",
                "properties" : "http://localhost:7474/db/data/node/53044/properties",
                "incoming_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/in/{-list|&|types}",
                "data" : {}
            }
        ]]
}

交易内

POST http : //localhost:7474/db/data/transaction HTTP/1.1
Accept : application / json;
stream = true
    X - Stream : true
    User - Agent : Neo4jClient / 0.0.0.0
    Authorization : Basic bmVvNGo6bmVvNGpuZW80ag ==
    Content - Type : application / json;
charset = utf - 8
    Host : localhost : 7474
    Content - Length : 273
    Expect : 100 - continue
    {
    "statements" : [{
            "statement" : "CREATE (o:Owner {owner})\r\nCREATE (i:Identity {identity})\r\nCREATE (o)-[:HAS]->(i)\r\nRETURN o",
            "resultDataContents" : [],
            "parameters" : {

                "owner" : {},
                "identity" : {}
            }
        }
    ]

}
HTTP / 1.1 201 Created
Date : Wed, 18 May 2016 12 : 04 : 00 GMT
Location : http : //localhost:7474/db/data/transaction/585
Content - Type : application / json
Access - Control - Allow - Origin :  *
Content - Length : 241
Server : Jetty(9.2.9.v20150224)
{
    "commit" : "http://localhost:7474/db/data/transaction/585/commit",
    "results" : [{
            "columns" : ["o"],
            "data" : [{
                    "row" : [{}
                    ],
                    "meta" : [{
                            "id" : 53046,
                            "type" : "node",
                            "deleted" : false
                        }
                    ]
                }
            ]
        }
    ],
    "transaction" : {
        "expires" : "Wed, 18 May 2016 12:05:00 +0000"
    },
    "errors" : []
}

我发现这个方法有问题;

public class CypherJsonDeserializer<TResult>

IEnumerable<TResult> ParseInSingleColumnMode(DeserializationContext context, JToken root, string[] columnNames, TypeMapping[] jsonTypeMappings)

接近该方法末尾的行;

 var parsed = CommonDeserializerMethods.CreateAndMap(context, newType, elementToParse, jsonTypeMappings, 0);

returns 不在事务中时正确形成的 'parsed' 变量(即它调用 Cypher API URL),但不填充属性该变量在交易中并且正在使用交易 API.

我的问题是,考虑到事务 returns 非常不同的数据是否应该调用此方法?

在那之后,一切都在你的脸上爆炸了。我目前对代码意图的了解还不够多,无法多说。作为 neo4jClient 用户,我还不到一周的时间。

将 neo4jClient 与 fiddler 和本地主机一起使用

在我的调查中,我还发现了与连接 Fiddler 以查看发生了什么有关的其他问题。

我使用 Fiddler 规则技巧将我的本地 URL 命名为 'localneoj4' 而不是 localhost:7474,并在 client.Connect 方法中使用该新名称,这样我可以看到当地的路况。

var rootUri = "http://localneo4j/db/data";
    IGraphClient client = new GraphClient(rootUri, username, password);
    client.Connect();

按照建议 here 并将其添加到我的规则中;

 if (oSession.HostnameIs("localneo4j")) {
     oSession.host = "localhost:7474"; }

这导致内部出现错误

public class NeoServerConfiguration          
internal static async Task<NeoServerConfiguration> GetConfigurationAsync(Uri rootUri, string username, string password, ExecutionConfiguration executionConfiguration)  

这可能影响了许多走这条路的开发人员。

因为 fiddlers 代理效果是在 neo4jClient 的地址概念和服务器的地址概念之间建立一个断开连接。

处理从以下行的连接响应返回的 URI 时出现字符串长度不匹配问题,因为所有 result.* 属性都以“http://localhost:7474/db/data/' but rootUriWithoutUserInfo starts with http://localneo4j/db/data/

开头
    var baseUriLengthToTrim = rootUriWithoutUserInfo.AbsoluteUri.Length - 1;

    result.Batch = result.Batch.Substring(baseUriLengthToTrim);
    result.Node = result.Node.Substring(baseUriLengthToTrim);
    result.NodeIndex = result.NodeIndex.Substring(baseUriLengthToTrim);
    result.Relationship = "/relationship"; //Doesn't come in on the Service Root
    result.RelationshipIndex = result.RelationshipIndex.Substring(baseUriLengthToTrim);
    result.ExtensionsInfo = result.ExtensionsInfo.Substring(baseUriLengthToTrim);

解决此问题的一个快速解决方法是,您在 fiddler 规则中使用的应用程序名称与其长度匹配 'localhost:7474'

更好的解决方法可能是(我已经针对 < == 和 > 相对地址长度对其进行了测试)作为完全消除协议服务器地址和端口的一种方式,但这取决于代码所有者我猜;

    private static string CombineTrailingSegments(string result, int uriSegementsToSkip)
    {
        return new Uri(result).Segments.Skip(uriSegementsToSkip).Aggregate(@"/", (current, item) =>{ return current += item;});
    }

then

    var uriSegementsToSkip = rootUriWithoutUserInfo.Segments.Length;    // which counts the db/data and adjusts for server configuration
    result.Batch = CombineTrailingSegments(result.Batch, uriSegementsToSkip);
    ...

你得到异常是因为你正在使用旧的 API 调用,TransactionScope 实现是针对 Cypher 调用,基本上是:client.Cypher.

一般来说,Neo4jClient 已经远离 Node<T>(在某些特定情况下有用)和 Relationship 类型。我认为您的 CreateOwnerNode 应该更像这样:

public Node<Owner> CreateOwnerNode(IGraphClient client, Owner owner, Identity identity)
{
    var query = client.Cypher
        .Create($"(o:{GetLabel<Owner>()} {{owner}})")
        .Create($"(i:{GetLabel<Identity>()} {{identity}})")
        .WithParams(new {owner, identity})
        .Create("(o)-[:HAS]->(i)")
        .Return(o => o.As<Node<Owner>>());

    return query.Results.Single();
}

private string GetLabel<TObject>()
{
    return typeof(TObject).Name;
}

您想尝试在单个查询中完成尽可能多的工作,如果您正在尝试 - 您将对数据库进行 3 次调用,这将一次完成。

我认为也许值得先使用普通的 Cypher 来习惯它,不应该有任何理由需要使用 CreateRelationship 东西 - 事实上,我' d 说任何时候你离开 .Cypher 位 - 仔细检查以确保它 真的 你想做什么。