Reputation: 41
Since 2014, there was an issue that relationship to multiple object types is not available: https://github.com/robinedwards/neomodel/issues/126
It's now 2016, and still I'm not aware of any solution regarding this critical issue.
Example for usage:
class AnimalNode(StructuredNode):
tail_size = IntegerProperty()
age = IntegerProperty()
name = StringProperty()
class DogNode(AnimalNode):
smell_level = IntegerProperty()
class CatNode(AnimalNode):
vision_level = IntegerProperty()
class Owner(StructuredNode):
animals_owned = RelationshipTo("AnimalNode", "OWNED_ANIMAL")
dog_node1 = DogNode(name="Doggy", tail_size=3, age=2, smell_level=8).save()
cat_node1 = CatNode(name="Catty", tail_size=3, age=2, vision_level=8).save()
owner = Owner().save()
owner.animals_owned.connect(dog_node1)
owner.animals_owned.connect(cat_node1)
If I try to access animals_owned
relationship of the owner
, as you expect, it retrives only AnimalNode baseclasses and not its subclasses (DogNode
or CatNode
) so I am not able to access the attributes: smell_level
or vision_level
I would want something like this to be permitted in neomodel:
class Owner(StructuredNode):
animals_owned = RelationshipTo(["DogNode", "CatNode"], "OWNED_ANIMAL")
and then when I will access animals_owned
relationship of owner
, It will retrieve objects of types DogNode
and CatNode
so I can access the subclasses attributes as I wish.
But the connect method yields the following error:
TypeError: isinstance() arg 2 must be a type or tuple of types
Is there any way to achieve that in neomodel in an elegant way?
Thanks!
Upvotes: 4
Views: 1677
Reputation: 610
I also had issues with Polymorhism in Neomodel. I don't know if this answers your question, but this question comes up at the top when googling. So to those who are unaware of this feature (see option 2 below), this answer is for them.
The issue I had was that if you have classes A,B derived from C, and you have mixed chains of relationships (the nodes can be either A or B) then there was no way to inflate to the correct type without doing some heavy cypher switch-cases and having a special "type" property in your base class, which tells you what derived type you're in. I still couldn't get that to work.
I actually refactored my neomodels into one largish class that "does everything", the C-struct "swiss army knife" approach. I dug deeper. There are currently two solutions to my polymorphism issue.
Drop Neomodel, and do all of your Neo4j interfacing with cypher. This might be an option if you're not really taking advantage of the features Neomodel has to offer and you're doing a lot of custom queries anyway. When you do this, you can use multi-labels like so: (A:C), or (B:C), which encodes in a sense what's what. I don't know if this fixes my original issue, but that's what I read others were doing to achieve "polymorphism".
Read this carefully:
Therefore, a Node with labels "BasePerson", "TechnicalPerson" would lead to the instantiation of a TechnicalPerson object. This automatic resolution is optional and can be invoked automatically via neomodel.Database.cypher_query if its resolve_objects parameter is set to True (the default is False).
That's from this page: https://neomodel.readthedocs.io/en/latest/extending.html
Therefore, I think that solves my original issue of wanting to do real polymorphism as in a C++ or Python class hierarchy itself.
This should allow you to auto-inflate to the correct subclasses whenever you need to (an aspect of polymorphism).
Disclaimer. I haven't tried it out yet, but intuitively it should work.
Upvotes: 0
Reputation: 81
I recently did something like this in order to implement a metadata model with inheritance. The relevant code is here: https://github.com/diging/cidoc-crm-neo4j/blob/master/crm/models.py
Basically the approach I took was to use plain-old multiple inheritance to build the models, which neomodel conveniently turns into correspondingly multiple labels on the nodes. Those models were all based on an abstract subclass of neomodel's StructuredNode
; I added methods to re-instantiate the node at various levels of the class hierarchy, using the labels()
and inherited_labels()
instance methods. For example, this method will re-instantiate a node as either its most derivative class or a specific class in its hierarchy:
class HeritableStructuredNode(neomodel.StructuredNode):
def downcast(self, target_class=None):
"""
Re-instantiate this node as an instance its most derived derived class.
"""
# TODO: there is probably a far more robust way to do this.
_get_class = lambda cname: getattr(sys.modules[__name__], cname)
# inherited_labels() only returns the labels for the current class and
# any super-classes, whereas labels() will return all labels on the
# node.
classes = list(set(self.labels()) - set(self.inherited_labels()))
if len(classes) == 0:
return self # The most derivative class is already instantiated.
cls = None
if target_class is None: # Caller has not specified the target.
if len(classes) == 1: # Only one option, so this must be it.
target_class = classes[0]
else: # Infer the most derivative class by looking for the one
# with the longest method resolution order.
class_objs = map(_get_class, classes)
_, cls = sorted(zip(map(lambda cls: len(cls.mro()),
class_objs),
class_objs),
key=lambda (size, cls): size)[-1]
else: # Caller has specified a target class.
if not isinstance(target_class, basestring):
# In the spirit of neomodel, we might as well support both
# class (type) objects and class names as targets.
target_class = target_class.__name__
if target_class not in classes:
raise ValueError('%s is not a sub-class of %s'\
% (target_class, self.__class__.__name__))
if cls is None:
cls = getattr(sys.modules[__name__], target_class)
instance = cls.inflate(self.id)
# TODO: Can we re-instatiate without hitting the database again?
instance.refresh()
return instance
Note that this works partly because all of the models are defined in the same namespace; this might get tricky if that were not the case. There are still some kinks here to work out, but it gets the job done.
With this approach, you can define a relation to a superior class, and then connect
nodes instantiated with inferior/more derivative classes. And then upon retrieval, "downcast" them to their original class (or some class in the hierarchy). For example:
>>> for target in event.P11_had_participant.all():
... original_target = target.downcast()
... print original_target, type(original_target)
{'id': 39, 'value': u'Joe Bloggs'} <class 'neomodel.core.E21Person'>
See this README for usage examples.
Upvotes: 2
Reputation: 510
This following is not a proper solution, but more of a workaround. As noted in the error, isinstance()
requires a tuple and not a dictionary. So the following will work:
class Owner(StructuredNode):
animals_owned = RelationshipTo((DogNode, CatNode), "OWNED_ANIMAL")
The limitation is that DogNode
and CatNode
have to be defined before the relationship; a quoted name will not work. It makes use of a feature of isinstance
which allows you to pass a tuple of possible classes.
However, this usage is not officially supported in neomodel (as of now). Trying to list all the nodes will give an error, since neomodel still expects the type to be a class name and not a tuple.
AnimalNode
-> Owner
)You can still make use of the relationship if you define it the other way as well, like
class AnimalNode(StructuredNode):
...
owner = RelationshipFrom("Owner", "OWNED_ANIMAL")
and then use AnimalNode.owner.get()
, DogNode.owner.get()
, etc. to retrieve the owner.
animals_owned
To generate animals_owned
the from the Owner
model, I used the following workaround method:
class Owner(StructuredNode):
...
def get_animals_owned(self):
# Possible classes
# (uses animals_owned property and converts to set of class names)
allowed_classes = set([i.__name__ for i in self.animals_owned.definition['node_class']])
# Retrieve all results with OWNED_ANIMAL relationship to self
results, columns = self.cypher('MATCH (o) where id(o)={self} MATCH (o)-[:OWNED_ANIMAL]->(a) RETURN a')
output = []
for r in results:
# Select acceptable labels (class names)
labels = allowed_classes.intersection(r[0].labels)
# Pick a random label from the selected ones
selected_label = labels.pop()
# Retrieve model class from label name
# see http://stackoverflow.com/a/1176179/1196444
model = globals()[selected_label]
# Inflate the model to the given class
output.append(model.inflate(r[0]))
return output
Testing:
>>> owner.get_animals_owned()
[<CatNode: {'age': 2, 'id': 49, 'vision_level': 8, 'name': 'Catty', 'tail_size': 3}>, <DogNode: {'age': 2, 'id': 46, 'smell_level': 8, 'name': 'Doggy', 'tail_size': 3}>]
Limitations:
PuppyModel
which inherits from DogModel
, and both are possible options, there's no easy way for the function to decide which one you really wanted).@property
decorator will help fix this)Of course, you will probably want to add more fine-tuning and safety-checks, but that should be enough to start off.
Upvotes: 1
Reputation: 348
Good question.
I guess you could manually check what type of object each element of owner.animals_owned
is and "inflate it" to the right type of object.
But would be really nice to have something automatic.
Upvotes: 1