如何使SQLAlchemy hybrid_property select表达式同时适用于select和filter

时间:2019-08-20 16:56:04

标签: python sqlalchemy

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

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

这是我的意思的示例,假设有两行分别称为value1value2,而namehybrid_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>]

1 个答案:

答案 0 :(得分:1)

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

as_scalar()

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

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

  

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

...这样就可以了。我们可以通过以下简单查询来证明这一点:

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

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

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始终为假,因此查询始终为空。

使用as_scalar()

the docsSelect.as_scalar()

  

返回此可选内容的“标量”表示形式,可以使用   作为列表达式。

     

通常情况下,一个select语句的列中只有一个列   子句可以用作标量表达式。

     

返回的对象是ScalarSelect的实例。

因此在此scanario中,该表达式返回一个ScalarSelect对象,该对象可以视为一列。

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

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

ScalarSelect具有与__eq__()相同的Column实现,这意味着在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 = ?

...但是即使我们已经过滤了值[<__main__.Model object at 0x000002060369FEC8>, <__main__.Model object at 0x000002060348B108>],也返回了两行。

在这部分文档中,他们正在处理称为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个结果:[<__main__.Model object at 0x000001606F030D48>]

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