SQLAlchemy 关联表(关联对象模式)引发 IntegrityError

Posted

技术标签:

【中文标题】SQLAlchemy 关联表(关联对象模式)引发 IntegrityError【英文标题】:SQLAlchemy association table (association object pattern) raises IntegrityError 【发布时间】:2013-10-01 15:51:51 【问题描述】:

我正在使用 SQLAlchemy 版本 0.8.2(尝试过 python 2.7.5 和 3.3.2)

我不得不在我的代码中使用关联对象模式(用于多对多关系),但是每当我添加关联时,它都会引发 IntegrityError 异常。这是因为它没有执行“INSERT INTO association (left_id, right_id, extra_data) [...]”,而是执行“INSERT INTO association (right_id, extra_data) [...]”,这将引发 IntegrityError异常,因为它缺少主键。

在尝试缩小问题范围并尽可能简化代码后,我找到了罪魁祸首,但我不明白为什么会这样。

我包含了我的完整代码,以便读者可以按原样进行测试。类声明与documentation 中的完全相同(带有反向引用)。

#!/usr/bin/env python2
import sqlalchemy
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, backref


Base = declarative_base()

class Association(Base):
    __tablename__ = 'association'
    left_id = Column(Integer, ForeignKey('left.id'), primary_key=True)
    right_id = Column(Integer, ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child", backref="parent_assocs")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)

    children = relationship("Association", backref="parent")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)



def main():
    engine = sqlalchemy.create_engine('sqlite:///:memory:', echo=True)
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    # populate old data
    session.add(Child()) 

    # new data
    p = Parent()
    session.add(p) # Commenting this fixes the error. 
    session.flush()

    # rest of new data
    a = Association(extra_data="some data")
    a.child = session.query(Child).one()
    # a.child = Child() # Using this instead of the above line avoids the error - but that's not what I want. 
    p.children.append(a)
    # a.parent = p # Using this instead of the above line fixes the error! They're logically equivalent. 

    session.add(p)
    session.commit()

if __name__ == '__main__':
    main()

因此,正如上面代码中的 cmets 所述,有三种方法可以解决/避免该问题。

    在声明关联之前不要将父级添加到会话中 为关联创建一个新子代,而不是选择一个已经存在的子代。 在关联上使用 backref

我不了解所有三种情况的行为。

第二种情况有所不同,所以它不是一个可能的解决方案。但是,我不理解这种行为,并且希望能解释为什么在这种情况下可以避免该问题。

我认为第一种情况可能与“Object States”有关,但我也不知道具体是什么原因造成的。哦,在第一次出现session.add(p) 之前添加session.autoflush=False 也解决了增加我困惑的问题。

对于第三种情况,我完全空白,因为它们在逻辑上应该是等价的。

感谢您的任何见解!

【问题讨论】:

【参考方案1】:

这里发生的情况是,当您调用p.children.append() 时,SQLAlchemy 无法在不先加载它的情况下附加到普通集合。当它加载时,自动刷新启动 - 你知道这一点,因为在你的堆栈跟踪中你会看到这样的一行:

File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1183, in _autoflush
  self.flush()

然后您的关联对象在此处以不完整状态刷新;它首先在会话中,因为当您说 a.child = some_persistent_child 时,一个事件会将 a 附加到 Childparent_assocs 集合中,然后 级联 Association 对象到会话中(有关这方面的一些背景信息和一种可能的解决方案,请参阅 Controlling Cascade on Backrefs。

但在不影响任何关系的情况下,当您遇到这种先有鸡还是先有蛋的问题时,最简单的解决方案是使用 no_autoflush 暂时禁用自动刷新:

with session.no_autoflush:
    p.children.append(a)

通过在加载 p.children 时禁用自动刷新,您的待处理对象 a 不会被刷新;然后它与已经持久的Parent 相关联(因为您已经添加并刷新了它)并准备好插入。

这可以让您的测试程序成功。

【讨论】:

以上是关于SQLAlchemy 关联表(关联对象模式)引发 IntegrityError的主要内容,如果未能解决你的问题,请参考以下文章

SQLAlchemy 通过关联对象声明性多对多自联接

SQLAlchemy 使用关联配置与自我的多对多关系

Flask/SQLAlchemy - 多对多关系的关联模型和关联表之间的区别?

具有多态实体的子类关联表的 SQLAlchemy 设置

sqlalchemy操作----多表关联

SQLALCHEMY 删除关联对象不会从父子节点中删除