Skip to content

Instantly share code, notes, and snippets.

@chongkong
Last active August 29, 2016 04:53
Show Gist options
  • Save chongkong/1460884de96866d42f5a197fcec5652b to your computer and use it in GitHub Desktop.
Save chongkong/1460884de96866d42f5a197fcec5652b to your computer and use it in GitHub Desktop.
get() query does not load joinedloaded relationship when reloading expired object
from sqlalchemy import Column, BigInteger, ForeignKey, create_engine
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Session = sessionmaker()
class Foo(Base):
__tablename__ = 'foo'
id = Column(BigInteger, primary_key=True, autoincrement=True)
bars = relationship('Bar', backref='foo', lazy='joined')
class Bar(Base):
__tablename__ = 'bar'
foo_id = Column(BigInteger, ForeignKey('foo.id'), primary_key=True)
bar_id = Column(BigInteger, primary_key=True, autoincrement=False)
class Hello(Base):
__tablename__ = 'hello'
id = Column(BigInteger, primary_key=True, autoincrement=True)
def main(sqlalchemy_dabatase_uri):
engine = create_engine(sqlalchemy_dabatase_uri, echo=True)
Base.metadata.create_all(bind=engine)
Session.configure(bind=engine)
session = Session()
try:
foo = Foo()
session.add(foo)
session.commit()
hello = Hello()
session.add(hello)
session.commit()
print('----------')
session.query(Foo).get(1)
print('----------')
except:
pass
Base.metadata.drop_all(bind=engine)
if __name__ == '__main__':
main('mysql+pymysql://local:Password!2@localhost:3306/test_utf8mb4?charset=utf8mb4')
from sqlalchemy import Column, BigInteger, ForeignKey, create_engine
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Session = sessionmaker()
class Foo(Base):
__tablename__ = 'foo'
id = Column(BigInteger, primary_key=True, autoincrement=True)
bars = relationship('Bar', backref='foo', lazy='joined')
class Bar(Base):
__tablename__ = 'bar'
foo_id = Column(BigInteger, ForeignKey('foo.id'), primary_key=True)
bar_id = Column(BigInteger, primary_key=True, autoincrement=False)
def main(sqlalchemy_dabatase_uri):
engine = create_engine(sqlalchemy_dabatase_uri, echo=True)
Base.metadata.create_all(bind=engine)
Session.configure(bind=engine)
session = Session()
try:
foo = Foo()
foo.bars.append(Bar(bar_id=1))
foo.bars.append(Bar(bar_id=2))
session.add(foo)
session.commit()
print('----------')
foo.bars
print('----------')
except:
pass
Base.metadata.drop_all(bind=engine)
if __name__ == '__main__':
main('mysql+pymysql://local:Password!2@localhost:3306/test_utf8mb4?charset=utf8mb4')
@chongkong
Copy link
Author

chongkong commented Aug 29, 2016

Run result

2016-08-29 12:02:56,828 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2016-08-29 12:02:56,829 INFO sqlalchemy.engine.base.Engine SELECT foo.id AS foo_id, foo.name AS foo_name 
FROM foo 
WHERE foo.id = %(param_1)s
2016-08-29 12:02:56,829 INFO sqlalchemy.engine.base.Engine {'param_1': 1}

foo is expired after second commit. When doing query(Foo).get(1), it is expected to do joinedload, but expired object is loaded only with its own attribute (not including relationship attrs) from loader.get_from_identity()

def get_from_identity(session, key, passive):
    """Look up the given key in the given session's identity map,
    check the object for expired state if found.
    """
    instance = session.identity_map.get(key)
    if instance is not None:

        state = attributes.instance_state(instance)

        # expired - ensure it still exists
        if state.expired:
            if not passive & attributes.SQL_OK:
                # TODO: no coverage here
                return attributes.PASSIVE_NO_RESULT
            elif not passive & attributes.RELATED_OBJECT_OK:
                # this mode is used within a flush and the instance's
                # expired state will be checked soon enough, if necessary
                return instance
            try:
                state._load_expired(state, passive)
            except orm_exc.ObjectDeletedError:
                session._remove_newly_deleted([state])
                return None
        return instance
    else:
        return None

which calls state._load_expired()

    def _load_expired(self, state, passive):
        """__call__ allows the InstanceState to act as a deferred
        callable for loading expired attributes, which is also
        serializable (picklable).
        """

        if not passive & SQL_OK:
            return PASSIVE_NO_RESULT

        toload = self.expired_attributes.\
            intersection(self.unmodified)

        self.manager.deferred_scalar_loader(self, toload)

        # if the loader failed, or this
        # instance state didn't have an identity,
        # the attributes still might be in the callables
        # dict.  ensure they are removed.
        self.expired_attributes.clear()

        return ATTR_WAS_SET

when you run into debugging mode, toload local variable contains only foo.id column, but it should also include foo.bars column, as it's the relationship with joinedload option

@chongkong
Copy link
Author

sample2.py run result:

2016-08-29 13:52:51,991 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2016-08-29 13:52:51,992 INFO sqlalchemy.engine.base.Engine SELECT foo.id AS foo_id 
FROM foo 
WHERE foo.id = %(param_1)s
2016-08-29 13:52:51,992 INFO sqlalchemy.engine.base.Engine {'param_1': 2}
2016-08-29 13:52:51,993 INFO sqlalchemy.engine.base.Engine SELECT bar.foo_id AS bar_foo_id, bar.bar_id AS bar_bar_id 
FROM bar 
WHERE %(param_1)s = bar.foo_id
2016-08-29 13:52:51,993 INFO sqlalchemy.engine.base.Engine {'param_1': 2}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment