【问题标题】:hybrid_property "is_parent" on self referencial one-to-many parent-child model in sqlalchemysqlalchemy 中自我引用的一对多父子模型上的 hybrid_property "is_parent"
【发布时间】:2020-09-16 03:58:03
【问题描述】:

我有一个具有一对多父子关系的自我参照模型。模型实例可以链接到父实例,然后该实例将构成观察组的一部分,每个子代的observation_id 和父代是组的父代id。这个observation_id 是模型的混合属性。我想添加一些 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 似乎总是评估为假。在表达式属性中生成的 SQL(基于上例中的调试打印)看起来是这样的:

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

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

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),但事实并非如此。如果这不是一个自引用模型,我认为(?)这将适用于不同的父/子类,但在这种情况下它不起作用。

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

任何建议将不胜感激!

【问题讨论】:

    标签: python orm sqlalchemy


    【解决方案1】:

    可行的is_parent表达式类方法:

    @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])                      
    

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

    编辑

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


    好问题,顺便说一句!定义明确!

    【讨论】:

    • 感谢您的回答。有趣的方法 - 它有效,但在整个桌子上执行 fetchall 似乎太慢而不实用。该表有数百万行,以这种方式评估 parent_id 表达式在当前表(不断增长)上需要几分钟。还有其他更有效的方法吗?
    • @Ari ,这是我对实施的主要关注。我会考虑的。
    • @Ari ,不,不,问题是.scalar() > 0 返回True,查询结果是单个布尔值。要提供过滤,您需要一个返回每个条目的布尔值的查询。如果您有一百万个条目,则查询必须返回一百万个布尔值。这就是为什么我认为没有一种有效的解决方案与我所做的有显着不同的原因。
    • 补充讨论:db.session.execute(db.select([func.count(AnnotationLabel.id)]).where(AnnotationLabel.parent_id==837838)).scalar() > 0 按预期返回 Truetype(db.select([func.count(AnnotationLabel.id)]).where(AnnotationLabel.parent_id==837838).as_scalar()>0) 是:sqlalchemy.sql.elements.BinaryExpression我最初在上面尝试的方法似乎在父/子类不同且不自引用时有效。例如this question的解决方案
    • 抱歉@|159,我无法在 5 分钟后编辑我之前的评论,所以我不得不删除它,但你上面的回答仍然是相关的。根据数据集的大小,标量评估仍然会很慢......嗯......我希望有一些方法可以解决这个问题!
    【解决方案2】:

    我想我会用当前最好的可行解决方案发布答案​​。这是基于@|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 父级并仅返回一个不同的 parent_id 列表以使用 .in_ 条件进行测试,而不是针对包括重复项在内的数百万个 null 值测试 .in_ 条件,这有效,但慢得难以置信。

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

    编辑

    此解决方案的性能不是很好,即使不过滤这些属性也会导致模型的查询延迟显着,因此我不得不停用is_parentobservation_id hybrid_properties。我定义了一个非混合属性并修改了我的查询以避开性能问题:

    @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)查询同一观察组的成员。不理想或不优雅 - 这种方法会导致我希望能够进行的查询类型受到一些限制,因此如果有更好的答案,我会接受。

    【讨论】:

    • 如果您认为某个答案有帮助或解决了您的问题,您可以投票或/或接受该答案。这就是 Stackoverflow 评级的工作原理。此外,如果它是最相关的,您可以接受自己的答案。
    • 我希望不必接受它作为答案,以防有人提供更好的解决方案,但我不确定是否有更好的选择。
    猜你喜欢
    • 2014-07-31
    • 1970-01-01
    • 2017-01-25
    • 2018-04-24
    • 2015-01-08
    • 1970-01-01
    • 2016-08-23
    • 2011-08-23
    • 2011-05-09
    相关资源
    最近更新 更多