`flush` не срабатывает при инициализации с объекта - PullRequest
0 голосов
/ 16 июня 2019

В настоящее время я работаю с перманентом при использовании метода flush (или commit, в зависимости от него) из SQLAlchemy session в Flask Alchemy

Партия flush всегда терпела неудачу с sqlalchemy.exc.ResourceClosedError: This transaction is closed (ошибка полного стека ниже). При непосредственном запуске insert вызов из движка работает, а также извлекает данные с помощью компоновщика query.

Кроме того, удаление элемента работает правильно (через session.delete(model) и session.commit())

Вот код ошибки:

roles_put = Blueprint('roles_put', __name__)


@roles_put.route('<role_id>', methods=['PUT'])
def role_update(role_id):
    role = Role.query.get(role_id)
    if not role:
        role = Role.query.filter_by(name=role_id).first()
        if not role:
            raise IDNotFoundError()

    print(role)
    role.set_data(
        request.form,
        [
            'name', 'manage_user', 'manage_video', 'manage_comment', 'manage_avatar', 'manage_channel', 'manage_reward',
            'manage_role', 'manage_top', 'manage_calendar', 'manage_setting', 'validate_video', 'moderate_comment',
        ]
    )
    MainAPI.db.session.add(role)
    MainAPI.db.session.flush()
    MainAPI.db.session.commit()
    # if not role.save():
    #     raise UpdateError()
    return jsonify(role.serialize())

SQLAlchemy инициализируется через:

app = Flask('Name')

def init_db(app, config):
        """
        Init SQLAlchemy DB
        """
        app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
        app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://%s:%s@%s/%s' % (
            config.get('database', 'user'),
            config.get('database', 'password'),
            config.get('database', 'host'),
            config.get('database', 'database')
        )
        print(app.config['SQLALCHEMY_DATABASE_URI'])


MainApi.db = SQLAlchemy(app)

Ролевая модель:

class RoleModel(MainApi.db.Model):
    __tablename__ = 'roles'
    LOCKED_ROLE_NAMES = ['guest', 'admin', 'logged', 'public']
    id = MainApi.db.Column(MainApi.db.Integer, primary_key=True, unique=True, autoincrement=True)
    name = MainApi.db.Column(MainApi.db.String(40), nullable=False, unique=True)

    # manage rights
    manage_user = MainApi.db.Column(MainApi.db.Boolean, nullable=False, default=False)
    moderate_comment = MainApi.db.Column(MainApi.db.Boolean, nullable=False, default=False)

    created_at = MainApi.db.Column(MainApi.db.DateTime, nullable=False, default=datetime.utcnow)
    last_updated_at = MainApi.db.Column(MainApi.db.DateTime, nullable=True)

    created_by = MainApi.db.Column(
        MainApi.db.Integer,
        MainApi.db.ForeignKey(
            'users.id', ondelete='RESTRICT', onupdate='CASCADE'
        ),
        nullable=True
    )
    last_updated_by = MainApi.db.Column(
        MainApi.db.Integer,
        MainApi.db.ForeignKey(
            'users.id', ondelete='CASCADE', onupdate='CASCADE'
        ), nullable=True
    )

    users = MainApi.db.relationship(
        'UserModel', foreign_keys='UserModel.role_id',
        back_populates='role'
    )

    @staticmethod
    def is_allowed(action, role):
        """
        Check if user having role can make action

        :param action: action name
        :type action: str
        :param role: user role
        :type role: int|str|RoleModel
        :return:
        """
        if isinstance(role, str):
            role = RoleModel.query.filter_by(name=role).first()
        elif isinstance(role, int):
            role = RoleModel.query.get(role)
        if not isinstance(role, RoleModel):
            raise Exception
        return getattr(role, action.strip().replace(' ', '_'))

    @staticmethod
    def get_role_id(role):
        """
        Check if user having role can make action

        :param role: user role
        :type role: int|str|RoleModel
        :return: role id
        :rtype: int
        """
        if isinstance(role, str):
            role = RoleModel.query.filter_by(name=role).first()
        elif isinstance(role, int):
            role = RoleModel.query.get(role)
        if not isinstance(role, RoleModel):
            raise Exception
        return role.id

    def serialize(self):
        users = [u.serialize() for u in self.users] if self.users else []
        return {
            'id':               self.id,
            'name':             self.name,
            'manage_user':      self.manage_user,
            'moderate_comment': self.moderate_comment,
            'created_at':       self.created_at,
            'last_updated_at':  self.last_updated_at,
            'created_by':       self.created_by,
            'last_updated_by':  self.last_updated_by,
            'users':            users,
        }


@event.listens_for(RoleModel.name, 'set', propagate=True)
def before_set_name(_target, value, old, _initiator):
    print(_initiator)
    print(request.url)
    if request and 'roles/init' not in request.url:
        if old in RoleModel.LOCKED_ROLE_NAMES or value in RoleModel.LOCKED_ROLE_NAMES:
            raise UnauthorizedError()


@event.listens_for(RoleModel, 'before_insert', propagate=True)
def receive_before_insert(_mapper, _connection, target):
    user = Registry.registered('current-user-id')
    target.created_at = datetime.utcnow()
    if user:
         target.created_by = user


@event.listens_for(RoleModel, 'before_update', propagate=True)
def receive_before_update(_mapper, _connection, target):
    user = Registry.registered('current-user-id')
    target.updated_at = datetime.utcnow()
    if user:
       target.updated_by = user

Полный стек ошибок:

INFO:werkzeug: * Running on http://localhost:8080/ (Press CTRL+C to quit)
<RoleModel 5>
<sqlalchemy.orm.attributes.Event object at 0x7f5baedf8788>
http://localhost:8080/MainApi/roles/5
ERROR:flask.app:Exception on /MainApi/roles/5 [PUT]
Traceback (most recent call last):
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1177, in _execute_context
    conn = self._revalidate_connection()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 469, in _revalidate_connection
    raise exc.ResourceClosedError("This Connection is closed")
sqlalchemy.exc.ResourceClosedError: This Connection is closed

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2556, in _flush
    flush_context.execute()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute
    rec.execute(self)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute
    uow,
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 236, in save_obj
    update,
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 978, in _emit_update_statements
    statement, multiparams
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 988, in execute
    return meth(self, multiparams, params)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/sql/elements.py", line 287, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1107, in _execute_clauseelement
    distilled_params,
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
    e, util.text_type(statement), parameters, None, None
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1466, in _handle_dbapi_exception
    util.raise_from_cause(sqlalchemy_exception, exc_info)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 383, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 128, in reraise
    raise value.with_traceback(tb)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1177, in _execute_context
    conn = self._revalidate_connection()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 469, in _revalidate_connection
    raise exc.ResourceClosedError("This Connection is closed")
sqlalchemy.exc.StatementError: (sqlalchemy.exc.ResourceClosedError) This Connection is closed
[SQL: UPDATE roles SET name=%s, manage_video=%s WHERE roles.id = %s]
[parameters: [{'name': 'admin2', 'manage_video': '0', 'roles_id': 5}]]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/app.py", line 2311, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/app.py", line 1834, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/app.py", line 1737, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/_compat.py", line 36, in reraise
    raise value
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/app.py", line 1832, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/flask/app.py", line 1818, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/titouan/main_api/python-api/main_api/routes/roles/put.py", line 28, in role_update
    MainApi.db.session.flush()
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/scoping.py", line 162, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2458, in flush
    self._flush(objects)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2596, in _flush
    transaction.rollback(_capture_exception=True)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 79, in __exit__
    compat.reraise(type_, value, traceback)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 129, in reraise
    raise value
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 2596, in _flush
    transaction.rollback(_capture_exception=True)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 509, in rollback
    self._assert_active(prepared_ok=True, rollback_ok=True)
  File "/home/titouan/main_api/python-api/venv/lib/python3.7/site-packages/sqlalchemy/orm/session.py", line 303, in _assert_active
    raise sa_exc.ResourceClosedError(closed_msg)
sqlalchemy.exc.ResourceClosedError: This transaction is closed

Версия:

  • Python 3.7.3 (VENV)
  • MySQL 8 (докер)
  • Flask-SQLAlchemy == 2.4.0 / Flask-SQLAlchemy == 2.3.2 (пробовал оба)
  • Настой == 1.0.3
  • SQLAlchemy == 1.3.4

Спасибо всем, у кого есть подсказка.

1 Ответ

0 голосов
/ 16 июня 2019

Наконец-то решил.

Проблема была в обратных вызовах моделей before_insert и before_update. Попытка извлечь current-user-id из Flask.g кажется странным образом влияет на session. Это, безусловно, связано с тем, как работает моя реализация класса Registry.

...