Search code examples
neo4jclient

how to query connected graph using neo4jclient like Master detail SQL in EF


I am new to the Neo4jClient as well as Neo4J so was not sure how to query for the data and get a master detail like data in the neo4j. Let me explain this with an example:

lets suppose I have a graph as below:

root -[:DEFINES] -> Shipment 1 
                -[:HAS_CONSIGNMENT]->Consignment 1
                            -[:HAS_ITEM]->Load Item 11
                            -[:HAS_ITEM]->Load Item 12
                            -[:HAS_CONSIGNEE]->Consignee 1
                -[:HAS_CONSIGNMENT]->Consignment 2
                            -[:HAS_ITEM]->Load Item 21
                            -[:HAS_ITEM]->Load Item 22
                            -[:HAS_CONSIGNEE]->Consignee 2

now suppose I want to get all the graph t o populate my Domain Model like below

public class Shipment
{
    public List<Consignment> Consignments {get; set;}
}

 public class Consignment
 {
     public List<LoadItem> LoadItems {get; set;}
     public Consignee ShippedTo {get; set;}
 }

 public class LoadItem
 {
 }

i know that I can probably build a Cypher query like below How to retrieve connected graph using neo4jclient

query = client.Cypher.Start(new { root = client.RootNode }).
            Match("root-[:DEFINES]->load-[:HAS_CONSIGNMENT]->consignments -[:HAS_ITEM]->loadItem").Match("consignments-[:HAS_CONSIGNEE]->consignee").
            Where((Load load) => load.Id == myId).
            Return(
            (load,consignments, loaditems)=>
            new {
                loadInfo = load.As<Node<Load>>(),
                consignments = consignments.CollectAs<Consignment>(),
                loadItems = loaditems.CollectAs<LoadItem>()
            }); 

but I am not sure how this can be converted to represent the second level of list that gives me that Consignment 2 has Load Item 21 & 22 where as Consignment 1 has Item 11 & 12.

can some one please help me understand how this works as I primarily have been working in the EF and the graph query is really new to me.

Regards Kiran


Solution

  • Right, this is the way I've got this working (I'm pretty sure Tatham will come along with a better answer - so hold out for a bit)

    public static ICollection<Shipment> Get()
    {
        var query = GraphClient.Cypher
            .Start(new {root = GraphClient.RootNode})
            .Match(
                string.Format("root-[:{0}]->shipment-[:{1}]-consignments-[:{2}]->loadItem", Defines.TypeKey, HasConsignment.TypeKey, HasItem.TypeKey),
                string.Format("consignments-[:{0}]->consignee", HasConsignee.TypeKey)
            )
            .Return((shipment, consignments, loadItem, consignee) =>
            new
            {
                Shipment = shipment.As<Node<Shipment>>(),
                Consignment = consignments.As<Consignment>(),
                LoadItem = loadItem.CollectAs<LoadItem>(),
                Consignee = consignee.As<Consignee>(),
            });
    
        var results = query.Results.ToList();
        var output = new List<Node<Shipment>>();
    
        foreach (var result in results)
        {
            var shipmentOut = output.SingleOrDefault(s => s.Reference == result.Shipment.Reference);
            if (shipmentOut == null)
            {
                shipmentOut = result.Shipment;
                shipmentOut.Data.Consignments = new List<Consignment>();
                output.Add(shipmentOut);
            }
    
            result.Consignment.LoadItems = new List<LoadItem>();
            result.Consignment.LoadItems.AddRange(result.LoadItem.Select(l => l.Data));
            shipmentOut.Data.Consignments.Add(result.Consignment);
        }
    
        return output.Select(s => s.Data).ToList();
    }
    

    This will get you all the Shipments and Consignments etc. However I note you do: .Where((Load load) => load.Id == myId) which implies you know the shipment id.

    As a consequence, we can simplify the code a little bit - as we don't need to use 'root', and we can pass in the Shipment ID.

    public static Shipment Get2(NodeReference<Shipment> shipmentNodeReference)
    {
        var query = GraphClient.Cypher
            .Start(new {shipment = shipmentNodeReference})
            .Match(
                string.Format("shipment-[:{0}]-consignments-[:{1}]->loadItem", HasConsignment.TypeKey, HasItem.TypeKey), 
                string.Format("consignments-[:{0}]->consignee", HasConsignee.TypeKey)
            )
            .Return((shipment, consignments, loadItem, consignee) =>
            new {
                Shipment = shipment.As<Node<Shipment>>(),
                Consignment = consignments.As<Consignment>(),
                LoadItem = loadItem.CollectAs<LoadItem>(),
                Consignee = consignee.As<Consignee>(),
            });
    
        var results = query.Results.ToList();
    
        //Assuming there is only one Shipment returned for a given ID, we can just take the first Shipment.
        Shipment shipmentOut = results.First().Shipment.Data;
        shipmentOut.Consignments = new List<Consignment>();
        foreach (var result in results)
        {
            result.Consignment.ShippedTo = result.Consignee;
            result.Consignment.LoadItems = new List<LoadItem>();
            result.Consignment.LoadItems.AddRange(result.LoadItem.Select(l => l.Data));
            shipmentOut.Consignments.Add(result.Consignment);
        }
    
        return shipmentOut;
    }
    

    For your info, the DataElements I was using were:

    public class Shipment
    {
        public string Id { get; set; }
        public List<Consignment> Consignments { get; set; }
        public override string ToString() { return Id; }
    }
    
    public class Consignment
    {
        public string Id { get; set; }
        public List<LoadItem> LoadItems { get; set; }
        public Consignee ShippedTo { get; set; }
        public override string ToString() { return Id; }
    }
    
    public class Consignee
    {
        public string Name { get; set; }
        public override string ToString() { return Name; }
    }
    
    public class LoadItem
    {
        public string Item { get; set; }
        public override string ToString() { return Item; }
    }
    

    With relationships all defined like:

    public class HasConsignment : Relationship, IRelationshipAllowingSourceNode<Shipment>, IRelationshipAllowingTargetNode<Consignment>
    {
        public const string TypeKey = "HAS_CONSIGNMENT";
    
        public HasConsignment() : base(-1) {}
        public HasConsignment(NodeReference targetNode): base(targetNode) {}
        public HasConsignment(NodeReference targetNode, object data) : base(targetNode, data) {}
    
        public override string RelationshipTypeKey { get { return TypeKey; } }
    }
    

    (with obvious changes where needed)