Django-rest-framework: Вызовы PUT не полностью «заменяют состояние целевого ресурса»

Созданный на 30 июн. 2016  ·  68Комментарии  ·  Источник: encode/django-rest-framework

РЕДАКТИРОВАТЬ: чтобы узнать текущий статус проблемы, перейдите к https://github.com/encode/django-rest-framework/issues/4231#issuecomment -332935943.

===

У меня возникла проблема с реализацией библиотеки Optimistic Concurrency в приложении, которое использует DRF для взаимодействия с базой данных. Я пытаюсь:

  • Подтвердите, что поведение, которое я вижу, связано с DRF
  • Подтвердите, что это предполагаемое поведение
  • Определите, есть ли какой-либо практический способ преодолеть это поведение

Недавно я добавил оптимистичный параллелизм в свое приложение Django. Чтобы сохранить поиск вики:

  • У каждой модели есть поле версии
  • Когда редактор редактирует объект, он получает версию редактируемого объекта.
  • Когда редактор сохраняет объект, включенный номер версии сравнивается с базой данных.
  • Если версии совпадают, редактор обновляет последний документ и происходит сохранение.
  • Если версии не совпадают, мы предполагаем, что между моментом загрузки и сохранения редактора было отправлено «конфликтующее» редактирование, поэтому мы отклоняем редактирование.
  • Если версия отсутствует, мы не можем проводить тестирование и должны отклонить редактирование.

У меня был устаревший пользовательский интерфейс, говорящий через DRF. Устаревший пользовательский интерфейс не обрабатывал номера версий. Я ожидал, что это вызовет ошибки параллелизма, но этого не произошло. Если я правильно понимаю обсуждение в # 3648:

  • DRF объединяет PUT с существующей записью. Это приводит к тому, что отсутствующий идентификатор версии заполняется текущим идентификатором базы данных.
  • Поскольку это всегда обеспечивает совпадение, пропуск этой переменной всегда будет нарушать оптимистичную систему параллелизма, которая обменивается данными через DRF.
  • ~ Нет простых вариантов (например, сделать поле «обязательным»), чтобы гарантировать, что данные отправляются каждый раз. ~ (изменить: вы можете обойти проблему, сделав его обязательным, как показано в этом комментарии )

Действия по воспроизведению

  1. Настройка поля Optimistic Concurrency в модели
  2. Создайте новый экземпляр и обновите несколько раз (чтобы убедиться, что у вас больше нет номера версии по умолчанию)
  3. Отправьте обновление (PUT) через DRF, за исключением идентификатора версии

    Ожидаемое поведение

Отсутствующий идентификатор версии не должен соответствовать базе данных и вызывать проблему параллелизма.

Фактическое поведение

Отсутствующий идентификатор версии заполняется DRF текущим идентификатором, поэтому проверка параллелизма проходит.

Enhancement

Все 68 Комментарий

Хорошо, не могу обещать, что смогу немедленно просмотреть этот довольно подробный тикет, так как грядущий выпуск 3.4 имеет приоритет. Но спасибо за такой подробный, хорошо продуманный вопрос. Это, скорее всего, будет рассматриваться в масштабе недель, а не дней или месяцев. Если вы добьетесь какого-либо прогресса, у вас возникнут какие-либо дальнейшие мысли, пожалуйста, обновите заявку и держите нас в курсе.

В ПОРЯДКЕ. Я почти уверен, что моя проблема заключается в сочетании двух факторов:

  1. DRF не требует поля в PUT (хотя оно требуется в модели), потому что оно имеет значение по умолчанию (версия = 0).
  2. DRF объединяет поля PUT с текущим объектом (без внедрения значения по умолчанию)

В результате DRF использует текущее значение (базы данных) и нарушает контроль параллелизма. Вторая половина проблемы связана с обсуждением в № 3648 (также цитируемом выше), и есть (до 3.x) обсуждение в № 1445, которое все еще кажется актуальным.

Я надеюсь, что конкретного (и все более распространенного) случая, когда поведение по умолчанию является извращенным, будет достаточно, чтобы возобновить дискуссию об «идеальном» поведении ModelSerializer. Очевидно, что я всего лишь на дюйм глубоко разбираюсь в DRF, но моя интуиция подсказывает, что следующее поведение подходит для обязательного поля и PUT:

  • При использовании нечастичного сериализатора мы должны либо получить значение, либо использовать значение по умолчанию, либо (если значение по умолчанию недоступно) вызвать ошибку проверки. Проверка всей модели должна применяться только к входным данным/значениям по умолчанию.
  • При использовании частичного сериализатора мы должны либо получить значение, либо вернуться к текущим значениям. К этим комбинированным данным должна применяться общемодельная проверка.
  • Я считаю, что текущий «нечастичный» сериализатор действительно квази-частичный:

    • Он не является частичным для полей, которые являются обязательными и не имеют значений по умолчанию.

    • Это частично для полей, которые являются обязательными и имеют значение по умолчанию (поскольку значение по умолчанию не используется)

    • Это частично для полей, которые не являются обязательными

Мы не можем изменить маркер (1) выше, иначе значения по умолчанию станут бесполезными (нам требуется ввод, хотя мы знаем значение по умолчанию). Это означает, что мы должны решить проблему, изменив пункт 2 выше. Я согласен с вашим аргументом в # 2683, что:

Значения модели по умолчанию являются значениями модели по умолчанию. Сериализатор должен опустить значение и возложить ответственность за это на Model.object.create() .

Чтобы соответствовать такому разделению проблем, update должен создать новый экземпляр (делегируя модели все значения по умолчанию) и применить отправленные значения к этому новому экземпляру. Это приводит к поведению, запрошенному в #3648.

Попытка описать путь миграции помогает показать, насколько странным является текущее поведение. Конечная цель состоит в том, чтобы

  1. Исправить ModelSerializer,
  2. Добавьте флаг для этого квазичастичного состояния и
  3. Сделайте этот флаг значением по умолчанию (для обратной совместимости)

Как называется этот флаг? Текущий сериализатор модели на самом деле является частичным сериализатором, который (несколько произвольно) требует полей, удовлетворяющих условию required==True and default==None . Мы не можем явно использовать флаг partial без нарушения обратной совместимости, поэтому нам нужен новый (надеюсь, временный) флаг. У меня осталось quasi_partial , но из-за моей неспособности выразить произвольное требование required==True and default==None мне так ясно, что это поведение должно быть прекращено в срочном порядке.

Вы можете добавить extra_kwargs в Meta сериализатора, сделав version обязательным полем.

class ConcurrentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ConcurrentModel
        extra_kwargs = {'version': {'required': True}}

Спасибо @anoopmalev. Это удержит меня в производственном отделении.

После «поспать на нем» я понимаю, что есть лишняя морщина. Все, что я сказал, должно относиться к полям сериализатора. Если поле не включено в сериализатор, его не следует изменять. Таким образом, все сериализаторы являются (и должны быть) частичными для не включенных полей. Это немного сложнее, чем мой «создать новый экземпляр» выше.

Я считаю, что этот вопрос необходимо свести к более ограниченному предложению, чтобы двигаться вперед.
Кажется слишком широким, чтобы действовать в его текущем состоянии.
На данный момент я закрываю это - если кто-то может сократить его до краткого, действенного заявления о желаемом поведении, тогда мы можем пересмотреть. До тех пор я думаю, что это просто слишком широко.

Вот краткое предложение... для нечастичного сериализатора:

  1. Для любого поля, не указанного в сериализаторе (неявно или явно) или помеченного как доступное только для чтения, сохраните существующее значение.
  2. Для всех остальных полей используйте первый доступный вариант:

    1. Заполнить отправленным значением

    2. Заполните значением по умолчанию, включая значение, подразумеваемое blank и/или null

    3. Создать исключение

Для ясности проверка выполняется на конечном продукте этого процесса.

Т.е. вы хотите установить required=True для любого поля сериализатора, у которого нет модели по умолчанию, для обновлений?

Я правильно понял?

Да (и многое другое). Вот как я понимаю различие partial (все поля необязательны) и non-partial (все поля обязательны). Единственный случай, когда сериализатору non-partial не требуется поле, — это наличие значения по умолчанию (узко или широко определенного), поскольку сериализатор может использовать это значение по умолчанию, если значение не указано._

Курсивом выделено то, что DRF в настоящее время не делает, и более важное изменение в моем предложении. Текущая реализация просто пропускает это поле.

У меня было второе предложение, но на самом деле это отдельный вопрос о том, насколько щедрым вы хотите быть с идеей «по умолчанию». Текущее поведение является «строгим» в том смысле, что только default рассматривается как таковое. Если вы _действительно_ хотите уменьшить количество требуемых данных, вы также можете сделать поля blank=True необязательными... предполагая, что отсутствующее значение является пустым значением.

@claytondaley Я использую OOL с DRF с 2-кратного увеличения следующим образом:

class VersionModelSerializer(serializers.ModelSerializer, BaseSerializer):
    _initial_version = 0

    _version = VersionField()

    def __init__(self, *args, **kwargs):
        super(VersionModelSerializer, self).__init__(*args, **kwargs)

        # version field should not be required if there is no object
        if self.instance is None and '_version' in self.fields and\
                getattr(self, 'parent', None) is None:
            self.fields['_version'].read_only = True
            self.fields['_version'].required = False

        # version field is required while updating instance
        if self.instance is not None and '_version' in self.fields:
            self.fields['_version'].required = True

        if self.instance is not None and hasattr(self.instance, '_version'):
            self._initial_version = self.instance._version

    def validate__version(self, value):
        if self.instance is not None:
            if not value and not isinstance(value, int):
                raise serializers.ValidationError(_(u"This field is required"))

        return value
   # more code & helpers

он отлично работает со всеми видами бизнес-логики и никогда не вызывал никаких проблем.

Это было случайно закрыто? Я ответил на конкретный вопрос и не услышал причину, что не так с предложением.

@claytondaley, почему OOL должен быть частью DRF? Проверьте мой код — он работает, просто найдите его в большом приложении (1400 тестов). VersionField — это всего лишь IntegerField .

Вы жестко запрограммировали OOL в сериализатор. Это неправильное место для этого, потому что у вас есть состояние гонки. Параллельные обновления (с одной и той же предыдущей версией) будут проходить через сериализатор... но только одно будет выигрывать при сохранении.

Я использую django-concurrency , что помещает логику OOL в действие сохранения (там, где оно должно быть). В основном UPDATE... WHERE version = submitted_version . Это атомарно, поэтому нет условий гонки. Однако он выявляет недостаток в логике сериализации:

  • Если для поля в модели задано значение по умолчанию, DRF устанавливает значение required=False . (Правильная) идея состоит в том, что DRF может использовать это значение по умолчанию, если значение не отправлено.
  • Однако если это поле отсутствует, DRF не использует значение по умолчанию. Вместо этого он объединяет отправленные данные с текущей версией объекта.

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

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

@claytondaley

Вы жестко запрограммировали OOL в сериализатор.

Я? Нашли ли вы какую-либо логику OOL в моем сериализаторе помимо требования к полю?

Это неправильное место для этого, потому что у вас есть состояние гонки.

Сорри, я просто не вижу, где здесь состояние гонки.

Я использую django-concurrency, который помещает логику OOL в действие сохранения (там, где оно принадлежит).

Я также использую django-concurrency :) Но это уровень модели, а не сериализатор. На уровне сериализатора вам просто нужно:

  • убедитесь, что поле _version всегда требуется (когда оно должно быть)
  • убедитесь, что ваш сериализатор знает, как обрабатывать ошибки OOL (эту часть я пропустил)
  • убедитесь, что ваш apiview знает, как обрабатывать ошибки OOL и вызывает HTTP 409 с возможным контекстом diff

на самом деле, я не использую django-concurrency из-за проблемы, которую автор пометил как «не исправит»: он обходит OOL, когда используется obj.save(update_fields=['one', 'two', 'tree']) , что я нашел плохой практикой, поэтому я разветвил пакет.

вот недостающий метод save сериализатора, о котором я упоминал ранее. это должно решить все ваши проблемы:

    def save(self, **kwargs):
        try:
            self.instance = super(VersionModelSerializer, self).save(**kwargs)
            return self.instance
        except VersionException:
            # Use select_for_update so we have some level of guarantee
            # that object won't be modified at least here at the same time
            # (but it may be modified somewhere else, where select_for_update
            # is not used!)
            with transaction.atomic():
                db_instance = self.instance.__class__.objects.\
                    select_for_update().get(pk=self.instance.pk)
                diff = self._get_serializer_diff(db_instance)

                # re-raise exception, so api client will receive friendly
                # printed diff with writable fields of current serializer
                if diff:
                    raise VersionException(diff)

                # otherwise re-try saving using db_instance
                self.instance = db_instance
                if self.is_valid():
                    return super(VersionModelSerializer, self).save(**kwargs)
                else:
                    # there are errors that could not be displayed to a user
                    # so api client should refresh & retry by itself
                    raise VersionException

        # instance.save() was interrupted by application error
        except ApplicationException as logic_exc:
            if self._initial_version != self.instance._version:
                raise VersionException

            raise logic_exc

Прости. Я не читал ваш код, чтобы понять, что вы делаете. Я видел сериализатор. Вы, очевидно, можете обойти проблему, взломав сериализатор, но вам не нужно... потому что недостаток в логике DRF существует сам по себе. Я просто использую OOL, чтобы подчеркнуть суть.

И вы должны попробовать этот код с последней версией django-concurrency (используя IGNORE_DEFAULT=False ). django-concurrency также игнорировал значения по умолчанию, но я отправил патч. Был странный угловой случай, который мне пришлось выследить, чтобы заставить его работать для обычных случаев.

Я думаю, что это называется расширением функциональности по умолчанию, а не взломом. Я думаю, что лучшее место для такой поддержки функций — пакет django-concurrency .

Я перечитал все обсуждение проблемы и нашел ваше предложение слишком широким, и во многих местах оно потерпит неудачу (из-за волшебного использования значений по умолчанию из разных источников в разных условиях). DRF 3.x стал намного проще и предсказуемее, чем 2.x, пусть так и останется :)

Вы не можете исправить это на уровне модели, потому что он не работает в сериализаторе (до того, как он попадет в модель). Отложите OOL... почему нам не нужно поле, если установлено значение default ?

Нечастичный сериализатор «требует» всех полей (фундаментально), и все же мы пропускаем это. Это ошибка? Или у нас есть логическое объяснение?

как вы можете видеть в моем примере кода — поле _version всегда правильно требуется во всех возможных случаях.

Кстати, оказалось, что я позаимствовал код lvl модели с https://github.com/gavinwahl/django-optimistic-lock , а не с django-concurrency , что сложно почти без причины.

... поэтому ошибка заключается в том, что «нечастичные сериализаторы неправильно установили некоторые поля как необязательные». Это альтернатива. Потому что это (неявное) обязательство, которое делает нечастичный сериализатор.

Могу процитировать :

По умолчанию сериализаторам должны быть переданы значения для всех обязательных полей, иначе они вызовут ошибки проверки.

Это ничего не говорит о обязательном (за исключением случаев, когда указано значение по умолчанию).

(и я понимаю, что говорю о двух разных уровнях, но ModelSerializer не должен отменять обязательные поля, если он не собирается брать на себя ответственность за это решение)

Кажется, я потерял твою мысль..

(и я понимаю, что говорю о двух разных уровнях, но ModelSerializer не должен отменять обязательные поля, если он не собирается брать на себя ответственность за это решение)

Что в этом плохого?

Хорошо, позвольте мне попробовать другой угол.

  • Предположим, у меня есть нечастичный сериализатор модели (редактирование: все значения по умолчанию), который охватывает все поля в моей модели.

Должны ли CREATE или UPDATE с одними и теми же данными когда-либо создавать другой объект (без идентификатора)

Можете ли вы описать свои идеи, используя действительно простую модель и сериализатор и несколько строк, которые показывают неудачное/ожидаемое поведение?

Я соберу что-нибудь завтра, так как здесь уже поздно... но чем глубже я погружаюсь, тем больше смысла # 3648 делает для нечастичного сериализатора. Между тем, почему для ModelSerializer не требуются все поля модели? Возможно, ваше обоснование отличается от моего.

ModelSerializer проверяет ограниченную модель и решает, нужна ли она, не так ли?

Я не имею в виду механически как . Базовое предположение для нечастичного сериализатора — требовать все (цитируется выше). Если get_field_kwargs собирается отклониться от этого предположения (в частности, здесь ), у этого должна быть веская причина. Что это за причина?

Мой предпочтительный ответ - тот, который я продолжаю давать, «потому что он может использовать это значение по умолчанию, если значение не отправлено» (но тогда DRF должен фактически использовать значение по умолчанию). Есть ли еще один ответ, который мне не хватает? Причина, по которой поля со значениями по умолчанию не требуются?

Очевидно, я предпочитаю «полное» решение. Однако я допускаю, что есть и второй ответ. Мы могли бы потребовать эти поля по умолчанию. Это устраняет (в настоящее время произвольный) особый случай. Это упрощает/уменьшает код. Он внутренне непротиворечив. Это решает мою проблему.

По сути, это делает нечастичный сериализатор действительно нечастичным.

Теперь я по крайней мере знаю, что вы имеете в виду. Вы проверили, что такое поведение ModelForm в таком случае? (Не могу сделать это сам на мобильном телефоне)

Документы Django говорят, что «пусто» определяет, является ли поле обязательным или нет. Я предлагаю вам создать отдельный тикет для этой проблемы, так как он содержит много несвязанных комментариев. На мой взгляд, modelserializer может работать как модельная форма: требуются пустые элементы управления параметрами, «null» сообщает, является ли None приемлемым вводом, а «default» не влияет на эту логику.

Я готов открыть второй тикет, но боюсь, что для бланка требуется аналогичный код. Из дискуссионной группы django :

если мы возьмем существующую форму модели и шаблон, который работает, добавим в модель необязательное символьное поле, но не сможем добавить соответствующее поле в шаблон HTML (например, человеческая ошибка, забыл о шаблоне, не сказал автору шаблона сделать изменение, не понял, что необходимо внести изменения в шаблон), когда эта форма будет отправлена, Django предположит, что пользователь предоставил пустое строковое значение для отсутствующего поля, и сохранит его в модели, удалив любое существующее значение .

Чтобы быть последовательным, у нас было бы обязательство выполнить вторую половину контракта, установив отсутствующее значение пустым. Это немного менее проблематично, потому что пробел может быть заполнен без ссылки на модель, но очень похоже (и, я думаю, согласуется с #3648).

@tomchristie , не могли бы вы кратко рассказать об этом: почему состояние required зависит от свойства поля модели defaults ?

Почему требуемое состояние зависит от свойства по умолчанию поля модели?

Просто это: если поле модели имеет значение по умолчанию, вы можете не указывать его в качестве входных данных.

На самом деле я согласен с таким поведением. ModelForm, несмотря на то, что код делает то же самое (сгенерированный html предоставит значения по умолчанию). Если DRF будет иметь другую логику, то «по умолчанию» никогда не будет применяться. Я закончил с этой проблемой.

@pySilver на самом деле, вот поведение ModelForm:

# models.py

from django.db import models

class MyModel(models.Model):
    no_default = models.CharField(max_length=100)
    has_default = models.CharField(max_length=100, default="iAmTheDefault")

Для ясности материал по-прежнему называется «частичным», потому что _update_ является частичным. Я также тестировал полное («полное») обновление, но код был не нужен, чтобы показать поведение:

# in manage.py shell
>>> from django import forms
>>> from django.conf import settings
>>> from form_serializer.models import MyModel
>>>
>>> class MyModelForm(forms.ModelForm):
...     class Meta:
...         model = MyModel
...         fields = ['no_default', 'has_default']
...
>>>
>>> partial = MyModel.objects.create()
>>> partial.id = 2
>>> partial.no_default = "Must replace me"
>>> partial.has_default = "I should be replaced"
>>> partial.save()
>>>
>>>
>>> POST_PARTIAL = {
...     "id": 2,
...     "no_default": "must change me",
... }
>>>
>>>
>>> form_partial = MyModelForm(POST_PARTIAL)
>>> form_partial.is_valid()
False
>>> form_partial._errors
{'has_default': [u'This field is required.']}

ModelForm требует этого ввода, даже если он имеет значение по умолчанию. Это одно из двух внутренне непротиворечивых поведений.

Почему требуемое состояние зависит от свойства по умолчанию поля модели?

Просто это: если поле модели имеет значение по умолчанию, вы можете не указывать его в качестве входных данных.

@tomchristie в принципе согласен. Но каково ожидаемое поведение?

  • При создании я получаю значение по умолчанию (тривиально, все согласны с тем, что это правильно)
  • При обновлении что я должен получить?

Мне кажется, что я должен получить значение по умолчанию при обновлении. Я не понимаю, почему нечастичный сериализатор должен вести себя по-разному в этих двух случаях. Нечастичный означает, что я отправляю "полную" запись. Таким образом, полная запись должна быть заменена.

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

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

Я, конечно, не разделяю ваших предпочтений; Я хочу, чтобы все мои интерфейсы были строгими, если только я намеренно не сделаю их иначе. Однако ваше различие между ЧАСТИЧНЫМ и НЕЧАСТИЧНЫМ уже обеспечивает (теоретически) то, что мы оба хотим.

Я считаю, что partial ведет себя именно так, как вы хотите:

  • ОБНОВЛЕНИЯ являются 100% частичными
  • CREATE (я предполагаю) являются частичными по отношению к default и blank (логические исключения). Во всех остальных случаях ограничения модели/базы данных связываются.

Я просто пытаюсь добиться согласованности в нечастичном сериализаторе. Если вы исключите особый случай для default , ваши существующие нечастичные сериализаторы станут строгим сериализатором, который мне нужен. Они также достигают паритета с ModelForm.

Я понимаю, что это создает небольшой разрыв в проекте, но это не первый раз, когда кто-то вносит подобные изменения. Добавьте «устаревший» флаг по умолчанию для текущего поведения, добавьте предупреждение (о том, что поведение по умолчанию изменится) и измените значение по умолчанию в следующем основном выпуске.

Что еще более важно, если вы хотите, чтобы ваши сериализаторы были новыми де-факто для Django, вы все равно в конечном итоге внесете это изменение. Количество людей, переходящих из ModelForm, значительно превысит существующую пользовательскую базу, и они будут ожидать, по крайней мере, этого изменения.

Вставляю свои два цента:
Я склонен согласиться с @claytondaley. PUT — это идемпотентная замена ресурса, PATCH — обновление существующего ресурса. Возьмем следующий пример:

class Profiles(models.Model):
    username = models.CharField()
    role = models.CharField(default='member', choices=(
        ('member', 'Member'), 
        ('moderator', 'Moderator'),
        ('admin', 'Admin'), 
    ))

Новые профили разумно имеют роль участника по умолчанию. Возьмем следующие запросы:

POST /profiles username=moe
PUT /profiles/1 username=curly
PATCH /profiles/1 username=larry&role=admin
PUT /profiles/1 username=curly

В настоящее время после первого PUT данные профиля будут содержать {'username': 'curly', 'role': 'member'} . После второго PUT у вас будет {'username': 'curly', 'role': 'admin'} . Не нарушает ли это идемпотентность? (Я не совсем уверен - я законно спрашиваю)

Редактировать:
Я думаю, что все согласны с семантикой PATCH.

После второго PUT у вас будет {'username': 'curly', 'role': 'admin'}

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

у меня еще никогда не было реальных проблем с этим

То же самое здесь, но до сих пор наши проекты полагались на PATCH :)
Тем не менее, вариант использования OP с управлением версиями модели для обработки параллелизма имеет для меня смысл. Я ожидаю, что PUT будет использовать значение по умолчанию (если значение опущено), вызывая исключение параллелизма.

Позвольте мне начать с признания того, что сериализатор не обязательно должен следовать RESTful RFC. Тем не менее, они должны по крайней мере предлагать режимы, которые _являются_ совместимыми, особенно в пакете, предлагающем поддержку REST.

Мой первоначальный аргумент исходил из первых принципов, но в RFC (раздел 4.3.4) конкретно говорится (выделено мной):

Фундаментальное различие между методами POST и PUT подчеркивается разным назначением вложенного представления. Целевой ресурс в запросе POST предназначен для обработки вложенного представления в соответствии с собственной семантикой ресурса, тогда как вложенное представление в запросе PUT определяется как замена состояния целевого ресурса.
...
Исходный сервер, который разрешает PUT для данного целевого ресурса, ДОЛЖЕН отправить ответ 400 (Bad Request) на запрос PUT, который содержит поле заголовка Content-Range (раздел 4.2 [RFC7233]), поскольку полезная нагрузка, вероятно, будет частичным контентом. это было ошибочно PUT как полное представление . Частичные обновления содержимого возможны путем нацеливания на отдельно идентифицированный ресурс, состояние которого перекрывает часть более крупного ресурса, или с помощью другого метода, специально определенного для частичных обновлений (например, метод PATCH, определенный в [RFC5789]).

Таким образом, PUT никогда не должен быть частичным (см. также здесь ). Однако раздел о PUT также разъясняет:

Метод PUT запрашивает, чтобы состояние целевого ресурса было создано или заменено состоянием, определенным представлением, включенным в полезную нагрузку сообщения запроса. Успешный PUT данного представления будет означать, что последующий GET для того же целевого ресурса приведет к отправке эквивалентного представления в ответе 200 (OK).

Пункт о GET (хотя и не обязательный) говорит в пользу моего «компромиссного» решения. Хотя вставка пробелов/значений по умолчанию удобна, она не обеспечивает такого поведения. Гвоздь в гроб, вероятно, заключается в том, что это решение сводит к минимуму путаницу, поскольку не будет пропущенных полей, вызывающих сомнения.

Очевидно, что PATCH — это указанный вариант для частичных обновлений, но он описывается как «набор инструкций», а не просто частичный PUT, поэтому меня это всегда немного беспокоит. В разделе POST (4.3.3) фактически говорится:

Метод POST запрашивает, чтобы целевой ресурс обрабатывал представление, включенное в запрос, в соответствии с собственной конкретной семантикой ресурса. Например, POST используется для следующих функций (среди прочего):

  • Предоставление блока данных, таких как поля, введенные в HTML-форму, в процесс обработки данных;

...

  • Добавление данных к существующему представлению (представлениям) ресурса.

Я думаю, что есть аргумент в пользу использования POST для частичных обновлений, поскольку:

  • концептуально изменение данных не отличается от добавления
  • POST разрешено использовать свои собственные правила, поэтому эти правила могут быть частичным обновлением.
  • эту операцию можно легко отличить от CREATE по наличию идентификатора

Даже если DRF не стремится к полному соответствию, нам нужен сериализатор, совместимый с операцией PUT спецификации (т. е. заменой всего объекта). Самый простой (и явно наименее запутанный) ответ — потребовать все поля. Также предполагается, что PUT по умолчанию не должен быть частичным, а частичные обновления должны использовать другое ключевое слово (PATCH или даже POST).

Я думаю, что только что столкнулся со своей первой проблемой PUT при переносе нашего приложения на drf3.4.x :)

<strong i="6">@cached_property</strong>
    def _writable_fields(self):
        return [
            field for field in self.fields.values()
            if (not field.read_only) or (field.default is not empty)
        ]

Это приводит к тому, что мои .validated_data содержат данные, которые я не предоставил в запросе PUT и не предоставил вручную в сериализаторе. Значения были получены из default= на уровне сериализатора. Таким образом, в основном, намереваясь обновить определенное поле, я также неожиданно перезаписываю некоторые из этих полей значениями по умолчанию.

Рад за меня, я использую пользовательский ModelSerializer, поэтому я могу легко решить проблему.

@pySilver Я не понимаю содержание последнего комментария.

@rpkilby «Давайте возьмем следующие запросы ... Разве это не нарушает идемпотентность»

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

Вот несколько вариантов поведения PUT .

  • Поля являются обязательными, за исключением случаев, когда required=False или у них есть default . (Существующий)
  • Все поля обязательны к заполнению. (Более строгая, более согласованная семантика полного обновления _ но_ неудобна, потому что на самом деле она более строгая, чем семантика первоначального создания для POST)
  • Поля не требуются (т.е. просто отражайте поведение PATCH)

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

Я полагаю, что в некоторых случаях использования может возникнуть проблема, если есть поле, которое не нужно предоставлять для запросов POST , но которое впоследствии требуется для запросов PUT . Более того, PUT-as-create сама по себе является допустимой операцией, поэтому опять же было бы странно, если бы она имела другую семантику «обязательности» для POST.

Если кто-то хочет продвинуться вперед, я бы _настоятельно_ предложил начать как сторонний пакет, который реализует разные базовые классы сериализатора. Затем мы можем ссылаться на это из документации сериализаторов. Если дело сделано правильно, то мы можем рассмотреть возможность адаптации поведения по умолчанию в какой-то момент в будущем.

@tomchristie, что я хочу сказать:

У меня есть сериализатор с полем language только для чтения и моделью:

class Book(models.Model):
      title = models.CharField(max_length=100)
      language = models.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'))

class BookUpdateSerialzier(serializers.ModelSerializer):
      # language is readonly, I dont want to let users update that field using this serializer
      language = serializers.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'), read_only=True)
      class Meta:
          model = MyModel
          fields = ('title', 'language', )

book = Book(title="To be or 42", language="pl")
book.save()

s = BookUpdateSerialzier(book, data={'title': 'Foobar'}, partial=True)
s.is_valid()
assert 'language' in s.validated_data # !!! 
assert 'pl' == s.validated_data # AssertionError... here :(
  • Я не передал language в запросе и не ожидаю увидеть это в проверенных данных. Будучи переданным в update , он перезапишет мой экземпляр значениями по умолчанию, несмотря на то, что объекту уже назначено какое-то значение, отличное от значения по умолчанию.
  • Было бы менее проблематично, если бы в этом случае validated_data['language'] равнялось бы book.language .

@pySilver - Да, это было решено в https://github.com/tomchristie/django-rest-framework/pull/4346 только сегодня.

Как оказалось, вам не нужно default= в поле сериализатора в вашем примере, так как у вас есть значение по умолчанию для ModelField .

@tomchristie Вы хотя бы согласны с тем, что текущее поведение PUT не соответствует спецификации RFC? И что оба моих предложения (требовать все или ввести значения по умолчанию) сделают это так?

@tomchristie отличные новости!

Как оказалось, вам не нужен default= в поле сериализатора в вашем примере, так как у вас есть значение по умолчанию в ModelField.

Да, я просто хотел сделать это очень явным для демонстрации.

Наконец-то стало понятно, что @tomchristie не выступает за/против поведения сериализатора изолированно. Я считаю, что его возражения проистекают (неявно) из требования, чтобы один сериализатор поддерживал все режимы REST. Это проявляется в его жалобах на то, как строгий сериализатор повлияет на POST. Поскольку режимы REST несовместимы, текущим решением является сериализатор, не специфицированный ни для одного режима.

Если это реальный корень возражения, давайте возьмем его прямо. Как один сериализатор может обеспечить поведение спецификаций для всех режимов REST? Мой импровизированный ответ заключается в том, что PARTIAL vs. NON-PARTIAL реализуется на неправильном уровне:

  • У нас есть частичные и нечастичные сериализаторы. Этот подход означает, что нам нужно несколько сериализаторов для поддержки поведения спецификации для всех режимов.
  • На самом деле нам нужна частичная и нечастичная проверка (или что-то в этом роде). Различные режимы REST должны запрашивать разные режимы проверки у сериализатора.

Чтобы обеспечить разделение задач, сериализатор не должен знать режим REST, поэтому его нельзя реализовать как сторонний сериализатор (и я подозреваю, что сериализатор даже не имеет доступа к этому режиму). Вместо этого DRF должен передать дополнительную информацию сериализатору (примерно replace=True для PUT ). Сериализатор может решить, как это реализовать (требовать все поля или ввести значения по умолчанию).

Очевидно, что это всего лишь грубое предложение, но, возможно, оно позволит выйти из тупика.

Более того, PUT-as-create сама по себе является допустимой операцией, поэтому опять же было бы странно, если бы она имела другую семантику «обязательности» для POST.

Я согласен с тем, что вы можете создавать с помощью PUT, но я не согласен с тем, что семантика одинакова. PUT работает с определенным ресурсом:

Метод PUT запрашивает, чтобы состояние целевого ресурса было создано или заменено состоянием, определенным представлением, включенным в полезную нагрузку сообщения запроса.

Поэтому я считаю, что семантика create на самом деле отличается:

  • ПОЧТАto /citizen/ ожидает, что будет сгенерирован SSN (номер социального страхования)
  • СТАВИТЬto /citizen/<SSN> обновляет данные для определенного SSN. Если в этом SSN нет данных, это приводит к созданию.

Поскольку «id» должен быть включен в URI PUT, вы можете обращаться с ним по мере необходимости. Напротив, «id» не является обязательным в POST.

Поскольку «id» должен быть включен в URI PUT, вы можете обращаться с ним по мере необходимости. Напротив, «id» не является обязательным в POST.

Верно. Я имел в виду конкретно тот факт, что предлагаемое изменение «заставить PUT строго требовать _все_ поля» будет означать, что PUT-as-create будет вести себя иначе, чем POST-as-create. если поля обязательны или нет.

Сказав, что я прихожу к ценности наличия опции PUT-is-strict поведения.

(Убедитесь, что _all_ поля строго обязательны в этом случае, убедитесь, что поля _no_ не требуются в PATCH, и используйте флаг required= для POST)

Как один сериализатор может обеспечить поведение спецификаций для всех режимов REST?

Мы можем различать создание, обновление и частичное обновление, учитывая то, как создается экземпляр сериализатора, поэтому я не думаю, что это проблема.

Вы уже заметили, что можете create использовать PUT или POST . У них разная семантика и разные требования, поэтому create не должен зависеть от режима REST. Я думаю, что различие действительно происходит как часть is_valid . Мы просим конкретный режим проверки:

  • нет проверки наличия поля (PATCH)
  • проверка на основе флагов required (POST)
  • строгая проверка наличия поля (PUT)

Избегая логики, связанной с ключевыми словами, из операций CRUD, мы также уменьшаем связь между сериализатором и DRF. Если бы режимы проверки можно было настраивать, они были бы полностью универсальными (даже если бы мы реализовали только 3 конкретных случая для наших 3 ключевых слов).

Вы делаете хорошую работу, аргументируя меня этой функциональностью, вот. :)

Различные «режимы проверки» при вызове .is_valid() — это переворот, который не пройдет.

Возможно, мы _могли бы_ рассмотреть вариант 'complete=True' для существующего модуля 'partial=True' kwarg. Это достаточно легко вписывалось бы в то, как все работает в настоящее время, и по-прежнему поддерживало бы случай «строгих полей».

Подходит ли сериализатор для решения этой проблемы? Это требование тесно связано с ключевыми словами REST, так что, возможно, это подходящее место для его применения. Для поддержки этого подхода сериализатору нужно только предоставить список полей, которые он принимает в качестве входных данных.

Еще немного в сторону ... есть ли где-нибудь хорошее обсуждение разделения (распределения) проблем Django? У меня возникли проблемы с тем, чтобы ограничить себя ответами, дружественными к Django, потому что я не знаю ответа на такие вопросы, как «почему проверка является частью сериализации». В документах по сериализации для 1.9 даже не упоминается проверка. И, строго из первого принципа, похоже:

  1. Модель должна отвечать за проверку внутренней согласованности и
  2. «Представление» (в данном случае процессор режима REST) ​​должно отвечать за соблюдение бизнес-правил (таких как RFC), связанных с этим представлением.

Если ответственность за проверку уходит, сериализаторы могут быть на 100% частичными (по умолчанию) и специализированными для правил ввода-вывода, таких как «только чтение». Построенный таким образом ModelSerializer будет поддерживать широкий спектр представлений.

Подходит ли сериализатор для решения этой проблемы?

да.

В документах по сериализации для 1.9 даже не упоминается проверка.

Встроенная сериализация Django бесполезна для Web APIS, она действительно ограничена выгрузкой и загрузкой фикстур.

Вы знаете архитектурные предположения как Django, так и DRF лучше меня, поэтому я должен полагаться на вас в том, как это сделать. Конечно, kwarg init имеет право на это... реконфигурация сериализатора «по требованию». Единственным ограничением является то, что их нельзя переконфигурировать «на лету», но я предполагаю, что экземпляры одноразовые, так что это не является серьезной проблемой.

Я собираюсь отложить это на данный момент. Мы можем переоценить после v3.7

Решать вам, ребята, но я хочу убедиться, что вы понимаете, что это не билет для добавления поддержки параллелизма. Настоящая проблема заключается в том, что один сериализатор не может правильно проверить как PUT, так и POST в текущей архитектуре. Параллелизм только что предоставил «проваленный тест».

TL;DR Вы можете понять, почему эта проблема заблокирована, начав с предложенного Томом исправления .

Таким образом, предлагаемое решение состоит в том, чтобы сделать все поля обязательными для запроса PUT . Есть (по крайней мере) две проблемы с этим подходом:

  1. Сериализаторы думают о действиях, а не о методах HTTP, поэтому однозначного сопоставления нет. Очевидным примером является create , потому что его разделяют PUT и POST . Обратите внимание, что функция create-by- PUT по умолчанию отключена, поэтому предлагаемое исправление, вероятно, лучше, чем ничего.
  2. Нам не нужно требовать все поля в PUT (настроение разделяют #3648, #4703). Если обнуляемое поле отсутствует, мы знаем, что оно может быть None. Если поле со значением по умолчанию отсутствует, мы знаем, что можем использовать значение по умолчанию. PUT s на самом деле имеют те же требования к полям (полученные из модели), что и POST .

Настоящая проблема заключается в том, как мы обрабатываем отсутствующие данные и основное предложение в № 3648, № 4703, и здесь остается правильное решение. Мы можем поддерживать все режимы HTTP (включая create-by- PUT ), если введем такую ​​концепцию, как if_missing_use_default . В моем первоначальном предложении это было представлено как замена partial , но проще (и может быть необходимо) думать об этом как об ортогональной концепции.

если мы введем такое понятие, как if_missing_use_default.

Ничто не мешает кому-либо реализовать либо это, либо строгое «требовать все поля» в качестве базового класса сериализатора и обернуть это как стороннюю библиотеку.

Мое мнение таково, что строгий режим «требовать все поля» также может превратить его в ядро, это очень очевидное поведение, и я понимаю, почему это было бы полезно.

Я не уверен, что «разрешить полям быть необязательными, но заменить все, используя значения по умолчанию модели, если они существуют». обновить себя). Если мы хотим более строгого поведения, мы должны просто иметь более строгое поведение.

В любом случае, правильный подход к этому — проверить его как сторонний пакет, а затем обновить нашу документацию, чтобы мы могли ссылаться на него.

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

С радостью возьму пулл-реквесты в качестве отправной точки для этого, и еще счастливее включить сторонний пакет, демонстрирующий такое поведение.

приближается к значению, имеющему возможность поведения PUT-is-strict.

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

Похоже, это будет иметь очень нелогичное поведение (например, поля «created_at», которые автоматически обновляются).

Поле created_at должно быть равно read_only (или исключено из сериализатора). В обоих этих случаях он не изменится (обычное поведение сериализатора). В нелогичном случае, когда поле не доступно только для чтения в сериализаторе, вы получите нелогичное поведение автоматического его изменения.

С радостью возьму пулл-реквесты в качестве отправной точки для этого, и еще счастливее включить сторонний пакет, демонстрирующий такое поведение.

Абсолютно. Вариант «использовать значения по умолчанию» является идеальным случаем для стороннего пакета, поскольку изменение представляет собой тривиальную оболочку вокруг (одного метода) существующего поведения и (если вы покупаете аргумент по умолчанию) работает для всех нечастичных сериализаторов.

tomchristie закрыл это 4 часа назад

Возможно, вы рассмотрите возможность добавления ярлыка, такого как «Добро пожаловать в PR» или «Плагин стороннего производителя», и оставить открытыми действительные / подтвержденные проблемы, подобные этой. Я часто ищу открытые проблемы, чтобы увидеть, сообщалось ли о проблеме, и ее прогресс в направлении решения. Я воспринимаю закрытые проблемы как «недействительные» или «исправленные». Смешивание нескольких «действительных, но закрытых» проблем с тысячами недействительных/исправленных проблем не способствует эффективному поиску (даже если вы знали, что они могут быть там).

Возможно, вы рассмотрите возможность добавления метки, например, «Добро пожаловать в PR» или «Плагин стороннего производителя».

Это было бы достаточно разумно, но мы хотели бы, чтобы наш трекер проблем отражал активную или действенную работу над самим проектом.

Для нас действительно важно стараться держать наши проблемы в узком кругу. Изменение приоритетов может означать, что мы иногда решим повторно открыть проблемы, которые мы ранее закрыли. Прямо сейчас я думаю, что это выпало из «основная команда хочет решить эту проблему в ближайшем будущем».

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

оставляя действительные/подтвержденные проблемы, подобные этой, открытыми.

Еще немного контекста стиля управления задачами — https://www.dabapps.com/blog/sustainable-open-source-management/

Была ли эта страница полезной?
0 / 5 - 0 рейтинги