Обновить строку (SQLAlchemy) с данными из зефира

Я использую Flask, Flask-SQLAlchemy, Flask-Marshmallow + marshmallow-sqlalchemy, пытаясь реализовать метод REST api PUT. Я не нашел учебника с использованием SQLA и Marshmallow, реализующих обновление.

Вот код:

class NodeSchema(ma.Schema):
    # ...


class NodeAPI(MethodView):
    decorators = [login_required, ]
    model = Node

    def get_queryset(self):
        if g.user.is_admin:
            return self.model.query
        return self.model.query.filter(self.model.owner == g.user)

    def put(self, node_id):
        json_data = request.get_json()
        if not json_data:
            return jsonify({'message': 'Invalid request'}), 400

        # Here is part which I can't make it work for me
        data, errors = node_schema.load(json_data)
        if errors:
            return jsonify(errors), 422

        queryset = self.get_queryset()


        node = queryset.filter(Node.id == node_id).first_or_404()
        # Here I need some way to update this object
        node.update(data) #=> raises AttributeError: 'Node' object has no attribute 'update'

        # Also tried:
        # node = queryset.filter(Node.id == node_id)
        # node.update(data) <-- It doesn't if know there is any object
        # Wrote testcase, when user1 tries to modify node of user2. Node doesn't change (OK), but user1 gets status code 200 (NOT OK).

        db.session.commit()
        return jsonify(), 200

Ответ 1

Вы должны передать редактируемый объект как параметр в schema.load(), например:

node_schema.load(json_data, instance=Node().query.get(node_id))

И если вы хотите загрузить без всех обязательных полей Model, вы можете добавить partial=True, например так:

node_schema.load(json_data, instance=Node().query.get(node_id), partial=True)

http://marshmallow-sqlalchemy.readthedocs.org/en/latest/api_reference.html#marshmallow_sqlalchemy.ModelSchema.load

Ответ 2

Я боролся с этой проблемой в течение некоторого времени, и в результате снова и снова возвращался к этому посту. В итоге, что осложнило мою ситуацию, так это то, что возникла сложная проблема, связанная с сеансами SQLAlchemy. Я полагаю, что это достаточно часто для Flask, Flask-SQLAlchemy, SQLAlchemy и Marshmallow, чтобы провести обсуждение. Я, конечно, не претендую на то, чтобы быть экспертом в этом, и все же я полагаю, что то, что я заявляю ниже, по существу правильно.

На самом деле db.session тесно связана с процессом обновления БД с помощью Marshmallow, и из-за этого решил дать подробности, но сначала кратко об этом.

Короткий ответ

Вот ответ, который я получил для обновления базы данных с помощью Marshmallow. Это отличается от очень полезного поста Jair Perrut. Я посмотрел на API Marshmallow и все же не смог заставить его решение работать в представленном коде, потому что в то время, когда я экспериментировал с его решением, я не управлял своими сеансами SQLAlchemy должным образом. Чтобы пойти немного дальше, можно сказать, что я не управлял ими вообще. Модель может быть обновлена следующим образом:

user_model = user_schema.load(user)
db.session.add(user_model.data)
db.session.commit()

Дайте session.add() модель с первичным ключом, и она примет обновление, оставит первичный ключ, и вместо этого будет создана новая запись. Это не так уж удивительно, поскольку в MySQL есть предложение ON DUPLICATE KEY UPDATE, которое выполняет обновление, если ключ присутствует, и создает, если нет.

подробности

Сеансы SQLAlchemy обрабатываются Flask-SQLAlchemy во время запроса к приложению. В начале запроса сеанс открывается, а когда запрос закрывается, этот сеанс также закрывается. Flask предоставляет хуки для настройки и разрушения приложения, в котором можно найти код для управления сеансами и соединениями. В конце концов, однако, сеанс SQLAlchemy управляется разработчиком, а Flask-SQLAlchemy просто помогает. Вот частный случай, иллюстрирующий управление сессиями.

Рассмотрим функцию, которая получает пользовательский словарь в качестве аргумента и использует его с помощью Marshmallow() для загрузки словаря в модель. В этом случае требуется не создание нового объекта, а обновление существующего объекта. В начале нужно помнить 2 вещи:

  • Классы моделей определены в модуле python отдельно от любого кода, и эти модели требуют сеанса. Часто разработчик (документация Flask) помещает строку db = SQLAlchemy() в начало этого файла, чтобы удовлетворить это требование. Это фактически создает сеанс для модели.
    • from flask_sqlalchemy import SQLAlchemy
    • db = SQLAlchemy()
  • В некоторых других отдельных файлах может также потребоваться сеанс SQLAlchemy. Например, коду может потребоваться обновить модель или создать новую запись, вызвав там функцию. Здесь можно найти db.session.add(user_model) и db.session.commit(). Этот сеанс создается так же, как в пункте выше.

Создано 2 сеанса SQLAlchemy. Модель находится в одной (SignallingSession), а модуль использует свою собственную (scoped_session). На самом деле их 3. Зефир UserSchema имеет sqla_session = db.session: к нему присоединяется сеанс. Это третий, и подробности можно найти в коде ниже:

from marshmallow_sqlalchemy import ModelSchema
from donate_api.models.donation import UserModel
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class UserSchema(ModelSchema):
    class Meta(object):
        model = UserModel
        strict = True
        sqla_session = db.session

def some_function(user):
    user_schema = UserSchema()
    user['customer_id'] = '654321'
    user_model = user_schema.load(user)

    # Debug code:
    user_model_query = UserModel.query.filter_by(id=3255161).first()
    print db.session.object_session(user_model_query)
    print db.session.object_session(user_model.data)
    print db.session

    db.session.add(user_model.data)
    db.session.commit()

    return

Во главе этого модуля импортируется модель, которая создает свой сеанс, а затем модуль создает свой собственный. Конечно, как указывалось, есть также сессия Зефир. В какой-то степени это вполне приемлемо, потому что SQLAlchemy позволяет разработчику управлять сессиями. Рассмотрим, что происходит, когда some_function(user) где user['id'] присваивается какое-то значение, которое существует в базе данных.

Поскольку user включает действительный первичный ключ, db.session.add(user_model.data) знает, что он не создает новую строку, а обновляет существующую. Такое поведение не должно вызывать удивления, и его следует хотя бы несколько ожидать, поскольку из документации MySQL:

13.2.5.2 INSERT... ON DUPLICATE KEY UPDATE Синтаксис
Если вы укажете условие ON DUPLICATE KEY UPDATE и строка, которая будет вставлена, приведет к дублированию значения в индексе UNIQUE или PRIMARY KEY, произойдет ОБНОВЛЕНИЕ старой строки.

Затем видно, что фрагмент кода обновляет customer_id в словаре для пользователя с первичным ключом 32155161. Новый идентификатор customer_id - "654321". Словарь загружен с Зефиром и фиксацией, сделанной к базе данных. Исследуя базу данных, можно обнаружить, что она действительно обновлена. Вы можете попробовать два способа проверить это:

  • В коде: db.session.query(UserModel).filter_by(id=325516).first()
  • В MySQL: select * from user

Если вы должны рассмотреть следующее:

  • В коде: UserModel.query.filter_by(id=3255161).customer_id

Вы обнаружите, что запрос возвращает None. Модель не синхронизирована с базой данных. Мне не удалось правильно управлять нашими сеансами SQLAlchemy. Чтобы внести ясность в это, рассмотрим выходные данные операторов печати при выполнении отдельного импорта:

  • <sqlalchemy.orm.session.SignallingSession object at 0x7f81b9107b90>
  • <sqlalchemy.orm.session.SignallingSession object at 0x7f81b90a6150>
  • <sqlalchemy.orm.scoping.scoped_session object at 0x7f81b95eac50>

В этом случае сеанс UserModel.query отличается от сеанса Marshmallow. Зефирная сессия - это то, что загружается и добавляется. Это означает, что запрос модели не покажет наши изменения. На самом деле, если мы делаем:

  • db.session.object_session (user_model.data).commit()

Модельный запрос теперь вернет обновленный customer_id! Рассмотрим второй вариант, где импорт осуществляется через flask_essentials:

from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
ma = Marshmallow()
  • <sqlalchemy.orm.session.SignallingSession object at 0x7f00fe227910>
  • <sqlalchemy.orm.session.SignallingSession object at 0x7f00fe227910>
  • <sqlalchemy.orm.scoping.scoped_session object at 0x7f00fed38710>

И сеанс UserModel.query теперь совпадает с сеансом user_model.data (Marshmallow). Теперь UserModel.query отражает изменение в базе данных: сеансы Marshmallow и UserModel.query совпадают.

Примечание: сеанс сигнализации является сеансом по умолчанию, который использует Flask-SQLAlchemy. Он расширяет систему сеансов по умолчанию с выбором привязки и отслеживанием изменений.

Ответ 3

Я выкатил собственное решение. Надеюсь, это поможет кому-то другому. Решение реализует метод обновления в Node модели.

Решение:

class Node(db.Model):
    # ...

    def update(self, **kwargs):
        # py2 & py3 compatibility do:
        # from six import iteritems
        # for key, value in six.iteritems(kwargs):
        for key, value in  kwargs.items():
            setattr(self, key, value)


class NodeAPI(MethodView):
    decorators = [login_required, ]
    model = Node

    def get_queryset(self):
        if g.user.is_admin:
            return self.model.query
        return self.model.query.filter(self.model.owner == g.user)

    def put(self, node_id):
        json_data = request.get_json()
        if not json_data:
            abort(400)

        data, errors = node_schema.load(json_data)  # validate with marshmallow
        if errors:
            return jsonify(errors), 422

        queryset = self.get_queryset()
        node = queryset.filter(self.model.id == node_id).first_or_404()
        node.update(**data)
        db.session.commit()
        return jsonify(message='Successfuly updated'), 200