使父对象不出现在before_flush事件侦听器的session.dirty中

Gre*_*0ry 5 python sqlalchemy

我一直在玩SQLAlchemy,发现我无法可靠地跟踪数据库中的更改内容.

我创建了一个例子来解释我的担忧:

import re
import datetime

from sqlalchemy import create_engine

from sqlalchemy.ext.declarative import (
    declarative_base,
    declared_attr,
    )

from sqlalchemy import (
    create_engine,
    event,
    Column,
    Boolean,
    Integer,
    String,
    Unicode,
    DateTime,
    Index,
    ForeignKey,
    CheckConstraint,
    )

from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    Session,
    relationship,
    backref,
    )

import transaction

from zope.sqlalchemy import ZopeTransactionExtension

class ExtendedSession(Session):
    my_var = None

DBSession = scoped_session(
    sessionmaker(extension=ZopeTransactionExtension(),
        class_=ExtendedSession
        )
    )

class BaseModel(object):
    query = DBSession.query_property()

    id = Column(
        Integer,
        primary_key=True,
        )

    @declared_attr
    def __tablename__(cls):
        class_name = re.sub(r"([A-Z])", r"_\1", cls.__name__).lower()[1:]
        return "{0}".format(
            class_name,
            )

Base = declarative_base(cls=BaseModel)

def initialize_sql(engine):
    DBSession.configure(bind=engine)
    Base.metadata.bind = engine

engine = create_engine("sqlite://")
initialize_sql(engine)

class Parent(Base):
    # *** Columns
    col1 = Column (
        String,
        nullable=False,
        )
    # *** Relationships
    # *** Methods
    def __repr__(self):
        return "<Parent(id: '{0}', col1: '{1}')>".format(
            self.id,\
            self.col1,\
            )

class Child(Base):
    # *** Columns
    col1 = Column (
        String,
        nullable=False,
        )
    parent_id = Column (
        Integer,
        ForeignKey (
            Parent.id,
            ondelete="CASCADE",
            ),
        nullable=False,
        )
    # *** Relationships
    parent = relationship (
        Parent,
        backref=backref(
            "child_elements",
            uselist=True,
            cascade="save-update, delete",
            lazy="dynamic",
            ),
        # If below is uncommented then instance of Parent won't appear in session.dirty
        # However this relationship will never be loaded (even if needed)
        #lazy="noload",
        )
    # *** Methods
    def __repr__(self):
        return "<Child(id: '{0}', col1: '{1}', parent_id: '{2}')>".format(
            self.id,\
            self.col1,\
            self.parent_id,\
            )

@event.listens_for(DBSession, 'before_flush')
def before_flush(session, flush_context, instances):
    time_stamp = datetime.datetime.utcnow()

    if session.new:
        for elem in session.new:
            print(" ### NEW {0}".format(repr(elem)))

    if session.dirty:
        for elem in session.dirty:
            print(" ### DIRTY {0}".format(repr(elem)))

    if session.deleted:
        for elem in session.deleted:
            print(" ### DELETED {0}".format(repr(elem)))

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

with transaction.manager:
    parent = Parent(col1="parent")
    DBSession.add(parent)
    DBSession.flush()

    # Below loop is to demonstrate that
    # each time child object is created and linked to parent
    # parent is also marked as modified
    # how to avoid that?
    # or optionally is it possible to detect this in before_flush event
    # without issuing additional SQL query?
    for i in range(0, 10):
        parent=Parent.query.filter(Parent.col1 == "parent").first()
        child = Child(col1="{0}".format(i))
        child.parent = parent
        DBSession.add(child)
        DBSession.flush()

    # Below update will not cause associated instance of Parent appearing in session.dirty
    child = Child.query.filter(Child.col1=="3").first()
    child.col1="updated"
    DBSession.add(child)
    DBSession.flush()
Run Code Online (Sandbox Code Playgroud)

简而言之 - 有两个对象:

  • 孩子 - 与父母联系

每次我添加Child的新实例并将其与Parent的实例链接,Parent的实例也出现在before_flush事件的session.dirty中.

SQLAlchemy社区建议这种行为是预期的(虽然我认为必须有一个选项来改变默认行为 - 我在doco中找不到它)

所以这是我的问题:是否可以配置关系,当我添加一个新的Child实例并将其链接到Parent的实例时,那个Parent的实例将不会出现在session.dirty中?

我已经尝试过设置关系,lazy="noload"因为我可能需要使用该关系(因此我可能需要加载它),因此它不是一个选项.

我也接受一个解决方案,它允许我检测到before_load事件处理程序中没有更改Parent - 但是我不想触发额外的查询来实现这一点.

非常感谢你的帮助,

格雷格

Gre*_*0ry 4

经过几个小时的研究和 SQLAlchemy 社区的提示,我找到了似乎按我需要的方式工作的解决方案(注意session.dirty块中的附加条件)。

@event.listens_for(DBSession, 'before_flush')
def before_flush(session, flush_context, instances):
    time_stamp = datetime.datetime.utcnow()

    if session.new:
        for elem in session.new:
            print(" ### NEW {0}".format(repr(elem)))

    if session.dirty:
        for elem in session.dirty:
            # Below check was added to solve the problem
            if ( session.is_modified(elem, include_collections=False) ):
                print(" ### DIRTY {0}".format(repr(elem)))

    if session.deleted:
        for elem in session.deleted:
            print(" ### DELETED {0}".format(repr(elem)))
Run Code Online (Sandbox Code Playgroud)

与我的解决方案相关的文档可以在这里找到:http://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.session.Session.is_modified

简而言之 - 指定include_collections=Falseinsidesession.is_modified会使 SQLAlchemy 忽略多值集合已更改的情况(在我的情况下,如果子项发生更改,则父项将被该附加检查过滤掉)。