与neomodel中多种类型(多态)的关系

时间:2016-03-02 10:25:35

标签: python neo4j py2neo neomodel

自2014年以来,存在与多种对象类型的关系不可用的问题: https://github.com/robinedwards/neomodel/issues/126

现在是2016年,我仍然不知道有关这个关键问题的任何解决方案。

使用示例:

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)

如果我尝试访问animals_owned的{​​{1}}关系,正如您所期望的那样,它只会检索AnimalNode基类而不是其子类(ownerDogNode),所以我我无法访问属性:CatNodesmell_level

我希望在neomodel中允许这样的事情:

vision_level

然后当我访问class Owner(StructuredNode): animals_owned = RelationshipTo(["DogNode", "CatNode"], "OWNED_ANIMAL") 的{​​{1}}关系时,它将检索animals_ownedowner类型的对象,以便我可以按照自己的意愿访问子类属性。

但是connect方法会产生以下错误:

DogNode

有没有办法以优雅的方式在neomodel中实现这一目标?

谢谢!

3 个答案:

答案 0 :(得分:1)

我最近做了类似的事情,以实现具有继承的元数据模型。相关代码位于:https://github.com/diging/cidoc-crm-neo4j/blob/master/crm/models.py

基本上我采用的方法是使用普通的多重继承来构建模型,新模型可以方便地在节点上变成相应的多个标签。这些模型都基于neomodel的StructuredNode的抽象子类;我添加了使用labels()inherited_labels()实例方法在类层次结构的各个级别重新实例化节点的方法。例如,此方法将节点重新实例化为其最衍生类或其层次结构中的特定类:

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

请注意,这部分是因为所有模型都在同一名称空间中定义;如果不是这样的话,这可能会变得棘手。这里仍有一些问题需要解决,但它可以完成工作。

使用这种方法,您可以定义与上级类的关系,然后使用劣等/更多派生类来实例化connect个节点。然后在检索时,将它们“向下”转移到它们的原始类(或层次结构中的某个类)。例如:

>>> 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'>

有关使用示例,请参阅this README

答案 1 :(得分:0)

好问题。

我猜你可以手动检查owner.animals_owned的每个元素是什么类型的对象,&#34;膨胀它&#34;到正确类型的对象。

但是自动拥有一些东西真的很棒。

答案 2 :(得分:0)

以下不是一个合适的解决方案,而是更多的解决方法。如错误中所述,isinstance()需要元组而不是字典。所以以下内容将起作用:

class Owner(StructuredNode):
    animals_owned = RelationshipTo((DogNode, CatNode), "OWNED_ANIMAL")

限制是必须在关系之前定义<{1}}和DogNode;引用的名称不起作用。它利用CatNode的一个特性,允许你传递可能类的元组。

但是,neomodel(截至目前)尚未正式支持此用法。尝试列出所有节点会产生错误,因为neomodel仍然期望该类型是类名而不是元组。

反向访问关系(isinstance - &gt; AnimalNode

如果您以其他方式定义关系,您仍然可以使用该关系,例如

Owner

然后使用class AnimalNode(StructuredNode): ... owner = RelationshipFrom("Owner", "OWNED_ANIMAL") AnimalNode.owner.get()等来检索所有者。

生成DogNode.owner.get()

的变通方法

要从animals_owned模型生成animals_owned,我使用了以下解决方法:

Owner

测试:

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

限制:

  • 如果有多个可接受的模型类型可用,则会选择随机模型。 (这可能是它尚未正式实施的部分原因:例如,如果>>> 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}>] 继承自PuppyModel,并且两者都是可能的选项,那么&和#39;这个函数决定你真正想要的那个是不容易的。)
  • 设置功能假定多个模型(只有一个模型不起作用)
  • 需要根据模型和关系手动编写Cypher查询(尽管如此,自动化应该非常简单)
  • 通过功能进行访问(添加DogModel装饰器将有助于解决此问题)

当然,您可能希望添加更多微调和安全检查,但这应该足以开始了。