I have some data modeled as a simple set of nested c# objects, that I am trying to create/retrieve from a neo4j database using the .Net Neo4jClient.
My classes are in the form:
public class Foo {
public int ID { get; set; }
public List<Bar> bar { get; set;}
}
public class Bar {
public int ID { get; set; }
public List<Baz> baz { get; set;}
}
public class Baz {
public int ID { get; set; }
}
Once the data has been stored in the correct form in the database:
(f:Foo)-[h:HASBAR]->(b:Bar)-[hb:HASBAZ]->(bz:Baz)
I am able to retrieve the data into my class structure using collect and optional match using the following query:
List<Foo> foolist = WebApiConfig.GraphClient.Cypher
.Match("(f:Foo)-[h:HASBAR]->(b:Bar)")
.OptionalMatch("(b)-[hb:HASBAZ]->(bz:Baz)")
.With("f, { ID: b.ID, baz: collect(bz) } as Bar")
.With("{ ID:f.ID, bar:collect(Bar) } as Foo")
.Return<Foo>("Foo")
.Results
.ToList();
This all works perfectly and the data is correctly serialized into the proper classes.
My question is how should I perform the reverse?
As in given a single Foo class, containing multiple bar and baz classes nested, can I create the above data structure in the database in a single query?
Or do I have to write a query per each level of nesting?
I know I will probably have to list properties when creating, as if I give the client a Foo class it will create a node with "bar" as a property.
My problem mostly comes from the third level of nesting, if I treat the second level (bar) as an array (passing in Foo.bar) as a variable, I can create multiple [:HASBAR] relationships. But within the same query I have not found a way to relate the correct Baz node with the Bar node.
Am I approaching this in the correct way?
Any responses are appreciated, thanks in advance...
Well, it is possible to do it in one query - unfortunately I don't think you can use the tasty UNWIND
or FOREACH
due to the secondary nesting, and you'll need to do some funky things with the classes, but well, here goes:
First, we need to define the classes, so we can deserialize the properties, but not serialize them, to that end
public class Foo
{
public int ID { get; set; }
[JsonIgnore]
public List<Bar> bar { get; set; }
[JsonProperty("bar")]
private List<Bar> barSetter { set { bar = value;} }
}
public class Bar
{
public int ID { get; set; }
[JsonIgnore]
public List<Baz> baz { get; set; }
[JsonProperty("baz")]
private List<Baz> bazSetter { set { baz = value; } }
}
public class Baz
{
public int ID { get; set; }
}
What is this craziness??!?! Well - By using [JsonIgnore]
we tell Json not to serialize or deserialize a given property - but we want to deserialize so your retrieval query will work - so having the private
setter with JsonProperty
allows us to achieve this.
The added bonus of this approach is that you don't need to specify properties to serialize in the Cypher
generation bit. And here it is in all it's glory:
var query = gc.Cypher
.Create("(f:Foo {fooParam})")
.WithParam("fooParam", foo);
for (int barIndex = 0; barIndex < foo.bar.Count; barIndex++)
{
var barIdentifier = $"bar{barIndex}";
var barParam = $"{barIdentifier}Param";
query = query
.With("f")
.Create($"(f)-[:HASBAR]->({barIdentifier}:Bar {{{barParam}}})")
.WithParam(barParam, foo.bar[barIndex]);
for (int bazIndex = 0; bazIndex < foo.bar[barIndex].baz.Count; bazIndex++)
{
var bazIdentifier = $"baz{barIndex}{bazIndex}";
var bazParam = $"{bazIdentifier}Param";
query = query
.With($"f, {barIdentifier}")
.Create($"({barIdentifier})-[:HASBAZ]->({bazIdentifier}:Baz {{{bazParam}}})")
.WithParam(bazParam, foo.bar[barIndex].baz[bazIndex]);
}
}
The f:Foo
bit is as per normal, the subsequent for
loops allow you to define each identifier and set the parameters.
I don't think this is an ideal solution, but it will work, and will execute in 1 query. Obvs, this could get unwieldy with lots of nested values.