为啥在提交对象之前 SQLAlchemy 默认列值不可用?

Posted

技术标签:

【中文标题】为啥在提交对象之前 SQLAlchemy 默认列值不可用?【英文标题】:Why isn't SQLAlchemy default column value available before object is committed?为什么在提交对象之前 SQLAlchemy 默认列值不可用? 【发布时间】:2012-12-09 18:30:26 【问题描述】:

最近我发现 SQLAlchemy 的 Column 默认值不能像我预期的那样工作:

>>> Base = declarative_base()
>>> class TestModel(Base):
    ...     __tablename__ = 'tmodel'
...     id = sa.Column(sa.Integer, primary_key=True)
...     foo = sa.Column(sa.Integer, default=0)
...    
>>> tmodel_instance = TestModel()
>>> print tmodel_instance.foo
None
>>> session.add(tmodel_instance)
>>> print tmodel_instance.foo
None
>>> session.commit()
>>> print tmodel_instance.foo
0

我希望tmodel_instance.foo在对象实例化后立即等于0,但似乎只有在执行INSERT命令时才使用默认值,这让我很困惑。为什么人们更喜欢default 而不是server_default?以及如何实现我想要的?我应该在__init__ 中指定所有默认参数吗?这似乎是代码重复:要更改默认值,我必须更改两次并保持这些值相等——有什么方法可以避免这种情况吗?

【问题讨论】:

+1 同意。持久化一个对象的行为会改变它的属性是没有意义的。 【参考方案1】:

出于以下四个原因之一,人们更喜欢默认而不是服务器默认:

    您希望运行一个 Python 函数,而不是 SQL 函数,作为默认值(或者一个还需要一些每个 INSERT Python 状态的 SQL 表达式)。

    默认是主键列的一部分。 ORM 无法在没有主键的情况下加载行,因此在使用 ORM 时,server_default 通常对于 PK 列没有用处。

    数据库不支持您要运行的 SQL 表达式作为“服务器默认值”。

    您正在处理无法/不想更改的架构。

在这种情况下,当您希望应用程序中的“foo”为“0”而与数据库操作无关时,可以选择:

    使用__init__()。是蟒蛇!

    使用事件。

这里是__init__()

class TestModel(Base):
   # ...

   def __init__(self):
       self.foo = 0

这里是事件(特别是init event):

from sqlalchemy import event

@event.listens_for(Foo, "init")
def init(target, args, kwargs):
    target.foo = 0

【讨论】:

但是为什么 sqlAlchemy 模型是这样设计的:创建后没有默认值而不是默认值?这肯定会让习惯于其他 ORM 库的人感到困惑 “默认”通常是 SQL 表达式或完整的服务器端值。在 INSERT 发生之前无法知道它,或者更不理想的是,它是否在创建对象时笨拙地预先执行(这本身会降低值成为真正的“INSERT 默认值”)。因此,仅使核心级插入默认值的一个子集的行为完全不同是不一致的。 虽然session.flush(model)session.refresh(model) 仍然会产生None 值,但很奇怪,因为刷新会话会生成主键...【参考方案2】:

您可以使用来自sqlalchemy_utilsforce_instant_defaults 侦听器来更改此行为:

from sqlalchemy_utils import force_instant_defaults

force_instant_defaults()

class TestModel(Base):
    __tablename__ = 'tmodel'
    id = sa.Column(sa.Integer, primary_key=True)
    foo = sa.Column(sa.Integer, default=0)

model = TestModel()
assert model.foo == 0

【讨论】:

【参考方案3】:

您可以使用init 事件来填充默认值。这个事件监听器会这样做:

from sqlalchemy import event
from sqlalchemy.orm import mapper
from sqlalchemy.inspection import inspect


def instant_defaults_listener(target, args, kwargs):
    for key, column in inspect(target.__class__).columns.items():
        if column.default is not None:
            if callable(column.default.arg):
                setattr(target, key, column.default.arg(target))
            else:
                setattr(target, key, column.default.arg)


event.listen(mapper, 'init', instant_defaults_listener)

【讨论】:

这工作了很长时间,但现在我得到一个错误:sqlalchemy Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with * has an attribute '_supports_population' mapper。 Krassowski's answer 为我工作。

以上是关于为啥在提交对象之前 SQLAlchemy 默认列值不可用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 sqlalchemy 没有正确设置默认值?

在 SQLAlchemy 中,如何在提交之前预览 SQL 语句以进行调试?

SQLAlchemy列值基于另一个

sqlalchemy,使用反向包含(不在)子列值列表中进行选择

Sqlalchemy 使用 map func 返回列值

Django ORM和SQLAlchemy类比