【问题标题】:How to make SQLAlchemy hybrid_property select expressions work for both select and filter如何使 SQLAlchemy hybrid_property 选择表达式适用于选择和过滤器
【发布时间】:2019-08-22 18:18:45
【问题描述】:

我一直在尝试编写一些具有混合属性的表达式,但我发现它们非常有限,我想知道我是否可以绕过这些限制。

基本上,我发现它们可以与 session.query(Model.hybrid_property)session.query(Model).filter(Model.hybrid_property==x) 一起使用,但不能同时使用。

这是我的意思的一个例子,假设有两行称为value1value2namehybrid_property

# With as_scalar()
>>> session.query(Model).filter(Model.value=='value1').all()
[([<__main__.Model object],)]         # this is wanted
>>> session.query(Model.value).all()
[(u'value1',)]

# Without as scalar()
>>> session.query(Model).filter(Model.value=='value1').all()
[]
>>> session.query(Model.value).all()
[(u'value1',), (u'value2',)]          # this is wanted

根据是否使用as_scalar(),它会改变它的作用。有没有办法让它同时工作?

这是一些示例代码(经过编辑以显示完全不起作用的示例):

import os
from sqlalchemy import create_engine, Column, Integer, String, select, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import sessionmaker, relationship

Engine = create_engine('sqlite://')

Base = declarative_base(bind=Engine)

Session = sessionmaker(bind=Base.metadata.bind)


class ModelRelation(Base):
    __tablename__ = 'ModelRelation'
    row_id = Column(Integer, primary_key=True)
    name = Column(String(64))


class Model(Base):
    __tablename__ = 'Model'
    row_id = Column(Integer, primary_key=True)
    relation_id = Column(Integer, ForeignKey('ModelRelation.row_id'))

    relation = relationship('ModelRelation')

    @hybrid_property
    def value(self):
        return self.relation.name

    @value.expression
    def value(cls):
        return select([ModelRelation.name]).where(ModelRelation.row_id==cls.relation_id)

    @hybrid_property
    def value_scalar(self):
        return self.relation.name

    @value_scalar.expression
    def value_scalar(cls):
        return select([ModelRelation.name]).where(ModelRelation.row_id==cls.relation_id).as_scalar()


Base.metadata.create_all()

if __name__ == '__main__':
    session = Session()

    script1 = Model(relation=ModelRelation(name='value1'))
    session.add(script1)
    script2 = Model(relation=ModelRelation(name='value2'))
    session.add(script2)
    session.commit()

    print([i.value for i in session.query(Model).all()])
    print(session.query(Model.value).all())
    print(session.query(Model).filter(Model.value=='value1').all())
    print()
    print([i.value_scalar for i in session.query(Model).all()])
    print(session.query(Model.value_scalar).all())
    print(session.query(Model).filter(Model.value_scalar=='value1').all())

    session.close()

输出是:

[u'value1', u'value2']
[(u'value1',), (u'value2',)]
[]

[u'value1', u'value2']
[(u'value1',)]
[<__main__.Model object at 0x041D5C90>]

【问题讨论】:

  • 我认为每个@hybrid_property 不应该有多个表达式,并且您不直接调用表达式方法,仍然查询@hybrid_property 属性。例如。删除value_scalar()方法,这个session.query(Model.value).all()返回[('value1',), ('value2',)]session.query(Model).filter(Model.value == "value1").all()返回[&lt;__main__.Model object at 0x0000022578989E08&gt;]。没有写答案,因为不确定我是否正确理解了您的问题,但我之前没有看到单个混合属性具有多种表达方法的模式。
  • 这是.filter(Model.value_not_scalar == "value1")查询生成的sql:SELECT "Model".row_id AS "Model_row_id", "Model".name AS "Model_name" FROM "Model" WHERE 0 = 1。请注意末尾的 WHERE 0 = 1 部分,因此结果为空。
  • 我通常不会对一个属性执行两个表达式,我只是发现它给出了相同的结果。当我清理我的代码时,我并没有意识到删除关系将允许hybrid_property 自动生成表达式,但我的实际问题是关系不会发生这种情况。虽然再次添加了关系(我将更新问题中的代码),但如果不专门定义表达式,它将无法工作。

标签: python sqlalchemy


【解决方案1】:

您看到的可变性是由于表达式返回的对象类型以及表达式的使用位置。

没有as_scalar()

您的表达式返回一个Select 对象。

session.query(Model.value).all() 中,您的表达式被传递给session.query()the docs 可以接受:

一系列实体和/或 SQL 表达式。

...所以没关系。我们可以通过这个简单的查询来证明:

print(session.query(select([1])).all())  # [(1,)]

在第二个查询 session.query(Model).filter(Model.value == "value1").all() 中,您现在使用相等比较左侧的 Select,然后将该比较的结果传递给 query.filter()。 SQLAlchemy 通过重载Column 上的__eq__() 方法使用丰富的比较来比较列(like)元素,您可以自己查看:

print(Column.__eq__)  # <function ColumnOperators.__eq__ at 0x000001F851FB11F8>

但是你的表达式返回一个Select 对象:

print(Select.__eq__)  # <slot wrapper '__eq__' of 'object' objects>
# which is just the same __eq__ method that every python object has, defined on object
print(Select.__eq__ is object.__eq__)  # True

既然我们知道Select.__eq__() 方法没有被重载,那么在Select 对象和字符串之间进行任何== 比较的结果是什么?始终False。当我们将False 作为唯一过滤器传递给查询时会发生什么?:

print(session.query(Model).filter(False).all())
# SELECT "Model".row_id AS "Model_row_id", "Model".relation_id AS "Model_relation_id" FROM "Model" WHERE 0 = 1

WHERE 0 = 1 始终评估为 false,因此查询始终为空。

as_scalar()

来自the docsSelect.as_scalar()

返回这个可选的“标量”表示,可以使用 作为列表达式。

通常,选择语句在其列中只有一列 子句有资格用作标量表达式。

返回的对象是 ScalarSelect 的一个实例。

所以在这个scanario中,表达式返回一个ScalarSelect对象,可以像列一​​样对待。

首先,解决.filter(Model.value_scalar=='value1')查询行为之间的差异:

print(ScalarSelect.__eq__ is Column.__eq__)  # True

ScalarSelect 具有与Column 相同的__eq__() 实现,这意味着在Query.filter() 的上下文中,相等性测试会产生一些有意义的东西:

print(Model.value_scalar == "value1")
# (SELECT "ModelRelation".name FROM "ModelRelation", "Model" WHERE "ModelRelation".row_id = "Model".relation_id) = :param_1

因此,在这种情况下,查询会产生合理的结果。

但是,在session.query(Model.value_scalar).all() 的情况下,它只返回一个值,即使表中有两行。

这个查询生成的sql是这样的:

SELECT (SELECT "ModelRelation".name
FROM "ModelRelation", "Model"
WHERE "ModelRelation".row_id = "Model".relation_id) AS anon_1

由于ScalarSelect 被解释为一列,它本身是被选择的,而不是被从中选择的,就像在没有as_scalar() 的情况下一样。为什么SELECT (SELECT...) AS anon_1 只从查询中返回一行,这有点超出我的理解,但我可以告诉你它发生在数据库级别,不是 sqlalchemy 处理结果并且出于某种原因只返回一个。这将通过原始 dbapi 连接执行相同的查询:

with Engine.connect() as conn:
    cxn = conn.connection
    cursor = cxn.cursor()
    cursor.execute("""
        SELECT (SELECT "ModelRelation".name
        FROM "ModelRelation", "Model"
        WHERE "ModelRelation".row_id = "Model".relation_id) AS anon_1
    """)
    print(cursor.fetchall())  # [('value1',)]

因此,当表达式返回 Column 时,您似乎会得到最一致的行为。

文档中有一个关于Join Dependent Hybrid Relationships 的部分,它只使用相关对象列作为表达式值,但您需要在查询中提供连接。

如果模型是:

class Model(Base):
    __tablename__ = "Model"
    row_id = Column(Integer, primary_key=True)
    relation_id = Column(Integer, ForeignKey("ModelRelation.row_id"))

    relation = relationship("ModelRelation")

    @hybrid_property
    def value(self):
        return self.relation.name

    @value.expression
    def value(cls):
        return ModelRelation.name

此查询:session.query(Model.value).all() 呈现为

SELECT "ModelRelation".name AS "ModelRelation_name" FROM "ModelRelation"

...并按预期返回[('value1',), ('value2',)]

但是这个查询:session.query(Model).filter(Model.value == "value1").all() 呈现为:

SELECT "Model".row_id AS "Model_row_id", "Model".relation_id AS "Model_relation_id" 
FROM "Model", "ModelRelation" 
WHERE "ModelRelation".name = ?

...但即使我们已经过滤了值,也会返回两行:[&lt;__main__.Model object at 0x000002060369FEC8&gt;, &lt;__main__.Model object at 0x000002060348B108&gt;]

在这部分文档中,他们正在处理名为 UserSavingsAccount 的模型,他们说:

但是,在表达式级别,预计 User 类 将在适当的上下文中使用,以便适当的连接 到 SavingsAccount 将出现

所以如果我们查询session.query(Model).join(ModelRelation).filter(Model.value == "value1").all(),渲染的查询变成:

SELECT "Model".row_id AS "Model_row_id", "Model".relation_id AS "Model_relation_id" 
FROM "Model" JOIN "ModelRelation" 
ON "ModelRelation".row_id = "Model".relation_id 
WHERE "ModelRelation".name = ?

...并返回正确的 1 结果:[&lt;__main__.Model object at 0x000001606F030D48&gt;]

文档继续描述另一个示例 Correlated Subquery Relationship Hybrid,但我发现当 select() 是查询的目标实体时,它具有与上述完全相同的限制,因为它只返回一个结果。

【讨论】:

  • 啊,非常感谢您提供非常详细的回复,了解它为什么会这样工作很有用。我很高兴你找到了解决这个特定问题的方法,尽管一般表达式的限制确实有点可惜。
  • 我想知道是否有一种方法可以让 ScalarSelect 等。 al 可能被专门作为查询的目标来处理,但我今天读到了一个有趣的引述,例如,“软件中让我们感到沮丧的限制很可能是由于我们不理解的限制”。我敢打赌,这里也是如此。
  • 哈哈,这可能是真的。我想这只是SQLAlchemy 通常非常直观且易于使用,但突然之间你会遇到这样的情况,其中具有更好控制的解决方案不能按预期工作,而工作解决方案(在这种情况下,返回 @如果您需要做一些更复杂的事情,987654375@) 感觉有点受限:)
  • 与开源一样,未来掌握在您的手中。有一个邮件列表,您可能会在其中找到更好的解释,解释为什么这些东西表现得像现在这样,如果确实可以做得更好,那将是一个发起改变的好地方。 groups.google.com/forum/m/#!forum/sqlalchemy
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-21
  • 2017-09-12
  • 2018-10-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多