Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 919
Joe Down Avatar asked Oct 31 '25 13:10

Joe Down


2 Answers

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.

like image 169
Erick Peirson Avatar answered Nov 03 '25 03:11

Erick Peirson


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.

like image 35
ivan Avatar answered Nov 03 '25 02:11

ivan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!