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

时间:2013-10-01 15:51:51

标签: python orm sqlalchemy

我正在使用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异常,因为它缺少主键。

在尝试缩小问题一段时间并尽可能简化代码之后,我找到了罪魁祸首(s?),但我不明白为什么它会以这种方式表现。

我包含了我的完整代码,因此读者可以按原样进行测试。类声明与documentation(带backrefs)完全相同。

#!/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()

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

  1. 在声明关联之前,不要将父级添加到会话
  2. 为关联创建一个新子项,而不是选择已存在的子项。
  3. 在关联中使用backref
  4. 我不明白这三种情况的行为。

    第二种情况有所不同,所以这不是一种可能的解决方案。然而,我不理解这种行为,并且会理解为什么在这种情况下避免问题的原因。

    我认为第一个案例可能与“Object States”有关,但我不确切地知道究竟是什么导致了它。哦,在第一次出现session.autoflush=False之前添加session.add(p)也解决了问题,这增加了我的困惑。

    对于第三种情况,我正在绘制一个完整的空白,因为它们在逻辑上应该是等价的。

    感谢您的任何见解!

1 个答案:

答案 0 :(得分:3)

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

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

然后,您的关联对象在此处以不完整状态刷新;它首先出现在会话中,因为当您说a.child = some_persistent_child时,事件会将a追加到parent_assocs Child的{​​{1}}集合,然后级联 Association对象进入会话(有关此问题的背景知识,请参阅Controlling Cascade on Backrefs),以及一种可能的解决方案。

但是在不影响任何关系的情况下,当您遇到这种鸡/蛋类问题时,最简单的解决方案是使用no_autoflush临时禁用autoflush:

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

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

这使您的测试程序能够成功。