Joe Down
Joe Down

Reputation: 41

Relationship to multiple types (polymorphism) in neomodel

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

Answers (4)

Daniel Donnelly
Daniel Donnelly

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.

  1. 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".

  2. 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

Erick Peirson
Erick Peirson

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

Hippo
Hippo

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.

Accessing relationship in reverse (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.

Workaround method to generate 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:

  • If the multiple acceptable model types are available, a random one will be picked. (That's probably part of the reason it hasn't been officially implemented: for example if there's a 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).
  • Set function assumes multiple models (having a single one will not work)
  • Cypher query needs to be manually written depending on the model and relationship (it should be pretty straightforward to automate, though)
  • Access is via a function (adding an @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

ivan
ivan

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

Related Questions