Как не запустить транзакцию автоматически в Pyramid?

Я создал простое приложение Pyramid, которое использует SQLAlchemy, pyramid_tm, pyramid_beaker и alembic. База данных - PostgreSQL, а адаптер - pg8000. Сейчас я пытаюсь реализовать вход в систему, но первый запрос БД к базе данных создает транзакцию BEGIN и зависает навсегда. Я хотел бы настраивать транзакции только при необходимости (UPDATE, DELETE, INSERT и более сложные мульти-запросы).

models/user.py:

from sqlalchemy import Column
from sqlalchemy import Unicode
from sqlalchemy import Sequence
from sqlalchemy import Integer
from sqlalchemy import Index
from sqlalchemy import CheckConstraint
from sqlalchemy import text
from sqlalchemy import func
from sqlalchemy.dialects.postgresql import TIMESTAMP

from pyramid.security import Allow

import sqlalchemy.orm.exc as a_exc

import logging

log = logging.getLogger(__name__)


from ..models import DBSession
from ..models import Base

class UserNotFoundException(ValueError):
    pass


class User(Base):
    __tablename__ = 'users'

    __table_args__ = (
        CheckConstraint("login ~* '^[a-z]{3,}$'", name = __tablename__ + "_chk_login"),
        CheckConstraint("login != ''", name = __tablename__ + "_chk_login_not_empty"),
        CheckConstraint("password != ''", name = __tablename__ + "_chk_pw_not_empty"),
        Index(__tablename__ + "_idx_lower_login", text("lower(login)"), unique = True),
    )

    id = Column(Integer, Sequence('users_id_seq'), primary_key = True)
    login = Column(Unicode(64), unique = True, nullable = False, server_default = text("''"))
    password = Column(Unicode(255), nullable = False, server_default = text("''"))
    added = Column(TIMESTAMP, nullable = False, server_default = text("NOW()"))


    @property
    def __acl__(self):
        return [(Allow, self.login, 'view'), ]

    def __init__(self, login, password):
        self.login = login
        self.password = password

    @classmethod
    def get_user(self, login):
        try:
            u = DBSession.query(User).filter(User.login == login).one()
            DBSession.flush()
            return u
        except a_exc.NoResultFound as exc:
            raise UserNotFoundException(exc)

    @classmethod
    def get_user_count(self):
        u = DBSession.query(func.count(User.id)).scalar()
        DBSession.flush()
        return u

    @classmethod
    def create_session(self, login: str, password: str) -> object:
        u = self.get_user(login)

        import bcrypt
        password = password.encode('utf-8')

        try:
            verified = bcrypt.checkpw(password = password, hashed_password = u.password.encode('utf-8'))
        except Exception as exc:
            raise

        if verified != True:
            raise Exception("Coulnd't verify password hash")

        return {'userid': u.id}

    @classmethod
    def add_user(self, login, password):
        import bcrypt
        password = password.encode('utf-8')

        encrypted_pw = bcrypt.hashpw(password, bcrypt.gensalt())
        verified = False

        log.debug("Encrypted PW: '%s'", encrypted_pw)

        try:
            verified = bcrypt.checkpw(password = password, hashed_password = encrypted_pw)
        except Exception:
            raise

        if verified != True:
            raise Exception("Coulnd't verify password hash")

        try:
            DBSession.begin(subtransactions=True)
            DBSession.add(User(login = login, password = encrypted_pw.decode()))
            DBSession.commit()
            log.debug("User added: '%s'", login)
        except Exception as exc:
            DBSession.rollback()
            log.debug("User add failed for user '%s'", login)
            raise

views/views.py:

@view_config(route_name = 'login', renderer = 'templates/login.pt')
def app_login_view(request: Request):
    if request.authenticated_userid:
        # Already logged in -> redirect
        import pyramid.httpexceptions as exc
        return exc.HTTPFound(request.route_path('home'))

    user_not_found_error = {
        'page_background': 'warning',
        'page_title':      _(u"Login failed"),
        'page_text':       _(u"Check username and password."),
    }

    form_user = request.POST.get('user')
    form_password = request.POST.get('password')

    from ..models import User, UserNotFoundException

    if User.get_user_count() == 0:
        # No users in DB
        log.debug("Creating admin user")
        User.add_user(u"admin", u"admin")

    try:
        ses = User.create_session(form_user, form_password)
        request.session['userid'] = ses['userid']
        request.session.save()
        remember(request, ses['userid'])
    except UserNotFoundException as exc:
        log.debug("User '%s' not found in database", form_user)
        return user_not_found_error
    except:
        raise

    # Redirect to front page
    import pyramid.httpexceptions as exc
    return exc.HTTPFound(request.route_path('home'))

Журнал:

INFO sqlalchemy.engine.base.Engine.dbconn BEGIN (implicit)
INFO sqlalchemy.engine.base.Engine.dbconn SELECT count(users.id) AS count_1 
FROM users
INFO sqlalchemy.engine.base.Engine.dbconn ()
DEBUG [waitress] Creating admin user
DEBUG [user][waitress] Encrypted PW: 'b'$2b$12$n6mN973Gz0wwX7B0kWI.Ae099h7mvLo.mEI.D2NFjZKaLKbGebK16''
INFO sqlalchemy.engine.base.Engine.dbconn INSERT INTO users (id, login, password) VALUES (nextval('users_id_seq'), %s, %s) RETURNING users.id
INFO sqlalchemy.engine.base.Engine.dbconn ('admin', '$2b$12$n6mN973Gz0wwX7B0kWI.Ae099h7mvLo.mEI.D2NFjZKaLKbGebK16')
INFO  [sqlalchemy.engine.base.Engine.dbconn:109][waitress] INSERT INTO users (id, login, password) VALUES (nextval('users_id_seq'), %s, %s) RETURNING users.id
INFO  [sqlalchemy.engine.base.Engine.dbconn:109][waitress] ('admin', '$2b$12$n6mN973Gz0wwX7B0kWI.Ae099h7mvLo.mEI.D2NFjZKaLKbGebK16')
... Hangs here forever ...

Если я удалю subtransactions=True от add_user() Я получил:

sqlalchemy.exc.InvalidRequestError: A transaction is already begun.  Use subtransactions=True to allow subtransactions.

Кроме того, когда я отправляю /login Я вижу переменные сеанса в Request Vars вкладка в панели DebugToolbar с _accessed_time а также _creation_time но ничего о userid и после перенаправления на / нет переменных сеанса вообще.

1 ответ

Надлежащим способом выполнить вставку и обработать ошибку (откат) является использование точки сохранения и flush(),

sp = request.tm.savepoint()
try:
    DBSession.add(User(login = login, password = encrypted_pw.decode()))
    DBSession.flush()
    log.debug("User added: '%s'", login)
except Exception as exc:
    sp.rollback()
    log.debug("User add failed for user '%s'", login)
    raise

Тем не менее, вы даже ничего не делаете с ошибкой в ​​вашем примере, так что вы можете просто использовать .add без каких-либо дополнительных шаблонов.

В конце запроса pyramid_tm выдаст окончательный коммит. Сброс выполняет ожидающие команды SQL в открытой транзакции в базе данных, что позволяет вам отследить потенциальные ошибки.

Другие вопросы по тегам