Question

I've been using SQLAlchemy 0.9.2 with Python Version 2.7.3 and have run into a bit of an odd problem that I can't quite seem to explain. Here is my relevant code:

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 ##

So a pretty basic set of models. The problem I'm experiencing is that I want to add a child to a parent that is saved to the database. The kicker is that the child is currently related to a parent that is not in the database. Consider the following example:

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

The error I'm receiving is that I'm breaking an integrity Constraint, namely '_name_constraint'. SQLAlchemy is telling me that is trying to insert a Parent with the same information. My question is, why in the world is it trying to add a secondary parent?

These are the steps I've taken so far and don't have a good answer for:

  • I've inspected db_parent.children[2] It points to the same memory address as p1 once I have appended it to the list
  • I've inspected p2.children after the append. Oddly, p2 has no children once I have appended its child to db_parent I think this has something to do with what is going on, I just don't understand why its happening

Any help would be much appreciated, as I simply don't understand what's going on here. If you need me to post more please let me know. Thanks in advance.

Was it helpful?

Solution

Okay, after some more digging I think I have found a solution to my problem, but I don't yet have the answer as to why its happening the way it is, but I think I may have a guess. The solution I discovered was to use session.expunge(p2) before session.commit()

I started exploring SQLAlchemy's Internals, particularly, the instance state. I found that once you add the child to the parent, the original parent's state becomes pending. Here is an example:

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()

The result of running this will be:

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

And there you have it. Once I thought about it a bit, I suppose it makes sense. There is no reason for the db_parent to ever enter a "pending" state because, you're not actually doing anything to the record in MySQL. My guess on why p2 becomes pending is due to an order of operations? In order for c3 to become pending, then all of its relationships must exist (to include p2) and so even when you change the child's parent, the session still think that it needs to add the parent.

I'd love for someone more knowledgeable on SQLAlchemy to correct me, but to the best of my knowledge, that's my best explanation :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top