Python 2.7 SQLAlchemy次要连接模型在奇怪的条件下失败

Python 2.7 SQLAlchemy次要连接模型在奇怪的条件下失败,python-2.7,model,sqlalchemy,Python 2.7,Model,Sqlalchemy,我有一个我根本无法解决的奇怪问题。从本质上讲,我有一个模型和系统,除了在一组非常具体(而且似乎武断)的情况下,它可以完美地工作 我会在一秒钟内粘贴模型,但这里的想法。我希望对某些表进行版本控制。这意味着对于一个给定的表,我将其分为两个表,一个是具有对象的自然键的主控部分,另一个是具有可能更改的所有关联数据的版本表。然后我的一些模型当然有一个关系,所以我创建了一个连接表来链接版本 parent_conds = [ getattr(search_parent.__class__, name)

我有一个我根本无法解决的奇怪问题。从本质上讲,我有一个模型和系统,除了在一组非常具体(而且似乎武断)的情况下,它可以完美地工作

我会在一秒钟内粘贴模型,但这里的想法。我希望对某些表进行版本控制。这意味着对于一个给定的表,我将其分为两个表,一个是具有对象的自然键的主控部分,另一个是具有可能更改的所有关联数据的版本表。然后我的一些模型当然有一个关系,所以我创建了一个连接表来链接版本

parent_conds = [
    getattr(search_parent.__class__, name) == getattr(search_parent, name)
    for name, column in search_parent.__class__.__mapper__.columns.items()
    if not column.primary_key
]

parent_match = session.query(Parent).filter(*parent_conds).first()

# We are going to make a new version
parent_match.current_version.active=False
parent_match.versions.append(search_parent.current_version)

for search_child in search_parent.children[:]:

    search_child.parent_id = parent_match.id

    search_conds = [
        getattr(search_child.__class__, name) == getattr(search_child, name)
        for name, column in search_child.__class__.__mapper__.columns.items()
        if not column.primary_key
    ]

    child_match = session.query(Child).filter(*search_conds).first()

    if child_match.current_version != search_child.current_version:
        # create a new version: deactivate the old one, insert the new
        child_match.current_version.active=False
        child_match.versions.append(search_child.current_version)

    else:
        # copy the old version to point to the new parent version
        children = parent_match.current_version.children

        children.append(child_match.current_version)
        children.remove(search_child.current_version)
        session.expunge(search_child.current_version)

    session.expunge(search_child)

session.expunge(search_parent)

session.add(parent_match)

session.commit()
以下是模型:

class Versioned(object):

    def __init__(self, **kwargs):

        super(Versioned, self).__init__(**kwargs)

        self.active = True
        self.created_on = datetime.datetime.now()

    active = Column(BOOLEAN)
    created_on = Column(TIMESTAMP, server_default=func.now())

    def __eq__(self, other):

        return self.__class__ == other.__class__ and \
            all([getattr(self, key) == getattr(other, key)
                for key in self.comparison_keys
                ])

    def __ne__(self, other):

        return not self.__eq__(other)

    comparison_keys = []

class Parent(Base):

    __tablename__ = 'parent'

    id = Column(INTEGER, primary_key=True)

    name = Column(TEXT)

    versions = relationship("ParentVersion", back_populates="master")

    children = relationship("Child", back_populates="parent")

    @property
    def current_version(self):
        active_versions = [v for v in self.versions if v.active==True]

        return active_versions[0] if active_versions else None

class ParentVersion(Versioned, Base):

    __tablename__ = 'parent_version'

    id = Column(INTEGER, primary_key=True)

    master_id = Column(INTEGER, ForeignKey(Parent.id))

    address = Column(TEXT)

    master = relationship("Parent", back_populates="versions")

    children = relationship("ChildVersion",
        secondary=lambda : Parent_Child.__table__
    )

class Child(Base):

    __tablename__ = 'child'

    id = Column(INTEGER, primary_key=True)

    parent_id = Column(INTEGER, ForeignKey(Parent.id))

    name = Column(TEXT)

    versions = relationship("ChildVersion", back_populates="master")

    parent = relationship("Parent", back_populates="children")

    @property
    def current_version(self):
        active_versions = [v for v in self.versions if v.active==True]

        return active_versions[0] if active_versions else None


class ChildVersion(Versioned, Base):

    __tablename__ = 'child_version'

    id = Column(INTEGER, primary_key=True)

    master_id = Column(INTEGER, ForeignKey(Child.id))

    age = Column(INTEGER)

    fav_toy = Column(TEXT)

    master = relationship("Child", back_populates="versions")

    parents = relationship("ParentVersion",
        secondary=lambda: Parent_Child.__table__,
    )

    comparison_keys = [
        'age',
        'fav_toy',
    ]

class Parent_Child(Base):

    __tablename__ = 'parent_child'

    id = Column(INTEGER, primary_key=True)

    parent_id = Column(INTEGER, ForeignKey(ParentVersion.id))
    child_id = Column(INTEGER, ForeignKey(ChildVersion.id))
好的,我知道最近的SQLAlchemy模型对版本控制有一些想法,可能是我做得不对。但这非常适合我的用例。所以,请幽默我,让我们假设模型是好的(一般来说,如果有一个小细节导致了错误,那么最好修复)

现在假设我想插入数据。我有一些来源的数据,我把它吸收进来并建立模型。即,将内容拆分为主/版本,分配子关系,分配版本关系。现在,我想将其与数据库中已有的数据进行比较。对于每个主对象,如果我找到它,我会比较版本。如果版本不同,则创建新版本。棘手的是,如果子版本不同,我想插入一个新的父版本,并更新其所有关系。也许代码解释这一部分更有意义
search\u parent
是我在预解析阶段创建的对象。它有一个版本和子对象,子对象也有版本

parent_conds = [
    getattr(search_parent.__class__, name) == getattr(search_parent, name)
    for name, column in search_parent.__class__.__mapper__.columns.items()
    if not column.primary_key
]

parent_match = session.query(Parent).filter(*parent_conds).first()

# We are going to make a new version
parent_match.current_version.active=False
parent_match.versions.append(search_parent.current_version)

for search_child in search_parent.children[:]:

    search_child.parent_id = parent_match.id

    search_conds = [
        getattr(search_child.__class__, name) == getattr(search_child, name)
        for name, column in search_child.__class__.__mapper__.columns.items()
        if not column.primary_key
    ]

    child_match = session.query(Child).filter(*search_conds).first()

    if child_match.current_version != search_child.current_version:
        # create a new version: deactivate the old one, insert the new
        child_match.current_version.active=False
        child_match.versions.append(search_child.current_version)

    else:
        # copy the old version to point to the new parent version
        children = parent_match.current_version.children

        children.append(child_match.current_version)
        children.remove(search_child.current_version)
        session.expunge(search_child.current_version)

    session.expunge(search_child)

session.expunge(search_parent)

session.add(parent_match)

session.commit()
好吧,再一次,这可能不是完美的,甚至不是最好的方法。但它确实有效。除了,这是我无法理解的。如果我要将孩子的年龄属性更新为整数值零,那么它将不起作用。如果子对象从0岁开始,我将其更改为其他对象,则效果非常好。如果从某个非零整数开始,并将年龄更新为0,则会收到以下警告:

SAWarning: Object of type <ChildVersion> not in session, add operation   along 'ParentVersion.children' won't proceed (mapperutil.state_class_str(child), operation, self.prop))
SAWarning:类型的对象不在会话中,沿“ParentVersion.children”添加操作将不会继续(mapperutil.state\u class\u str(child)、operation、self.prop))
将插入更新的版本,但不会插入到父\子联接表中。并不是它失败了,而是SQLAlchemy已经确定子对象不存在并且无法创建连接。但它确实存在,我知道它会被插入

同样,只有在插入年龄为0的新版本时才会发生这种情况。如果我在其他年龄段插入一个新版本,这完全符合我的要求

关于这个bug还有其他一些奇怪的事情——如果你没有插入足够的子元素,它就不会发生(似乎大约有12个触发了这个bug),有时候它也不会发生,这取决于其他属性。我想我还不完全明白是什么引起了它

感谢您抽出时间阅读本文。我有一个完整的工作演示与源数据完整,我很乐意分享,它只是需要一些设置,所以我不知道它是否适合在这篇文章。我希望有人能想到该看什么,因为在这一点上,我已经完全出局了

编辑:这是导致警告的完整堆栈跟踪

  File "repro.py", line 313, in <module>
  load_data(session, second_run)
File "repro.py", line 293, in load_data
  session.commit()
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 801, in commit
  self.transaction.commit()
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 392, in commit
  self._prepare_impl()
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 372, in _prepare_impl
  self.session.flush()
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2019, in flush
  self._flush(objects)
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2101, in _flush
  flush_context.execute()
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 373, in execute
  rec.execute(self)
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 487, in execute
  self.dependency_processor.process_saves(uow, states)
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/dependency.py", line 1053, in process_saves
  False, uowcommit, "add"):
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/dependency.py", line 1154, in _synchronize
  (mapperutil.state_class_str(child), operation, self.prop))
File "/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/util/langhelpers.py", line 1297, in warn
  warnings.warn(msg, exc.SAWarning, stacklevel=2)
File "repro.py", line 10, in warn_with_traceback
  traceback.print_stack()
/Users/me/virtualenvs/dev/lib/python2.7/site-packages/sqlalchemy/orm/dependency.py:1154: SAWarning: Object of type <ChildVersion> not in session, add operation along 'ParentVersion.children' won't proceed
(mapperutil.state_class_str(child), operation, self.prop))
文件“repo.py”,第313行,在
加载\u数据(会话,第二次\u运行)
load_数据中第293行的文件“repo.py”
session.commit()
提交中的文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/session.py”,第801行
self.transaction.commit()
提交中的文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/session.py”,第392行
self.\u prepare\u impl()
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/session.py”,第372行,在
self.session.flush()
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/session.py”,第2019行,刷新
自冲洗(对象)
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/session.py”,第2101行,在\u flush中
flush_context.execute()
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/unitofwork.py”,第373行,在execute中
rec.execute(self)
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/unitofwork.py”,第487行,在execute中
自相关性\u处理器。进程\u保存(uow,状态)
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/dependency.py”,第1053行,进程中保存
False,uowcommit,“添加”):
文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/dependency.py”,第1154行,在
(mapperutil.state\u class\u str(子级)、操作、self.prop)
警告中的文件“/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/util/langhelpers.py”,第1297行
警告。警告(消息,exc.SAWarning,堆栈级别=2)
文件“repo.py”,第10行,带回溯的警告
traceback.print_stack()
/Users/me/virtualenvs/dev/lib/python2.7/site packages/sqlalchemy/orm/dependency.py:1154:SAWarning:类型的对象不在会话中,沿“ParentVersion.children”添加操作将不会继续
(mapperutil.state\u class\u str(子级)、操作、self.prop)
编辑2: 下面是一个python文件的要点,您可以运行它来查看奇怪的行为。
出现此错误的原因是您无意中将对象添加到会话中

以下是MVCE:

engine = create_engine("sqlite://", echo=False)


def get_data():
    children = [
        Child(name="Carol", versions=[ChildVersion(age=0, fav_toy="med")]),
        Child(name="Timmy", versions=[ChildVersion(age=0, fav_toy="med")]),
    ]
    return Parent(
        name="Zane", children=children,
        versions=[
            ParentVersion(
                address="123 Fake St",
                children=[v for child in children for v in child.versions]
            )
        ]
    )


def main():
    Base.metadata.create_all(engine)

    session = Session(engine)
    parent_match = get_data()
    session.add(parent_match)
    session.commit()

    with session.no_autoflush:
        search_parent = get_data()

        parent_match.versions.append(search_parent.current_version)
        for search_child in search_parent.children[:]:
            child_match = next(c for c in parent_match.children if c.name == search_child.name)

            if child_match.current_version != search_child.current_version:
                child_match.versions.append(search_child.current_version)
            else:
                session.expunge(search_child.current_version)

            session.expunge(search_child)

        session.expunge(search_parent)
        session.commit()
旁白:这是您需要在问题本身中提供的内容。提供带有说明的tarball并不是获得答案的最佳方式

线路

parent_match.versions.append(search_parent.current_version)
不仅添加了
search\u parent.当前版本
,还添加了
search\u parent
,这反过来又添加了所有相关对象,包括其他子对象的子版本。根据您后来删除其他相关对象以防止它们被添加到会话的事实判断,我认为