hybrid_property "is_parent" 关于 sqlalchemy 中的自引用一对多父子模型

hybrid_property "is_parent" on self referencial one-to-many parent-child model in sqlalchemy

我有一个具有一对多父子关系的自我引用模型。模型实例可以链接到父实例,父实例将构成观察组的一部分,每个子实例 observation_id,父实例是组的父实例 id。这个observation_id是一个hybrid_property的模型。我想添加一些 hybrid_property 表达式以启用对这些混合属性的过滤,但我坚持使用 is_parent 表达式定义。以下是该模型的摘录:

class AnnotationLabel(Model):
    __tablename__ = 'annotation'
    id = db.Column(db.Integer, primary_key=True)
    ...
    parent_id = db.Column(db.ForeignKey("annotation.id", ondelete="CASCADE", nullable=True, index=True)
    parent = relationship('AnnotationLabel', remote_side='AnnotationLabel.id', 
        backref=backref('children', passive_deletes=True, lazy='dynamic'))

    @hybrid_property
    def is_child(self):
        """BOOLEAN, whether or not this annotation has a linked parent annotation"""
        return self.parent_id is not None

    @is_child.expression
    def is_child(cls):
        return cls.parent_id.isnot(None)

    @hybrid_property
    def is_parent(self):
        """BOOLEAN, whether or not this annotation has linked children / descendants"""
        return self.children.count() > 0

    @is_parent.expression
    def is_parent(cls):
        # TODO: this does not work. 
        q = select([func.count(cls.id)]).where(cls.parent_id==cls.id)
        print(q)  # debug
        return q.as_scalar() > 0

    @hybrid_property
    def observation_id(self):
        """INT, denoting the observation group id for linked observations of the same object (returns None if not linked)"""
        return self.id if self.is_parent else self.parent_id if self.is_child else None

    @observation_id.expression
    def observation_id(cls):
        # TODO: this may work if is_parent.expression was fixed? But haven't had a chance to test it
        return db.case([(cls.is_child, cls.parent_id), (cls.is_parent, cls.id)], else_=None)

目前 @is_parent.expression 似乎总是计算为 false。在表达式属性中生成的 SQL(基于上面示例中的调试打印)看起来是这样的:

SELECT count(annotation.id) AS count_1 FROM annotation WHERE annotation.parent_id = annotation.id

这永远不会真正发生,因为一个实例通常不是它自己的父实例,而是其他实例的父实例,因此,在对其进行过滤时,它总是 returns 什么都没有。例如:

printfmt="ID: {a.id}, parent_id: {a.parent_id}, observation_id: {a.observation_id}, is_parent: {a.is_parent}, is_child: {a.is_child}"  # instance print formatter

# THIS WORKS - returns the two child instances
for a in AnnotationLabel.query.filter(AnnotationLabel.is_child==True).all():
    print(printfmt.format(a=a))
# ID: 837837, parent_id: 837838, observation_id: 837838, is_parent: False, is_child: True
# ID: 837909, parent_id: 837838, observation_id: 837838, is_parent: False, is_child: True

# THIS WORKS, PARENT INSTANCE HAS CORRECT PROPERTIES
parent = AnnotationLabel.query.get(837838)   # get the parent in question
# This works, since it's using the instance attributes
print(printfmt.format(a=parent))
# ID: 837838, parent_id: None, observation_id: 837838, is_parent: True, is_child: False

# THIS DOES NOT WORK!!!??? .expression for is_parent is broken
for a in AnnotationLabel.query.filter(AnnotationLabel.is_parent==True).all():
    print(printfmt.format(a=a))
# returns nothing, should be list containing 1 parent instance

# THIS ALSO DOES NOT WORK PROPERLY - ONLY RETURNS CHILDREN, NOT PARENT
for a in AnnotationLabel.query.filter(AnnotationLabel.observation_id==837838).all():
    print(printfmt.format(a=a))
# ID: 837837, parent_id: 837838, observation_id: 837838, is_parent: False, is_child: True
# ID: 837909, parent_id: 837838, observation_id: 837838, is_parent: False, is_child: True

按照逻辑,我希望看到在上面的最后两个查询中返回父项 (id=837838),但事实并非如此。如果这不是一个自我参照模型,我 认为 (?) 这将适用于不同的 parent/child classes,但在这种情况下它不起作用。

如何为 class 表达式 @is_parent.expression 获得与 is_parent 的实例 hybrid_property 相同的功能,并使 is_parentobject_id 属性可查询?

如有任何建议,我们将不胜感激!

可行的is_parent表达式class方法:

@is_parent.expression                                                  
def is_parent(cls):                                                    
    parent_ids = db.session.execute(select([cls.parent_id])).fetchall()
    return cls.id.in_([i[0] for i in parent_ids])                      

您必须 return 形成表达式 class 方法的对象类型是 sqlalchemy.sql.elements.BinaryExpression,它根据条目提供布尔比较结果。这样,使用count是一个错误的假设。

编辑

原始解决方案与我所做的解决方案之间的主要区别在于查询结果的性质。 count 的结果 .scalar() > 0 是单个布尔值。传递给 filter(达到比较)的查询必须 return 每个元素的布尔值,因为它的过滤本质上是 table 内容的二进制屏蔽。


问得好,顺便说一句!定义好!

我想我会 post 使用当前最可行的解决方案来回答。这是基于@|159 提供的非常有用的答案的改进版本。 is_parent 表达式的当前可行解决方案是:

@is_parent.expression
def is_parent(cls):
    parent_ids = [i[0] for i in db.session.query(cls.parent_id).filter(cls.parent_id.isnot(None)).distinct().all()]
    return cls.id.in_(parent_ids)

这改进了过滤 null parents 并且只返回一个不同的 parent_ids 列表来测试 .in_ 条件,而不是测试 .in_针对数百万个空值(包括重复值)的条件,它有效,但速度慢得令人难以置信。

目前,对于只有很少 parents 的数据集的大小,这似乎适合快速运行,但如果 parents 的列表变得非常大(理论上它可以) ,我想这可能会再次变慢。我正在 post 总结迄今为止最好的工作解决方案作为思考的食物,希望有人可以提供更好、更具可扩展性的方法。

编辑

此解决方案的性能不是很好,即使不过滤这些属性也会导致模型出现严重的查询延迟,因此我不得不停用 is_parentobservation_id hybrid_properties。我定义了一个 non-hybrid 属性 并将我的查询修改为 side-step 性能问题:

@property
def observation_id(self):
    return self.parent_id if self.is_child else self.id if self.children.count()>0 else None

并且可以通过查询or_(AnnotationLabel.id==self.observation_id,AnnotationLabel.parent_id==self.observation_id)来查询同一观察组的成员。不理想或不优雅 - 这种方法导致我希望能够进行的查询类型受到一些限制,因此如果有更好的答案,我会接受。