【问题标题】:How to implement SQL level expression for this hybrid property in SQLAlchemy?如何在 SQLAlchemy 中为此混合属性实现 SQL 级别表达式?
【发布时间】:2017-02-10 23:11:37
【问题描述】:

我有一个分类帐表和一个相应的 python 类。 我使用SQLAlchemy定义了模型,如下,

class Ledger(Base):
    __tablename__ = 'ledger'

    currency_exchange_rate_lookup = {('CNY', 'CAD'): 0.2}

    amount = Column(Numeric(10, 2), nullable=False)
    currency = Column(String, nullable=False)
    payment_method = Column(String)
    notes = Column(UnicodeText)

    @hybrid_property
    def amountInCAD(self):
        if self.currency == 'CAD':
            return self.amount
        exchange_rate = self.currency_exchange_rate_lookup[(self.currency, 'CAD')]
        CAD_value = self.amount * Decimal(exchange_rate)
        CAD_value = round(CAD_value, 2)
        return CAD_value

    @amountInCAD.expression
    def amountInCAD(cls):
        amount = cls.__table__.c.amount
        currency_name = cls.__table__.c.currency
        exchange_rate = cls.currency_exchange_rate_lookup[(currency_name, 'CAD')]
        return case([
            (cls.currency == 'CAD', amount),
        ], else_ = round((amount * Decimal(exchange_rate)),2))

现在如您所见,我想创建一个名为“amountInCAD”的混合属性。 Python 级别的 getter 似乎工作正常。但是 SQL 表达式不起作用。

现在,如果我运行这样的查询:

>>>db_session.query(Ledger).filter(Ledger.amountInCAD > 1000)

SQLAlchemy 给了我这个错误:

  File "ledger_db.py", line 43, in amountInCAD
    exchange_rate = cls.currency_exchange_rate_lookup[(currency_name, 'CAD')]
KeyError: (Column('currency', String(), table=<ledger>, nullable=False), 'CAD')

我研究了 SQLAlchemy 关于混合属性的在线文档。 http://docs.sqlalchemy.org/en/latest/orm/mapped_sql_expr.html#using-a-hybrid 将我的代码与示例代码进行比较,我不明白为什么我的代码不起作用。如果在官方示例中,cls.firstname 可以引用一列值,为什么在我的代码中 cls.__table__.c.currency 只返回 Column 而不是它的值?

【问题讨论】:

    标签: python sqlalchemy descriptor


    【解决方案1】:

    cls.firstname 不是“引用值”,而是 Columnexample 中的 cls.firstname + " " + cls.lastname 生成一个字符串连接 SQL 表达式,如下所示:

    firstname || ' ' || lastname
    

    这是混合属性的一部分魔力:它们使编写可以在两个域中工作的简单表达式变得相对容易,但是您仍然需要了解何时处理 python 实例和构建 SQL 表达式。

    您可以重新考虑一下自己的混合,并在case 表达式中将转换选项实际传递给数据库:

    from sqlalchemy import func
    
    ...
    
    @amountInCAD.expression
    def amountInCAD(cls):
        # This builds a list of (predicate, expression) tuples for case. The
        # predicates compare each row's `currency` column against the bound
        # `from_` currencies in SQL.
        exchange_rates = [(cls.currency == from_,
                           # Note that this does not call python's round, but
                           # creates an SQL function expression. It also does not
                           # perform a multiplication, but produces an SQL expression
                           # `amount * :rate`. Not quite sure
                           # why you had the Decimal conversion, so kept it.
                           func.round(cls.amount * Decimal(rate), 2))
                          for (from_, to_), rate in
                          cls.currency_exchange_rate_lookup.items()
                          # Include only conversions to 'CAD'
                          if to_ == 'CAD']
        return case(exchange_rates +  [
            # The default for 'CAD'
            (cls.currency == 'CAD', cls.amount),
        ])
    

    通过这种方式,您可以有效地将汇率查询作为 CASE 表达式传递给 SQL。

    【讨论】:

    • @llja Everilä 谢谢。顺便说一句,如何在 SQL 表达式中使用 Decimal?我需要确保它是小数而不是浮点数。
    • 当然值应该是Decimal 开始。如果你之前有浮点数,那么你可能已经有一个不正确的值:Decimal(0.3) -> Decimal('0.299999999...')。您的 DB 列还应使用固定精度类型等。您的 DB-API 应该按原样正确处理 python Decimal 类型,如果您只是将一个作为绑定参数,但请检查。 DB-API 是 SQLAlchemy 在下面使用的,例如用于 Postgresql 的 psycopg2(常用)。
    • @llja Everilä 我正在使用 SQLite,不幸的是它在数据库后端不支持 Decimal。你能建议一个解决方法吗?仅在 SQL 级别。我已经为 python 级别使用了装饰类型。谢谢。
    • 抱歉,完全忘记了你在使用 SQLite。正如您所指出的,SQLite 不支持固定精度的数值以及对它们的任何数学运算。即使您将 Decimal 作为绑定参数传递,根据您的类型亲和性和转换首选项的顺序,它也会使用 INTEGERREAL 作为存储类。
    • 为了保持“固定精度”,您也许可以使用整数美分等,并自己对它们进行所需的数学和转换,但这将是一项艰巨的任务,但并非不可能。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-04
    • 1970-01-01
    • 2013-01-08
    • 1970-01-01
    相关资源
    最近更新 更多