I am building a small WSGI application and I am having an intermittent problem with SQLAlchemy throwing an UnboundExceptionError
.
When it happens, it seems to happen exclusively on the second request the browser makes. Refreshing the page (and all following page view attempts) run fine. It seems to only happen on the second request.
I am working with a lot of technologies that are new to me, so I am not entirely clear what I should be looking at to try and figure this out.
- CherryPyWSGIServer
- Routes
- AuthKit
- WebOb
- SQLAlchemy
- jinja2
Here are my SQLAlchemy related setup:
product_table = Table('product', metadata,
Column('productId', Integer, primary_key=True),
Column('name', String(255)),
Column('created', DateTime, default=func.now()),
Column('updated', DateTime, default=func.now(),
onupdate=func.current_timestamp()),
)
productversion_table = Table('productVersion', metadata,
Column('productVersionId', Integer, primary_key=True),
Column('productId', Integer, ForeignKey("product.productId"),
nullable=False),
Column('name', String(255)),
Column('created', DateTime, default=func.now()),
Column('updated', DateTime, default=func.now(),
onupdate=func.current_timestamp()),
)
sqlalchemy.orm.mapper(Product,
product_table,
properties={
'product_versions':
sqlalchemy.orm.relation(
ProductVersion,
backref='product',
cascade='all,delete-orphan')})
sqlalchemy.orm.mapper(ProductVersion,
productversion_table,
properties={ })
Here is my controller:
class Base(object):
def __init__(self, projenv, configuration, protected=True):
self.protected = protected
self.projenv = projenv
self.configuration = configuration
self.jinja2_env = Environment(
loader=PackageLoader('my.webapp', 'views'))
def __call__(self, environ, start_response):
if self.protected:
authkit.authorize.authorize_request(environ,
authkit.permissions.RemoteUser())
return self.handle_request_wrapper(environ, start_response)
def handle_request_wrapper(self, environ, start_response):
request = Request(environ)
response = Response()
model_and_view = self.handle_request(request, response)
if model_and_view['redirect']:
response = HTTPTemporaryRedirect(
location=model_and_view['redirect_url'])
else:
response.content_type = model_and_view['content_type']
template = self.jinja2_env.get_template(model_and_view['view'])
content = template.render(**model_and_view['model'])
response.body = str(content)
return response(environ, start_response)
class Product(Base):
def handle_request(self, request, response):
model_and_view = Base.handle_request(self, request, response)
url, match = request.environ['wsgiorg.routing_args']
product_repository = product_repository_from_config(self.configuration)
model_and_view['view'] = 'products/product.html'
model_and_view['model']['product'] = \
product_repository.get_product(match['product_id'])
return model_and_view
Here is my Product repository code:
def product_repository_from_config(configuration):
session = session_from_config(configuration)
return SqlAlchemyProductRepository(session, configuration)
class SqlAlchemyProductRepository(object):
"""SQLAlchemey Based ProductRepository."""
def __init__(self, session, configuration = None):
self.configuration = configuration
self.session = session
def get_product(self, product_id):
return self.session.query(Product).filter_by(
productId=product_id).first()
Here is my ORM utility:
engines = {}
def setup_session(engine, **kwargs):
session = sqlalchemy.orm.sessionmaker(bind=engine, **kwargs)
return session()
def session_from_config(configuration, init=False, **kwargs):
engine = engine_from_config(configuration, init)
return setup_session(engine, **kwargs)
def engine_from_config(configuration, init=False):
"""Create an SQLAlchemy engine from a configuration object."""
config = configuration.to_dict()
key = pickle.dumps(config)
if key not in engines:
engine = sqlalchemy.engine_from_config(configuration.to_dict(),
prefix = 'db.')
configure_mapping_for_engine(engine, init)
engines[key] = engine
return engines[key]
Here is my view (jinja2):
{% extends "shell.html" %}
{% set title = "Product - " + product.name %}
{% block content %}
<h1>Product</h1>
<ul>
<li><a href="{{ url_for('products') }}">Product List</a></li>
</ul>
<form method="post" action="{{ url_for('products/update', product_id=product.productId) }}">
Name <input type="text" name="name" value="{{ product.name|e }}" /><br />
<input type="submit" value="Update" />
</form>
<form enctype="multipart/form-data" method="post" action="{{ url_for('products/versions/add', product_id=product.productId) }}">
Version Name <input type="text" name="name" />
<input type="submit" value="Add Version" />
</form>
<ul>
{% for product_version in product.product_versions %}
<li>{{ product_version.name }}
<ul>
<li><a href="{{ url_for('products/versions/delete', product_id=product.productId, product_version_id=product_version.productVersionId) }}">delete</a></li>
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}
The error I get is:
UnboundExecutionError: Parent instance <Product at 0x9150c8c> is not bound to a Session; lazy load operation of attribute 'product_versions' cannot proceed
The stack trace shows this being thrown from:
{% for product_version in product.product_versions %}
What could cause my instance to become unbound from the Session between the time that I get it from the repository to the time that it is evaluated by jinja2 in the template?
I am leaning in the direction of thinking it might be the authkit call getting in the way of things but I am not sure what it could possibly be doing since it actually happens before the session is created and should probably not impact anything that happens later?