>自 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)
如果我尝试访问owner
的关系animals_owned
如您所料,它仅检索 AnimalNode 基类,而不检索其子类(DogNode
或 CatNode
),所以我无法访问属性:smell_level
或vision_level
我希望在 neomodel 中允许这样的事情:
class Owner(StructuredNode):
animals_owned = RelationshipTo(["DogNode", "CatNode"], "OWNED_ANIMAL")
然后当我访问animals_owned
owner
的关系时,它将检索DogNode
和CatNode
类型的对象,以便我可以根据需要访问子类属性。
但是连接方法会产生以下错误:
TypeError: isinstance() arg 2 must be a type or tuple of types
有没有办法在新模型中以优雅的方式实现这一目标?
谢谢!
我最近做了这样的事情,以实现具有继承的元数据模型。相关代码在这里: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'>
有关使用示例,请参阅此自述文件。
好问题。
我想您可以手动检查owner.animals_owned
的每个元素的对象类型,并将其"膨胀"为正确类型的对象。
但是如果有一些自动的东西会很好。
以下不是正确的解决方案,而更像是一种解决方法。如错误中所述,isinstance()
需要一个元组而不是字典。因此,以下内容将起作用:
class Owner(StructuredNode):
animals_owned = RelationshipTo((DogNode, CatNode), "OWNED_ANIMAL")
限制是必须在关系之前定义DogNode
和CatNode
;引用的名称将不起作用。它利用了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}>]
局限性:
- 如果有多个可接受的模型类型可用,则将随机选择一个模型类型。(这可能是它尚未正式实现的部分原因:例如,如果有一个继承自
DogModel
的PuppyModel
,并且两者都是可能的选项,那么函数就没有简单的方法来决定你真正想要哪一个)。 - 设置函数假设多个模型(只有一个模型不起作用)
- Cypher 查询需要根据模型和关系手动编写(不过,自动化应该非常简单)
- 通过函数访问(添加
@property
装饰器将有助于解决此问题)
当然,您可能希望添加更多的微调和安全检查,但这应该足以开始。