The following issue, which puzzled for several days, exists in several 
versions. I verified that it applies to version 1.0.9

In my understanding, the post_update flag is required on a relationship 
between to classes A and B if there is another relationship between B and A 
in order to break a circular dependency that can result from adding new 
instances in both classes that are mutually related. post_update=True means 
that the foreign key for the relationship is set in a separate UPDATE query 
after completion of the INSERT queries. For reasons that are not clear to 
me, a separate UPDATE query for the foreign is also issued for normal 
updates of the containing instance.

However, there is a particular case in which the UPDATE of the foreign is 
left out: When the relationship property is set to an instance without 
having been accessed in the session and is then set to Null, then required 
UPDATE query for the foreign key is not issued.

Please consider the test case below with a Container class and an Element 
class. There are relationships Element.container to Container and 
Container.topElement to Element. The latter is defined with the post_update 
flag. A container with two elements is created. Then the following steps 
are taken, to rule out any interference in separate sessions:

1. Set the container's topElement to one of the elements. This works 
normally.
2. Set the container's topElement to None without prior read access to the 
topElement property. This does not work.
3. Repeat the second step, but this time after accessing the topElement 
property. Then the update works.

Any thoughts on this issue? I wonder whether the post_update behavior 
should be limited to cases where actually new instances are inserted 
because only those can lead to circular dependencies.

Thanks,

Thorsten



from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.orm import Session, relationship, backref
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from contextlib import contextmanager



Base = declarative_base()


class Container(Base):
    __tablename__ = 'container'
    id = Column(Integer, primary_key=True)
    topElementID = Column(Integer, ForeignKey('element.id', 
name='fk_topelement', use_alter=True))
    name = Column(String(50), nullable=False)

    topElement = relationship("Element", 
        post_update=True,
        primaryjoin='Container.topElementID==remote(Element.id)')
                    
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "Container(name=%r, id=%r, topElementID=%r)" % (
                    self.name,
                    self.id,
                    self.topElement
                )
                
    def dump(self):
        return repr(self) + '\n' + '\n'.join(repr(e) for e in self.elements)
                
                
class Element(Base):
    __tablename__ = 'element'
    id = Column(Integer, primary_key=True)
    containerID = Column(Integer, ForeignKey('container.id'))
    name = Column(String(50), nullable=False)

    container = relationship("Container", 
primaryjoin='Element.containerID==remote(Container.id)',
                        backref=backref("elements", 
primaryjoin='remote(Element.containerID)==Container.id'),
                    )

    def __init__(self, name, container=None):
        self.name = name
        self.container = container

    def __repr__(self):
        return "Element(name=%r, id=%r, containerID=%r)" % (
                    self.name,
                    self.id,
                    self.containerID
                )

@contextmanager
def new_session(engine):
    session = Session(engine)
    yield session
    session.commit()
    session.close()

def msg(msg, *args):
    msg = msg % args
    print("\n\n\n" + "-" * len(msg.split("\n")[0]))
    print(msg)
    print("-" * len(msg.split("\n")[0]))

def getContainer(session, cid):
    return session.query(Container).filter(Container.id==cid).one()


if __name__ == '__main__':
    # engine = 
create_engine('mysql://testdb:testpass@localhost/testdb?charset=utf8&use_unicode=0',
 
echo=True)
    engine = create_engine('sqlite://', echo=True)

    Base.metadata.create_all(engine)
    session = Session(engine)

    # Creating a container with two elements; obtaining container ID for 
subsequent retrieval
    with new_session(engine) as session:
        container = Container('My container')
        Element('a', container=container)
        Element('b', container=container)
        session.add(container)
        session.flush()
        cID = container.id

    # Setting container's topElement to the element with name b
    with new_session(engine) as session:
        container = getContainer(session, cID)
        container.topElement = next(e for e in container.elements if e.name 
== 'b')

    with new_session(engine) as session:
        msg("After setting topElement=b:\n %s", getContainer(session, 
cID).dump())


    # Setting container's topElement to None
    with new_session(engine) as session:
        container = getContainer(session, cID)
        container.topElement = None

    with new_session(engine) as session:
        msg("After first attempt to set topElement=None:\n %s", 
getContainer(session, cID).dump())
   

    # Trying again after making sure the session has accessed the prior top 
element
    with new_session(engine) as session:
        container = getContainer(session, cID)
        container.topElement
        container.topElement = None
    
    with new_session(engine) as session:
        msg("After second attempt to set topElement=None:\n %s", 
getContainer(session, cID).dump())




This is the output:

---------------------------
After setting topElement=b:
 Container(name=u'My container', id=1, topElementID=Element(name=u'b', 
id=2, containerID=1))
Element(name=u'a', id=1, containerID=1)
Element(name=u'b', id=2, containerID=1)
---------------------------
2015-12-01 15:56:48,700 INFO sqlalchemy.engine.base.Engine COMMIT
2015-12-01 15:56:48,700 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2015-12-01 15:56:48,700 INFO sqlalchemy.engine.base.Engine SELECT 
container.id AS container_id, container."topElementID" AS 
"container_topElementID", container.name AS container_name 
FROM container 
WHERE container.id = ?
2015-12-01 15:56:48,700 INFO sqlalchemy.engine.base.Engine (1,)
2015-12-01 15:56:48,701 INFO sqlalchemy.engine.base.Engine COMMIT
2015-12-01 15:56:48,701 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2015-12-01 15:56:48,702 INFO sqlalchemy.engine.base.Engine SELECT 
container.id AS container_id, container."topElementID" AS 
"container_topElementID", container.name AS container_name 
FROM container 
WHERE container.id = ?
2015-12-01 15:56:48,702 INFO sqlalchemy.engine.base.Engine (1,)
2015-12-01 15:56:48,703 INFO sqlalchemy.engine.base.Engine SELECT 
element.id AS element_id, element."containerID" AS "element_containerID", 
element.name AS element_name 
FROM element 
WHERE element.id = ?
2015-12-01 15:56:48,703 INFO sqlalchemy.engine.base.Engine (2,)
2015-12-01 15:56:48,704 INFO sqlalchemy.engine.base.Engine SELECT 
element.id AS element_id, element."containerID" AS "element_containerID", 
element.name AS element_name 
FROM element 
WHERE element."containerID" = ?
2015-12-01 15:56:48,704 INFO sqlalchemy.engine.base.Engine (1,)



-------------------------------------------
After first attempt to set topElement=None:
 Container(name=u'My container', id=1, topElementID=Element(name=u'b', 
id=2, containerID=1))
Element(name=u'a', id=1, containerID=1)
Element(name=u'b', id=2, containerID=1)
-------------------------------------------
2015-12-01 15:56:48,704 INFO sqlalchemy.engine.base.Engine COMMIT
2015-12-01 15:56:48,705 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2015-12-01 15:56:48,705 INFO sqlalchemy.engine.base.Engine SELECT 
container.id AS container_id, container."topElementID" AS 
"container_topElementID", container.name AS container_name 
FROM container 
WHERE container.id = ?
2015-12-01 15:56:48,705 INFO sqlalchemy.engine.base.Engine (1,)
2015-12-01 15:56:48,706 INFO sqlalchemy.engine.base.Engine SELECT 
element.id AS element_id, element."containerID" AS "element_containerID", 
element.name AS element_name 
FROM element 
WHERE element.id = ?
2015-12-01 15:56:48,706 INFO sqlalchemy.engine.base.Engine (2,)
2015-12-01 15:56:48,707 INFO sqlalchemy.engine.base.Engine UPDATE container 
SET "topElementID"=? WHERE container.id = ?
2015-12-01 15:56:48,707 INFO sqlalchemy.engine.base.Engine (None, 1)
2015-12-01 15:56:48,707 INFO sqlalchemy.engine.base.Engine COMMIT
2015-12-01 15:56:48,708 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2015-12-01 15:56:48,708 INFO sqlalchemy.engine.base.Engine SELECT 
container.id AS container_id, container."topElementID" AS 
"container_topElementID", container.name AS container_name 
FROM container 
WHERE container.id = ?
2015-12-01 15:56:48,708 INFO sqlalchemy.engine.base.Engine (1,)
2015-12-01 15:56:48,709 INFO sqlalchemy.engine.base.Engine SELECT 
element.id AS element_id, element."containerID" AS "element_containerID", 
element.name AS element_name 
FROM element 
WHERE element."containerID" = ?
2015-12-01 15:56:48,709 INFO sqlalchemy.engine.base.Engine (1,)



--------------------------------------------
After second attempt to set topElement=None:
 Container(name=u'My container', id=1, topElementID=None)
Element(name=u'a', id=1, containerID=1)
Element(name=u'b', id=2, containerID=1)
--------------------------------------------

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to