Here's a helper function that accommodates for the object being expired:
from sqlalchemy.orm import exc from sqlalchemy import inspect def safe_is_persistent(obj): state = inspect(obj) if state.expired: try: state.mapper.primary_key_from_instance(obj) except exc.ObjectDeletedError: return False return state.persistent On Fri, Aug 24, 2018 at 9:56 AM, Mike Bayer <mike...@zzzcomputing.com> wrote: > On Fri, Aug 24, 2018 at 3:12 AM, Chris Wilson > <chris.wil...@cantabcapital.com> wrote: >> Dear Michael, >> >> >> >> I have discovered that under very particular circumstances, SQLAlchemy >> forgets that an object was newly created in a transaction, and if that >> transaction rolls back, the object’s state is not reset from persistent to >> transient. > > > right off I see it is attempting to use SAVEPOINT with SQLite which > won't work unless you apply: > http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#pysqlite-serializable. > but that shouldn't affect the ORM's sense of if the object is > persistent or not unless it is retrieved fresh. > > your example doesn't cut and paste cleanly but after looking for a bit > the problem is that because you are losing the identity of fido after > create() is done, you then ask it to fetch fido fresh into memory all > over again, then roll back the transaction, but the ORM has no idea > that this object was part of this transaction because you let it get > lost. the ORM doesn't track states that are garbage collected because > it is just as likely the program is running millions of states through > the transaction in batches and holding onto them when the program has > decided it doesn't care about them anymore would constitute a memory > leak. > > The .persistent attribute does not incur a refresh, however if you > access any of the normal attributes on the object like .id, *and* > you've also put the SAVEPOINT, you'll get: "Instance '<A at > 0x7f0651cc2f98>' has been deleted, or its row is otherwise not > present.". > >> >> >> >> It seems to rely on the following: >> >> >> >> · Open an outer transaction >> >> · Open an inner transaction, add object to session, close transaction >> to commit. The object is now persistent. >> >> · Clear all references to the object, allowing it to be GC’ed and >> removed from the session._new weakref map >> >> · Load the object from the database again. >> >> · Raise an exception to rollback the outer transaction. >> >> · SQLAlchemy does not realise that the object is new, and fails to >> reset its state. >> >> · Object is still “persistent”, but no longer in the database. >> >> >> >> Here is an example that reproduces the problem: >> >> >> >> from sqlalchemy import Column, Integer, Text, create_engine, inspect >> >> from sqlalchemy.ext.declarative import declarative_base >> >> from sqlalchemy.orm import sessionmaker >> >> >> >> Base = declarative_base() >> >> >> >> class Dog(Base): >> >> __tablename__ = 'dog' >> >> >> >> id = Column(Integer, primary_key=True) >> >> name = Column(Text) >> >> >> >> def create(): >> >> global session >> >> with session.begin(nested=True): >> >> fido = Dog(name="fido") >> >> session.add(fido) >> >> assert not inspect(fido).persistent, "should not yet be persisted to >> database" >> >> assert inspect(fido).persistent, "should now be persisted to database, >> but transaction not yet committed" >> >> >> >> def fetch(): >> >> global session >> >> global fido >> >> fido = session.query(Dog).filter_by(name="fido").one() >> >> assert inspect(fido).persistent, "should have been retrieved from the >> database, therefore persistent" >> >> >> >> def main(): >> >> engine = create_engine('sqlite://') >> >> engine.echo = True >> >> Base.metadata.create_all(engine) >> >> >> >> DBSession = sessionmaker(bind=engine) >> >> >> >> global session >> >> session = DBSession(autocommit=True) >> >> >> >> try: >> >> with session.begin(subtransactions=True) as trans: >> >> create() >> >> print(dict(trans._new)) >> >> fetch() >> >> print(dict(trans._new)) >> >> raise Exception("force transaction to roll back") >> >> except Exception: >> >> pass >> >> >> >> global fido >> >> assert not inspect(fido).persistent, "after rollback, should no longer >> be in the database" >> >> >> >> if __name__ == '__main__': >> >> main() >> >> >> >> Which fails the last assertion: >> >> >> >> AssertionError: after rollback, should no longer be in the database >> >> >> >> I know that this is a minor issue, but I found it quite confusing while >> debugging. >> >> >> >> I suspect that we might need to keep a strongly referenced list of “newly >> added” PKs in the outer session, instead of using _new. >> >> >> >> Thanks, Chris. >> >> >> >> >> >> >> >> >> ________________________________ >> This email is confidential. If you are not the intended recipient, please >> advise us immediately and delete this message. The registered name of >> Cantab- part of GAM Systematic is Cantab Capital Partners LLP. See - >> http://www.gam.com/en/Legal/Email+disclosures+EU for further information on >> confidentiality, the risks of non-secure electronic communication, and >> certain disclosures which we are required to make in accordance with >> applicable legislation and regulations. If you cannot access this link, >> please notify us by reply message and we will send the contents to you. >> >> GAM Holding AG and its subsidiaries (Cantab – GAM Systematic) will collect >> and use information about you in the course of your interactions with us. >> Full details about the data types we collect and what we use this for and >> your related rights is set out in our online privacy policy at >> https://www.gam.com/en/legal/privacy-policy. Please familiarise yourself >> with this policy and check it from time to time for updates as it >> supplements this notice >> ________________________________ >> >> -- >> SQLAlchemy - >> The Python SQL Toolkit and Object Relational Mapper >> >> http://www.sqlalchemy.org/ >> >> To post example code, please provide an MCVE: Minimal, Complete, and >> Verifiable Example. See http://stackoverflow.com/help/mcve for a full >> description. >> --- >> 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 https://groups.google.com/group/sqlalchemy. >> For more options, visit https://groups.google.com/d/optout. -- SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper http://www.sqlalchemy.org/ To post example code, please provide an MCVE: Minimal, Complete, and Verifiable Example. See http://stackoverflow.com/help/mcve for a full description. --- 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 https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.