Почему я получаю прерывистую UnboundExecutionError от SQLAlchemy при втором запросе WSGI? - PullRequest
0 голосов
/ 13 января 2010

Я создаю небольшое приложение WSGI и у меня периодически возникает проблема с SQLAlchemy, выдавая UnboundExceptionError.

Когда это происходит, это происходит исключительно по второму запросу браузера. Обновление страницы (и все последующие попытки просмотра страницы) работают нормально. Кажется, это происходит только по второму запросу.

Я работаю со многими новыми для меня технологиями, поэтому мне не совсем понятно, на что мне следует обращать внимание, чтобы попытаться это выяснить.

  • CherryPyWSGIServer
  • Маршруты
  • AuthKit
  • WebOb
  • SQLAlchemy
  • jinja2

Вот мои настройки, связанные с SQLAlchemy:

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={ })

Вот мой контроллер:

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

Вот мой код репозитория продукта:

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()

Вот моя утилита ORM:

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]

Вот мой взгляд (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 %}

Я получаю ошибку:

UnboundExecutionError: Parent instance <Product at 0x9150c8c> is not bound to a Session; lazy load operation of attribute 'product_versions' cannot proceed

Трассировка стека показывает, что это выбрасывается из:

{% for product_version in product.product_versions %}

Что может вызвать отсоединение моего экземпляра от сеанса между моментом, когда я получаю его из хранилища, и временем, когда он оценивается jinja2 в шаблоне?

Я склоняюсь к мысли, что это может быть вызовом authkit, но я не уверен, что он мог бы делать, поскольку это действительно происходит до создания сеанса и, вероятно, не должно влиять на что-либо, что случится позже?

1 Ответ

2 голосов
/ 14 января 2010

Как правило, у вас должна быть схема, в соответствии с которой для каждого запроса существует один сеанс, локальный для текущего потока и срываемый только в конце запроса (если он вообще может быть использован повторно при следующем запросе).запрос).Обычно вы видите код создания и очистки сеанса внутри всеобъемлющей оболочки запроса, в этом случае он, вероятно, будет в пределах handle_request_wrapper().Промежуточное программное обеспечение WSGI, такое как authkit, на самом деле не должно иметь доступа к сеансу в вашем собственном приложении, если вы как-то их не связываете.Во всем приведенном выше коде нет (правка: ОК, я смутно вижу, где он настраивается, создавая новый движок при каждом запросе - никогда не делайте этого ... также сеанс, вероятно, получает gc'ed до завершения запроса)индикация, когда ваш setup_session() действительно вызывается или что происходит с возвращенным сеансом по мере выполнения запроса.В любом случае, поскольку вы не используете контекстный менеджер, необходимо будет передать этот единственный сеанс всем функциям, задействованным в запросе - поскольку, если бы это был глобальный объект, вы получили бы одновременные потоки, обращающиеся к нему одновременно, что вызовет немедленныйпроблемы - и это довольно громоздкий шаблон.

scoped_session предоставлен, чтобы сделать шаблон «сеанс на поток» очень простым, а шаблон «настройка / разбор по запросам» устанавливается по умолчанию, когдаиспользуя пилоны например.Визуальная (хотя и ASCII) иллюстрация этого жизненного цикла находится на http://www.sqlalchemy.org/docs/05/session.html#lifespan-of-a-contextual-session.

...