Another day, another blog post update

Gist: https://gist.github.com/cskardon/7870411

I know! Another question on StackOverflow leads to yet another blog post being updated to use the newer Neo4jClient. So here goes:

Darko Micics has written a post about using Neo4jClient with C#.Net, and a question has arisen on StackOverflow asking about how to (in effect) do this in the new version of the Neo4JClient – the version where ‘CreateRelationship’ et al have been marked as obsolete.

Luckily, this is a pretty simple (twitter-ish) data set – but in this one, we’re also going to add some data to the relationships, which we eschewed last time. Oh, and we’ll use C# instead of F# :)

The Setup

So. We’ll start with the super simple ‘Person’ class, which is EXACTLY the same as Darko’s:

public class Person { public string Name { get; set; } }

Now, in Darko’s we have a ‘Knows’ and a ‘Hates’ relationship. This is where I will diverge slightly, as I personally prefer ‘Likes’ and ‘Dislikes’ (Hate is a very strong word) – plus, even if you hate someone (perhaps especially) you still ‘know’ them, otherwise how could you hate them?

ANYHEWS

We still want a reason for our dislikes (and maybe even our likes), and so we add another POCO – the ‘Reason’ class:

public class Reason { public string Value { get; set; } }

And because I’m a bit lazy, and prone to typos I’m going to introduce a ‘Relationship’ enum with the relationships we’ll be modeling:

public enum Relationship
{
    LIKES,
    DISLIKES
}

Creation

The original uses the ‘Create’ method on the Neo4jClient, but this uses the old skool REST API, and we don’t want to do this anymore, we want to Cypher this bad boy all the way, so let’s add a ‘Create’ method:

private void Create(Person p)
{
    Client.Cypher
        .Create("(p:Person {params})")
        .WithParam("params", p)
        .ExecuteWithoutResults();
}

Two things of note, firstly, we’re adding a label when creating (the :Person bit), and secondly we’re passing in the person as a parameter (the “params”, p bit). Now we can do the first bit of the code:

Create(new Person{Name = "Person A"});
Create(new Person{Name = "Person B"});
Create(new Person{Name = "Person C"});
Create(new Person{Name = "Person D"});

Now we want to relate them together, the cypher for this is pretty simple, MATCH the two Persons, create a new relationship between them. I’ve put this into a method, which also adds a reason if supplied:

private void CreateRelationship(Person from, Person to, Relationship relationship, Reason reason = null)
{
    var query = Client.Cypher
        .Match("(f:Person), (t:Person)")
        .Where((Person f) => f.Name == from.Name)
        .AndWhere((Person t) => t.Name == to.Name);

    if(reason == null)
        query
            .Create(string.Format("(f)-[:{0}]->(t)", relationship))
.ExecuteWithoutResults(); else query .Create(string.Format("(f)-[:{0} {{relParam}}]->(t)", relationship)) .WithParam("relParam", reason) .ExecuteWithoutResults(); }

Let’s break this down a bit, the first section creates the correct ‘MATCH / WHERE’ clause, which will find our two people. Obviously there is a huge issue if two people have the same name!!! The second part creates the actual create statement, adds the reason (if needed) and executes the query.

So now we can change our startup code to be:

var pA = new Person {Name = "Person A"};
var pB = new Person {Name = "Person B"};
var pC = new Person {Name = "Person C"};
var pD = new Person {Name = "Person D"};

Create(pA);
Create(pB);
Create(pC);
Create(pD);

CreateRelationship(pA, pB, Relationship.LIKES);
CreateRelationship(pB, pC, Relationship.LIKES);
CreateRelationship(pB, pD, Relationship.DISLIKES, new Reason{Value = "Crazy guy"});
CreateRelationship(pC, pD, Relationship.DISLIKES, new Reason{Value = "Don't know why..."});
CreateRelationship(pD, pA, Relationship.LIKES);

Which gives us the following graph:

image

(I’ve clicked on the dislikes relationship to show the value has been set).

Getting

Next up we want to get all the people who dislike Person D. The query is pretty simple in this case:

var query = Client.Cypher
    .Match("(p:Person)<-[r:DISLIKES]-(other:Person)")
    .Where((Person p) => p.Name == disliked.Name)
    .Return((other, r) =>
        new
        {
            Name = other.As<Person>().Name,
            Reason = r.As<Reason>().Value
        });

Here we project the results into an anonymous type – if we wanted to pass these outside of the method we’d need another class, but we all know that!

We can take those results and write them to the screen:

var res = query.Results.ToList();
Console.WriteLine("{0} disliked by:", disliked.Name);
foreach (var result in res)
    Console.WriteLine("\t{0} because {1}", result.Name, result.Reason);

Which gets us:

image

Aceness.

Improving

So, that basically updates the code ‘as-is’. First cut benefits include

  • no need for relationship classes,
  • we get to use labels
  • our code is directly transferable to Cypher, so we can test the queries on the Neo4j management page straight away.

There are some problems with the code (just some I hear you say?)

  • We could create multiple relationships between the same people – if I call ‘CreateRelationship’ n times, I will get n relationships – which in this example doesn’t make sense.
  • The labels are hand typed, which means they can be typo’d which will cause no end of trouble down the line.

Fixing those problems

Multiple Relationships

The first problem of multiples of the same relationship is a simple one to fix, we change the ‘create’ on our ‘CreateRelationship’ to be ‘CreateUnique’. So CreateRelationship becomes:

private void CreateRelationship(Person from, Person to, Relationship relationship, Reason reason = null)
{
    var query = Client.Cypher
        .Match("(f:Person), (t:Person)")
        .Where((Person f) => f.Name == from.Name)
        .AndWhere((Person t) => t.Name == to.Name);

    if(reason == null)
        query
            .CreateUnique(string.Format("(f)-[:{0}]->(t)", relationship)).ExecuteWithoutResults();
    else
        query
            .CreateUnique(string.Format("(f)-[:{0} {{relParam}}]->(t)", relationship))
            .WithParam("relParam", reason)
            .ExecuteWithoutResults();
}

and you can run the following Cypher.

Labels

Typo’s being the biggest issue, we could go down a similar route to using an enum, so:

public enum Labels {
    Person,
}

which we then use:

...Cypher.Match(string.Format("(p:{0})", Labels.Person))...

Or another option is to define a base class for all the objects we’re serializing like so:

public abstract class Neo4jObject
{
    [JsonIgnore]
    public string Labels { get; private set; }

    protected Neo4jObject(string labels)
    {
        Labels = labels;
    }
}

(We JsonIgnore the property, as we don’t want it serialized to the database). This is then implemented by all the data items you want stored:

public class Person : Neo4jObject
{
    public Person() : base("Person") { }
    public string Name { get; set; }
}

Used in very much the same way as the enum would be – the big downside is that you need an instance to get the labels, which tends to make you lean towards doing this sort of implementation instead:
public class Person : Neo4jObject
{
    public const string TypeLabels = "Person";
    public Person() : base(TypeLabels) { }
    public string Name { get; set; }
}

Which allows you to get the Labels without needing an instance. Benefits wise – anyone who subsequently uses your class – provided they use the ‘Labels’ property will get the same labels.

I’m still not 100% sure on which method I prefer at the moment, but I’m leaning towards the latter.

Anyhews, that as they say is it.

Print | posted @ Monday, December 9, 2013 10:51 AM

Comments on this entry:

No comments posted yet.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: