在更新/插入时修改其他对象

Kra*_*ang 5 python sqlalchemy

我有两个映射对象,父对象和子对象。

class Parent(Base):
    __tablename__ = 'parent'
    id = ...
    name = ...
    date_modified = Column(SA_DateTime, default=DateTime.now,
                           onupdate=DateTime.now, nullable=False)

class Child(Base):
    __tablename__ = 'child'
    id = ...
    name = ...
    date_modified = Column(SA_DateTime, default=DateTime.now,
                           onupdate=DateTime.now, nullable=False)
    parent = relationship(Parent, backref='parent')
Run Code Online (Sandbox Code Playgroud)

当孩子更新时,我不仅Child.date_modified要更改,还要更改Child.parent.date_modified.

我试图这样做:

@event.listens_for(Child, 'after_update')
def modified_listener(mapper, connection, target):
    if object_session(target).is_modified(target, include_collections=False):
        target.parent.date_modified = DateTime.now()
Run Code Online (Sandbox Code Playgroud)

但这不起作用,因为我已经处于冲洗状态并且我得到了类似的东西

SAWarning: Attribute history events accumulated on 1 previously clean instance within inner-flush event handlers have been reset, and will not result in database updates. Consider using set_committed_value() within inner-flush event handlers to avoid this warning.
Run Code Online (Sandbox Code Playgroud)

如何使用 SQLAlchemy 解决此问题?

Ilj*_*ilä 9

使用SQLAlchemy 事件的基本 update-parent-when-child-changes在此处此处之前已在此站点上进行了介绍,但在您的情况下,您正在尝试在刷新期间更新父级,可能使用来自子级的更新默认值,这将在更新后可见,或者完全是新值。在事件处理程序中修改父对象并不像您最初想象的那么简单:

警告

映射器级别的刷新事件只允许非常有限的操作,仅在被操作的行的本地属性上,以及允许在给定的Connection. 请完整阅读Mapper-level Events 中的注释以获取有关使用这些方法的指南;通常,该SessionEvents.before_flush()方法应首选用于一般的冲洗变化。

正如你所注意到的,简单

target.parent.date_modified = DateTime.now()
Run Code Online (Sandbox Code Playgroud)

在您的事件处理程序中警告:

SAWarning:在内部刷新事件处理程序中的 1 个先前干净的实例上累积的属性历史事件已被重置,并且不会导致数据库更新。考虑在内部刷新事件处理程序中使用 set_committed_value() 来避免此警告。

set_committed_value() 允许设置没有历史事件的属性,就好像设置值是原始加载状态的一部分。

您还注意到,在 after update 事件处理程序中接收目标并不能保证实际发出 UPDATE 语句:

对于标记为“脏”的所有实例,即使是那些没有对其基于列的属性进行净更改并且没有继续执行 UPDATE 语句的实例,也会调用此方法。

要检测对象上基于列的属性是否有净更改,从而导致 UPDATE 语句,请使用object_session(instance).is_modified(instance, include_collections=False).

因此,解决方案可能是使用事件目标中保存的信息使用给定的连接在父表上发出 UPDATE 语句,然后检查会话中是否存在父对象并设置它的提交值:

from sqlalchemy import event
from sqlalchemy.orm.attributes import set_committed_value
from sqlalchemy.orm.session import object_session

@event.listens_for(Child, 'after_update')                
def receive_child_after_update(mapper, connection, target):
    session = object_session(target)                       

    if not session.is_modified(target, include_collections=False):
        return                                                    

    new_date_modified = target.date_modified

    # Avoid touching the target.parent relationship attribute and
    # copy the date_modified value from the child to parent.
    # Warning: this will overwrite possible other updates to parent's
    # date_modified.
    connection.execute(
        Parent.__table__.
        update().        
        values(date_modified=new_date_modified).
        where(Parent.id == target.parent_id))

    parent_key = session.identity_key(Parent, target.parent_id)

    try:
        the_parent = session.identity_map[parent_key]

    except KeyError:
        pass

    else:
        # If the parent object is present in the session, update its
        # date_modified attribute **in Python only**, to reflect the
        # updated DB state local to this transaction.
        set_committed_value(
            the_parent, 'date_modified', new_date_modified)
Run Code Online (Sandbox Code Playgroud)


小智 0

你有没有尝试过:

@event.listens_for(Child, 'after_update')
def modified_listener(mapper, connection, target):
    if object_session(target).is_modified(target, include_collections=False):
        sa.orm.attributes.set_committed_value(
            target.parent,
            'date_modified',
            DateTime.now())
Run Code Online (Sandbox Code Playgroud)

按照:

http://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.attributes.set_comfilled_value

  • 使用 set_commited_value() 实际上不会将值保留在数据库中,相反会取消所有以前存在的历史记录。 (2认同)