【问题标题】:Relationship to multiple types (polymorphism) in neomodelneomodel中与多种类型(多态性)的关系
【发布时间】:2016-03-02 10:25:34
【问题描述】:

自 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)

如果我尝试访问owneranimals_owned 关系,如您所料,它只检索AnimalNode 基类而不是它的子类(DogNodeCatNode),所以我无法访问这些属性: smell_levelvision_level

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

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

然后当我访问owneranimals_owned 关系时,它将检索DogNodeCatNode 类型的对象,因此我可以根据需要访问子类属性。

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

TypeError: isinstance() arg 2 must be a type or tuple of types 

有什么方法可以优雅地在 neomodel 中实现这一点?

谢谢!

【问题讨论】:

    标签: python neo4j py2neo neomodel


    【解决方案1】:

    我最近做了类似的事情来实现一个带有继承的元数据模型。相关代码在这里:https://github.com/diging/cidoc-crm-neo4j/blob/master/crm/models.py

    基本上我采用的方法是使用普通的多重继承来构建模型,neomodel 可以方便地在节点上转换为相应的多个标签。这些模型都基于 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

    【讨论】:

      【解决方案2】:

      好问题。

      我猜你可以手动检查owner.animals_owned 的每个元素是什么类型的对象,然后“膨胀”到正确的对象类型。

      但如果有一些自动的东西真的很好。

      【讨论】:

        【解决方案3】:

        以下不是正确的解决方案,而是一种解决方法。如错误中所述,isinstance() 需要一个元组而不是字典。所以以下将起作用:

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

        限制是DogNodeCatNode 必须在关系之前定义;引用的名称将不起作用。它利用了isinstance 的一个特性,它允许你传递一个可能的类的元组。

        但是,neomodel(截至目前)并未正式支持这种用法。尝试列出所有节点会出错,因为 neomodel 仍然希望类型是类名而不是元组。

        反向访问关系(AnimalNode -> Owner

        如果你也以另一种方式定义它,你仍然可以使用关系,比如

        class AnimalNode(StructuredNode):
            ...
            owner = RelationshipFrom("Owner", "OWNED_ANIMAL")
        

        然后使用AnimalNode.owner.get()DogNode.owner.get()等来检索所有者。

        生成animals_owned的解决方法

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

        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}>]
        

        限制:

        • 如果有多种可接受的模型类型可用,则会随机选择一种。 (这可能是它尚未正式实施的部分原因:例如,如果有一个继承自 DogModelPuppyModel,并且两者都是可能的选项,那么该函数没有简单的方法来决定您真正想要哪个)。
        • Set 函数假定有多个模型(只有一个模型不起作用)
        • Cypher 查询需要根据模型和关系手动编写(不过自动化应该很简单)
        • 通过函数访问(添加 @property 装饰器将有助于解决此问题)

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

        【讨论】:

          猜你喜欢
          • 2020-10-17
          • 2019-11-05
          • 1970-01-01
          • 2016-06-29
          • 2021-04-13
          • 2018-09-17
          • 2014-01-10
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多