SQLAlchemy 关系追加错误

0 投票
1 回答
681 浏览
提问于 2025-04-18 05:52

我一直在用 SQLAlchemy 0.9.2 和 Python 2.7.3,遇到了一个奇怪的问题,搞不太清楚怎么回事。以下是我相关的代码:

Base = declarative_base()
class Parent(Base):
  __tablename__ = 'parents'
  __table_args__ = (UniqueConstraint('first_name', 'last_name', name='_name_constraint'),)

  id = Column(Integer, primary_key=True)
  first_name = Column(String(32), nullable=False)
  last_name = Column(String(32), nullable=False)
  children = relationship(Child, cascade='all,delete', backref='parent')

  ## Constructors and other methods ##

class Child(Base):
  __tablename__ = 'children'

  id = Column(Integer, primary_key=True)
  parent_id = Column(Integer, ForeignKey('parents.id'))
  foo = Column(String(32), nullable=False)

  ## Constructors and other methods ##

这是一些比较基础的模型。我的问题是,我想给一个已经保存在数据库里的父对象添加一个子对象,但这个子对象现在关联的是一个不在数据库里的父对象。举个例子:

database_engine = create_engine("mysql://user:password@localhost/db", echo=False)
session = scoped_session(sessionmaker(autoflush=True,autocommit=False))
p1 = Parent("Foo", "Bar")                  # Create a parent and append a child
c1 = Child("foo")
p1.children.append(c1)
session.add(p1)
session.commit()                           # This works without a problem

db_parent = session.query(Parent).first()
db_parent.children.append(Child("bar"))
session.commit()                           # This also works without a problem

p2 = Parent("Foo", "Bar")
c3 = Child("baz")
p2.children.append(c3)
db_parent = session.query(Parent).first()
db_parent.children.append(p2.children[0])
session.commit()                           # ERROR: This blows up

我收到的错误是破坏了一个完整性约束,具体是 '_name_constraint'。SQLAlchemy 告诉我,它试图插入一个信息相同的父对象。我的问题是,为什么它会试图添加一个额外的父对象呢?

到目前为止,我采取了以下步骤,但还是没有找到好的答案:

  • 我检查了 db_parent.children[2],发现它和 p1 指向的是同一个内存地址,当我把它添加到列表后。
  • 我检查了 p2.children,在添加后奇怪的是,p2 没有子对象,尽管我已经把它的子对象添加到了 db_parent。我觉得这和发生的事情有关系,但我不明白为什么会这样。

如果有人能帮忙,我会非常感激,因为我实在搞不懂这里发生了什么。如果你需要我提供更多信息,请告诉我。提前谢谢!

1 个回答

1

好的,经过一番深入研究,我觉得我找到了问题的解决办法,但我还不太明白为什么会这样。不过,我有个猜测。我发现的解决方案是在 session.commit() 之前使用 session.expunge(p2)

我开始研究 SQLAlchemy 的内部机制,特别是实例状态。我发现,一旦你把子对象添加到父对象,原始父对象的状态就会变成待处理状态。这里有个例子:

from sqlalchemy import inspect
p2             = Parent("Foo", "Bar")
p2_inst        = inspect(p2)
c3             = Child("Baz")
c3_inst        = inspect(c3)
db_parent      = session.query(Parent).first()
db_parent_inst = inspect(db_parent)
print("Pending State before append:")
print("p2_inst        : {}".format(p2_inst.pending))
print("c3_inst        : {}".format(c3_inst.pending))
print("db_parent_inst : {}".format(db_parent_inst.pending))
db_parent.children.append(p2.children[0])
print("Pending State after append:")
print("p2_inst        : {}".format(p2_inst.pending))
print("c3_inst        : {}".format(c3_inst.pending))
print("db_parent_inst : {}".format(db_parent_inst.pending))
session.expunge(p2)
print("Pending State after expunge:")
print("p2_inst        : {}".format(p2_inst.pending))
print("c3_inst        : {}".format(c3_inst.pending))
print("db_parent_inst : {}".format(db_parent_inst.pending))
session.commit()

运行这个代码的结果是:

Pending State before append:
p2_inst        : False
c3_inst        : False
db_parent_inst : False
Pending State after append:
p2_inst        : True
c3_inst        : True
db_parent_inst : False
Pending State after expunge:
p2_inst        : False
c3_inst        : True
db_parent_inst : False

就这样。一开始我想了想,这似乎是有道理的。因为你并没有对 MySQL 中的记录做任何操作,所以没有理由让 db_parent 进入“待处理”状态。我猜 p2 变成待处理状态的原因可能是操作顺序的问题?为了让 c3 变成待处理状态,它的所有关系必须存在(包括 p2),所以即使你改变了子对象的父对象,session 仍然认为它需要添加这个父对象。

如果有人对 SQLAlchemy 更了解,希望能纠正我,但就我所知,这就是我目前的最佳解释 :)

撰写回答