SQLAlchemy: Using sessionmaker as contextmanger forces to expunge manually - python

I'm using SQLAlchemy and trying to use the sessionmaker as a context manager for my transaction (following the documentation):
Session = sessionmaker(some_engine)
with Session.begin() as session:
query = session.query(SomeTable)
an_entry = query.one()
# session.expunge_all() # Fixes the exception
an_entry.attribute
Which raises an sqlalchemy.orm.exc.DetachedInstanceError: Instance <AnEntry at 0x7f0a9d1c2560> is not bound to a Session exception. This can be solved by expunging inside the contextmanager (see the line commented out above).
I'm surprised by this behavior, as the SQLAlchemy documentation and code indicate that closing a session should automatically expunges all ORM objects. And indeed, the following equivalent of the above works, without me having to manually expunge:
Session = sessionmaker(some_engine)
session = Session()
with session.begin():
query = session.query(SomeTable)
an_entry = query.one()
an_entry.attribute
Can anyone explain this behavior?

In the second, "working" example, the context manager is the SessionTransaction object returned by session.begin, not the session itself. The session is not closed before accessing the attribute and therefore there is no DetachedInstanceError: the instance is never expunged.
A better comparison might be why this code does not raise an error:
with Session() as s:
an_entry = s.query(SomeTable).one()
an_entry.attribute
The reason is that it does not commit. By default, ORM objects are expired when a commit occurs, so a subsequent attribute access requires a query to get the value. In the case of with Session.begin() as session:, the session is committed on leaving the with block, so accessing the attribute requires a query, which requires that the object be attached to a session.
Session and sessionmaker both accept an expire_on_commit boolean keyword argument to control this behaviour.

Related

Instance is not bound to a Session

Just like many other people I run into the problem of an instance not being bound to a Session. I have read the SQLAlchemy docs and the top-10 questions here on SO. Unfortunatly I have not found an explanation or solution to my error.
My guess is that the commit() closes the session rendering the object unbound. Does this mean that In need to create two objects, one to use with SQL Alchemy and one to use with the rest of my code?
I create an object something like this:
class Mod(Base):
__tablename__ = 'mod'
insert_timestamp = Column(DateTime, default=datetime.datetime.now())
name = Column(String, nullable=False)
Then I add it to the database using this function and afterwards the object is usesless, I cannot do anything with it anymore, always getting the error that it is not bound to a session. I have tried to keep the session open, keep it closed, copy the object, open two sessions, return the object, return a copy of the object.
def add(self, dataobjects: list[Base]) -> bool:
s = self.sessionmaker()
try:
s.add_all(dataobjects)
except TypeError:
s.rollback()
raise
else:
s.commit()
return True
This is my session setup:
self.engine = create_engine(f"sqlite:///{self.config.database['file']}")
self.sessionmaker = sessionmaker(bind=self.engine)
Base.metadata.bind = self.engine
My last resort would be to create every object twice, once for SQL Alchemy and once so I can actually use the object in my code. This defeats the purpose of SQL Alchemy for me.
ORM entities are expired when committed. By default, if an entity attribute is accessed after expiry, the session emits a SELECT to get the current value from the database. In this case, the session only exists in the scope of the add method, so subsequent attribute access raises the "not bound to a session" error.
There are a few ways to approach this:
Pass expire _on_commit=False when creating the session. This will prevent automatic expiry so attribute values will remain accessible after leaving the add function, but they may be stale.
Create the session outside of the add function and pass it as an argument (or return it from add, though that's rather ugly). As long as the session is not garbage collected the entities remain bound to it.
Create a new session and do mod = session.merge(mod) for each entity to bind it to the new session.
Which option you choose depends on your application.

sqlalchemy: Why create a sessionmaker before assign it to a Session object?

Why I always need to do that in 2 steps in SqlAlchemy?
import sqlalchemy as sa
import sqlalchemy.orm as orm
engine = sa.create_engine(<dbPath>, echo=True)
Session = orm.sessionmaker(bind=engine)
my_session = Session()
Why I cannot do it in one shot like (it's could be more simple, no?) :
import sqlalchemy as sa
import sqlalchemy.orm as orm
engine = sa.create_engine(<dbPath>, echo=True)
Session = orm.Session(bind=engine)
The reason sessionmaker() exists is so that the various "configurational" arguments it requires only need to be set up in one place, instead of repeating "bind=engine, autoflush=False, expire_on_commit=False", etc. over and over again. Additionally, sessionmaker() provides an "updateable" interface, such that you can set it up somewhere in your application:
session = sessionmaker(expire_on_commit=False)
but then later, when you know what database you're talking to, you can add configuration to it:
session.configure(bind=create_engine("some engine"))
It also serves as a "callable" to pass to the very common scoped_session() construct:
session = scoped_session(sessionmaker(bind=engine))
With all of that said, these are just conventions that the documentation refers to so that a consistent "how to use" story is presented. There's no reason you can't use the constructor directly if that is more convenient, and I use the Session() constructor all the time. It's just that in a non-trivial application, you will probably end up sticking that constructor call to Session() inside some kind of callable function anyway, sessionmaker() serves as a default for that callable.
In the most general sense, the Session establishes all conversations with the database and represents a “holding zone” for all the objects which you’ve loaded or associated with it during its lifespan. It provides the entrypoint to acquire a Query object, which sends queries to the database using the Session object’s current database connection, populating result rows into objects that are then stored in the Session, inside a structure called the Identity Map - a data structure that maintains unique copies of each object, where “unique” means “only one object with a particular primary key”.
Try to pprint and see whats inside;
import pprint
pprint.pprint(my_session)
Here's the rest of the story: http://docs.sqlalchemy.org/ru/latest/orm/session.html

Parent instance is not bound to a Session; lazy load operation of attribute ’account’ cannot proceed

While trying to do the following operation:
for line in blines:
line.account = get_customer(line.AccountCode)
I am getting an error while trying to assign a value to line.account:
DetachedInstanceError: Parent instance <SunLedgerA at 0x16eda4d0> is not bound to a Session; lazy load operation of attribute 'account' cannot proceed
Am I doing something wrong??
"detached" means you're dealing with an ORM object that is not associated with a Session. The Session is the gateway to the relational database, so anytime you refer to attributes on the mapped object, the ORM will sometimes need to go back to the database to get the current value of that attribute. In general, you should only work with "attached" objects - "detached" is a temporary state used for caching and for moving objects between sessions.
See Quickie Intro to Object States, then probably read the rest of that document too ;).
I had the same problem with Celery. Adding lazy='subquery' to relationship solved my problem.
I encountered this type of DetachedInstanceError when I prematurely close the query session (that is, having code to deal with those SQLAlchemy model objects AFTER the session is closed). So that's one clue to double check no session closure until you absolutely don't need interact with model objects, I.E. some Lazy Loaded model attributes etc.
I had the same problem when unittesting.
The solution was to call everything within the "with" context:
with self.app.test_client() as c:
res = c.post('my_url/test', data=XYZ, content_type='application/json')
Then it worked.
Adding the lazy attribute didn't work for me.
To access the attribute connected to other table, you should call it within session.
#contextmanager
def get_db_session(engine):
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = SessionLocal()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()
with get_db_session(engine) as sess:
data = sess.query(Groups).all()
# `group_users` is connected to other table
print([x.group_users for x in data]) # sucess
print([x.group_users for x in data]) # fail

When should I be calling flush() on SQLAlchemy?

I'm new to SQLAlchemy and have inherited a somewhat messy codebase without access to the original author.
The code is litered with calls to DBSession.flush(), seemingly any time the author wanted to make sure data was being saved. At first I was just following patterns I saw in this code, but as I'm reading docs, it seems this is unnecessary - that autoflushing should be in place. Additionally, I've gotten into a few cases with AJAX calls that generate the error "InvalidRequestError: Session is already flushing".
Under what scenarios would I legitimately want to keep a call to flush()?
This is a Pyramid app, and SQLAlchemy is being setup with:
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension(), expire_on_commit=False))
Base = declarative_base()
The ZopeTransactionExtension on the DBSession in conjunction with the pyramid_tm being active on your project will handle all commits for you. The situations where you need to flush are:
You want to create a new object and get back the primary key.
DBSession.add(obj)
DBSession.flush()
log.info('look, my new object got primary key %d', obj.id)
You want to try to execute some SQL in a savepoint and rollback if it fails without invalidating the entire transaction.
sp = transaction.savepoint()
try:
foo = Foo()
foo.id = 5
DBSession.add(foo)
DBSession.flush()
except IntegrityError:
log.error('something already has id 5!!')
sp.rollback()
In all other cases involving the ORM, the transaction will be aborted for you upon exception, or committed upon success automatically by pyramid_tm. If you execute raw SQL, you will need to execute transaction.commit() yourself or mark the session as dirty via zope.sqlalchemy.mark_changed(DBSession) otherwise there is no way for the ZTE to know the session has changed.
Also you should leave expire_on_commit at the default of True unless you have a really good reason.

SQLAlchemy, get object not bound to a Session

I am trying to get an collection of objects out of a database and pass it to another process that is not connected to the database. My code looks like the one below but I keep getting:
sqlalchemy.exc.UnboundExecutionError: Instance <MyClass at 0x8db7fec> is not bound to a Session; attribute refresh operation cannot proceed
When I try to look at the elements of my list outside of the get_list() method.
def get_list (obj):
sesson = Session()
lst = session.query(MyClass).all()
session.close()
return lst
However, if I use this:
def get_list_bis (obj)
session = Session()
return session.query(MyClass).all()
I am able to use the elements but worry about the state of the session since it was not closed.
What am I missing here?
If you want a bunch of objects produced by querying a session to be usable outside the scope of the session, you need to expunge them for the session.
In your first function example, you will need to add a line:
session.expunge_all()
before
session.close()
More generally, let's say the session is not closed right away, like in the first example. Perhaps this is a session that is kept active during entire duration of a web request or something like that. In such cases, you don't want to do expunge_all. You will want to be more surgical:
for item in lst:
session.expunge(item)
This often happens due to objects being in expired state, objects get expired for example after committing, then when such expired objects are about to get used the ORM tries to refresh them, but this cannot be done when objects are detached from session (e.g. because that session was closed). This behavior can be managed by creating session with expire_on_commit=False param.
>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.expired
True # then it will be refreshed...
In my case, I was saving a related entity as well, and this recipe helped me to refresh all instances within a session, leveraging the fact that Session is iterable:
map(session.refresh, iter(session)) # call refresh() on every instance
This is extremely ineffective, but works. Should be fine for unit-tests.
Final note: in Python3 map() is a generator and won't do anything. Use real loops of list comprehensions

Categories