2
votes

I would like to use OGM of py2neo to represent a relationship from one node type to two node types.

I have a solution (below) that works only to store nodes/relationships in the DB, and I could not find one that works properly when retrieving relationships.

This is my example. Consider the relationship OWNS from a Person to a Car:

from py2neo.ogm import GraphObject, Property, RelatedTo
from py2neo import Graph

class Person(GraphObject):
    name = Property()
    Owns = RelatedTo("Car")

class Car(GraphObject):
    model = Property()

g = Graph(host="localhost", user="neo4j", password="neo4j")

# Create Pete
p = Person()
p.name = "Pete"  

# Create Ferrari
c = Car()
c.model = "Ferrari"

# Pete OWNS Ferrari
p.Owns.add(c)

# Store
g.push(p)

This works well and fine. Now, let's assume that a Person OWNS a House as well (this code continues from the one above):

class House(GraphObject):
    city = Property()

# Create House
h = House()
h.city = "New York"

# Pete OWNS House in New York
p.Owns.add(h)

# Update
g.push(p)

The "to" end of the relationship OWNS is supposed to point to a Car, not a House. But apparently py2neo does not care that much and stores everything in the DB as expected: a Person, a Car and a House connected via OWNS relationships.

Now the problem is to use the above classes to retrieve nodes and relationships. While node properties are loaded correctly, relationships are not:

p = Person.select(g).where(name="Pete").first()
for n in list(p.Owns):
    print type(n).__name__

This results in:

Car
Car

This behavior is consistent with the class objects.

How can I model "Person OWNS Car" and "Person OWNS House" with the same class in py2neo.ogm? Is there any known solution or workaround that I can use here?

2
strange. I replicated the same scenario in my system. But for me only one line of Car was printed out.ham
I checked again with py2neo 3.1.1 and the output is as in the original question. Perhaps a different py2neo version? Still, my point here is that I'd like to achieve a 'Car House' output.gianko

2 Answers

2
votes

The issue is that "Owns" is set up as a relationship to the "Car" node. You need to set up another relationship to own a house. If you want the relationship to have the label of "OWNS" in Neo4j, you need to populate the second variable of the RelatedTo function. This is covered in the Py2Neo documentation (http://py2neo.org/v3/) in chapter 3.

class Person(GraphObject):
    name = Property()

    OwnsCar = RelatedTo("Car", "OWNS")
    OwnsHouse = RelatedTo("House" "OWNS")

class Car(GraphObject):
    model = Property()

class House(GraphObject):
    city = Property()

I do want to say that Rick's answer addressed something I was trying to figure out with labeling with the Py2Neo OGM. So thanks Rick!

1
votes

I had essentially the same question. I was unable to find an answer and tried to come up with a solution to this using both py2neo and neomodel.

Just a Beginner

It is important to note that I am definitely not answering this as an expert in either one of these libraries but rather as someone trying to evaluate what might be the best one to start a simple project with.

End Result

The end result is that I found a workaround in py2neo that seems to work. I also got a result in neomodel that I was even happier with. I ended up a little frustrated by both libraries but found neomodel the more intuitive to a newcomer.

An Asset Label is the Answer Right?

I thought that the answer would be to create an "Asset" label and add this label to House and Car and create the [:OWNS] relationship between Person and Asset. Easy right? Nope, apparently not. There might be a straightforward answer but I was unable to find it. The only solution that I got to work in py2neo was to drop down to the lower-level (not OGM) part of the library.

Here's what I did in py2neo:

class Person(GraphObject):
    name = Property()

class Car(GraphObject):
    name = Property()
    model = Property()
    asset = Label("Asset")

class House(GraphObject):
    name = Property()
    city = Property()
    asset = Label("Asset")

g = graph

# Create Pete
p = Person()
p.name = "Pete"
g.push(p)

# Create Ferrari
c = Car()
c.name = "Ferrari"
c.asset = True
g.push(c)

# Create House
h = House()
h.name = "White House"
h.city = "New York"
h.asset = True
g.push(h)

# Drop down a level and grab the actual nodes
pn = p.__ogm__.node
cn = c.__ogm__.node

# Pete OWNS Ferrari (lower level py2neo)
ap = Relationship(pn, "OWNS", cn)
g.create(ap)

# Pete OWNS House (lower level py2neo)
hn = h.__ogm__.node
ah = Relationship(pn, "OWNS", hn)
g.create(ah)

# Grab & Print
query = """MATCH (a:Person {name:'Pete'})-[:OWNS]->(n)
           RETURN labels(n) as labels, n.name as name"""
data = g.data(query)
for asset in data:
    print(asset)

This results in:

{'name': 'White House', 'labels': ['House', 'Asset']}
{'name': 'Ferrari', 'labels': ['Car', 'Asset']}

Neomodel Version

py2neo seems to do some clever tricks with the class names to do its magic and the library seems to exclude Labels from this magic. (I hope I am wrong about this but as I said, I could not solve it). I decided to try neomodel.

class Person(StructuredNode):
    name = StringProperty(unique_index=True)
    owns = RelationshipTo('Asset', 'OWNS')
    likes = RelationshipTo('Car', "LIKES")

class Asset(StructuredNode):
    __abstract_node__ = True
    __label__ = "Asset"
    name = StringProperty(unique_index=True)

class Car(Asset):
    pass

class House(Asset):
    city = StringProperty()

# Create Person, Car & House
pete = Person(name='Pete').save()
car = Car(name="Ferrari").save()
house = House(name="White House", city="Washington DC").save()

#Pete Likes Car
pete.likes.connect(car)

# Pete owns a House and Car
pete.owns.connect(house)
pete.owns.connect(car)

After these objects are created they are relatively simple to work with:

for l in pete.likes.all():
    print(l)

Result:

{'name': 'Ferrari', 'id': 385}

With the "abstract" relationship the result is an object of that type, in this case Asset.

for n in pete.owns.all():
    print(n)
    print(type(n))

Result:

{'id': 389}
<class '__main__.Asset'>

There seems to be a way to "inflate" these objects to the desired type but I gave up trying to figure that out in favor of just using Cypher. (Would appreciate some help understanding this...)

Dropping down to the Cypher level, we get exactly what we want:

query = "MATCH (a:Person {name:'Pete'})-[:OWNS]->(n) RETURN n"
results, meta = db.cypher_query(query)
for n in results:
    print(n)

Result:

[<Node id=388 labels={'Asset', 'Car'} properties={'name': 'Ferrari'}>]
[<Node id=389 labels={'Asset', 'House'} properties={'city': 'Washington DC', 'name': 'White House'}>]

Conclusion

The concept of Labels is very intuitive for a lot of the problems I would like to solve. I found py2neo's treatment of Labels confusing. Your workaround might be to drop down to the "lower-level" of py2neo. I personally thought the neomodel syntax was more friendly and suggest checking it out. HTH.