Zope/SQLAlchemy Transaction Integration
zope.sqlalchemy provides minimal integration between SQLAlchemy and Zope's transaction management system. It acts as a data manager, unifying various approaches to allow SQLAlchemy ORM sessions to participate in Zope transactions, often used in web frameworks like Pyramid. The library is currently at version 4.1, released in November 2025, and maintains an active release schedule.
Common errors
-
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed (for example, due to a delete or rollback) or validation is pending
cause Attempting to access an ORM object after the session it was loaded from has been closed or invalidated, typically after a `transaction.commit()` or `transaction.abort()`.fixReload the object within a new session context or configure `zope.sqlalchemy.register` with `keep_session=True` (use with caution in web contexts). Alternatively, only access object attributes that were loaded before the session was detached. -
ImportError: cannot import name 'ZopeTransactionExtension' from 'zope.sqlalchemy'
cause `ZopeTransactionExtension` was renamed to `ZopeTransactionEvents` in `zope.sqlalchemy` v1.2, and direct use of `extension` in `sessionmaker` is deprecated in favor of `register()`.fixUpdate your code to use `from zope.sqlalchemy import register` and then call `register(DBSession)` after creating your `scoped_session` factory. If you *must* use the class directly for compatibility, import `ZopeTransactionEvents` instead. -
Connection busy issue / The transaction is inactive due to a rollback in a subtransaction
cause Can occur in multi-threaded environments or complex transaction scenarios (e.g., with `Products.SQLAlchemyDA` or intricate nested transactions) where database connections or sessions are not properly isolated or released, leading to contention or premature invalidation.fixEnsure proper session scoping, especially `scoped_session` and `DBSession.remove()` at the end of each request/thread. Review transaction logic for any manual `session.commit()` calls within implicitly managed Zope transactions. Consider database pooling settings. For `session.merge()` issues, explicitly call `session.expire_all()` or configure `register(DBSession, initial_state='changed')` to ensure state is reloaded. -
sqlalchemy.exc.IntegrityError: (IntegrityError) NOT NULL constraint failed: user.name [SQL: INSERT INTO user DEFAULT VALUES]
cause This is a standard SQLAlchemy integrity error, but sometimes in Zope/Pyramid contexts, the traceback or error handling can be obscured if the transaction manager isn't set up to propagate exceptions cleanly.fixEnsure your SQLAlchemy operations are wrapped in `try...except` blocks that catch `IntegrityError` (and other `sqlalchemy.exc` errors) and explicitly call `transaction.abort()` to roll back the Zope transaction. Verify the data being committed adheres to database constraints.
Warnings
- breaking As of `zope.sqlalchemy` v3.0, you can no longer call `session.commit()` directly within a manual nested database transaction (i.e., when using `session.begin_nested()`). You must commit the savepoint returned by `session.begin_nested()` or use it as a context manager.
- breaking In `zope.sqlalchemy` v1.2, `ZopeTransactionExtension` was renamed to `ZopeTransactionEvents`. The recommended integration pattern also shifted from passing an `extension` argument to `sessionmaker` to using the `register()` function on the session factory.
- gotcha By default, `zope.sqlalchemy` often closes sessions after `transaction.commit()`. Accessing ORM-managed objects after a commit without reloading them can lead to `DetachedInstanceError`.
- gotcha When performing direct SQL write operations (not through the ORM), `zope.sqlalchemy` cannot automatically detect that the session has changed. This can lead to uncommitted changes if `transaction.commit()` is called without the session being marked as 'changed'.
- gotcha SQLAlchemy versions 2.0.32 through 2.0.35 are known to cause deadlocks when running tests on Python 3.11+. These versions are explicitly excluded from `zope.sqlalchemy`'s supported versions.
Install
-
pip install zope.sqlalchemy
Imports
- register
from zope.sqlalchemy import register
- ZopeTransactionExtension
from zope.sqlalchemy import ZopeTransactionExtension
from zope.sqlalchemy import ZopeTransactionEvents
Quickstart
import os
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from zope.sqlalchemy import register
import transaction
# 1. Define a simple SQLAlchemy model
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
def __repr__(self):
return f"<User(id={self.id}, name='{self.name}')>"
# 2. Create an engine (using an in-memory SQLite for example)
# Use an environment variable for a real database DSN, e.g., POSTGRES_DSN='postgresql://user:pass@host/db'
database_url = os.environ.get('TEST_DSN', 'sqlite:///:memory:')
engine = create_engine(database_url)
# 3. Create tables (only if they don't exist)
Base.metadata.create_all(engine)
# 4. Create a SQLAlchemy session factory and scope it
# Scoped sessions are important for web applications or multi-threaded environments
Session = sessionmaker(bind=engine)
DBSession = scoped_session(Session)
# 5. Register the scoped session factory with zope.sqlalchemy
# This integrates its commits/rollbacks with Zope transactions
register(DBSession)
# --- Usage Example ---
# Get a session from the factory (it's thread-local/scoped)
session = DBSession()
try:
# Add a new user
new_user = User(name='Alice')
session.add(new_user)
# The transaction manager handles committing/rolling back the session
transaction.commit()
print(f"Added user: {new_user}")
# After commit, the session might be closed/invalidated by default
# Attempting to access new_user might raise DetachedInstanceError if not reloaded
# print(new_user.name) # This might fail if session is closed.
except Exception as e:
transaction.abort()
print(f"Transaction aborted due to error: {e}")
finally:
# Ensure the session is removed from the scope, especially in web requests
DBSession.remove()
# Start a new transaction for another operation
session = DBSession()
try:
# Query users
users = session.query(User).all()
print(f"Current users: {users}")
transaction.commit()
except Exception as e:
transaction.abort()
print(f"Query transaction aborted: {e}")
finally:
DBSession.remove()