SQLAlchemy Session and Database Restore: Identity Map Serving Stale Rows After Backup Reload
0 reputation · 06 Jul 2025, 01:04 UTC
0 reputation · 06 Jul 2025, 01:04 UTC
We are designing a workflow where a database snapshot is restored into a running application that uses SQLAlchemy (2.0 assumed, though 1.4 compatibility is a concern). After the restore completes, the same process continues querying through an existing Session, and we are unsure how the session's identity map interacts with the freshly restored rows.
Our concern is the boundary between the ORM's in-memory state and the restored database state. Since the identity map keys objects by primary key, rows that existed before the restore may still be returned from the session's cache rather than reflecting the restored data. We also want to avoid situations where re-adding restored rows triggers duplicate key errors because detached or stale objects are still tracked.
We have seen references to Session.expunge() and Session.expunge_all() as ways to detach objects, and to creating a fresh session after restore, but the documented guarantees around which approach reliably forces queries to hit the database again are not clear to us, especially given behavior differences noted between 1.4 and 2.0.
Specifically:
Session.get() in 2.0 consult the identity map in a way that could return pre-restore state?After a database snapshot is restored, calling Session.expunge_all() on the existing session is sufficient to make subsequent queries return the restored rows, provided there are no pending transactions or uncommitted changes in that session. If a transaction is active, it is safer to discard the session and create a new one.
SQLAlchemy's identity map stores ORM instances keyed by their primary key. Session.get() (both 1.4 and 2.0) first looks in this map; if an instance is found it is returned without issuing SQL. Therefore, any object that existed before the restore will be returned from the cache unless it has been removed from the map. expunge_all() removes all instances from the identity map, turning them into detached objects. The next query for a given primary key will miss the map, trigger a SELECT, and construct a fresh instance from the restored row.
Session.get() always checks the identity map before issuing SQL (the lookup logic is unchanged between 1.4 and 2.0).Session.expunge_all() clears the map; after it returns, the session holds no strong references to the expunged instances.Session after the restore also yields a clean identity map, giving the same result as expunging when no transaction is active.session.expunge_all().session = SessionMaker() (or your scoped session factory).# Arrange: populate DB, load object into session
obj_before = session.get(MyModel, pk)
assert obj_before.attr == 'old_value'
# Act: restore backup into the same database (outside SQLAlchemy)
restore_backup() # your backup/restore procedure
# Verify stale cache returns old data if not expunged
obj_stale = session.get(MyModel, pk)
assert obj_stale.attr == 'old_value' # still from identity map
# Expunge all
session.expunge_all()
# Act: query again
obj_fresh = session.get(MyModel, pk)
assert obj_fresh.attr == 'restored_value' # reflects backup data
# Optional: check that identity changed
assert obj_fresh._sa_instance_state.identity_key != obj_before._sa_instance_state.identity_keyIf you observe that obj_fresh.attr still equals the old value, the session likely still holds a reference (e.g., you kept obj_before elsewhere) or there is an uncommitted transaction interfering with the expunge. In that case, create a new session and repeat the query.
Do you have any pending transactions or uncommitted changes in the session at the moment the backup is restored? Knowing this determines whether a simple expunge_all() is safe or whether you must replace the session entirely.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.