Django-rest-framework: PUT 调用不会完全“替换目标资源的状态”

创建于 2016-06-30  ·  68评论  ·  资料来源: encode/django-rest-framework

编辑:对于问题的当前状态,跳到https://github.com/encode/django-rest-framework/issues/4231#issuecomment -332935943

===

我在使用 DRF 与数据库交互的应用程序上实现Optimistic Concurrency 库时遇到问题。 我试图:

  • 确认我看到的行为归因于 DRF
  • 确认这是预期的行为
  • 确定是否有任何实用的方法来克服这种行为

我最近在我的 Django 应用程序中添加了乐观并发。 为您节省 Wiki 查找:

  • 每个模型都有一个版本字段
  • 当编辑者编辑一个对象时,他们会得到他们正在编辑的对象的版本
  • 编辑器保存对象时,将包含的版本号与数据库进行比较
  • 如果版本匹配,则编辑器更新最新文档并保存
  • 如果版本不匹配,我们假设在编辑器加载和保存之间提交了“冲突”编辑,因此我们拒绝编辑
  • 如果缺少版本,我们将无法进行测试并应拒绝编辑

我有一个通过 DRF 交谈的旧版 UI。 旧版 UI 不处理版本号。 我预计这会导致并发错误,但事实并非如此。 如果我正确理解#3648 中的讨论:

  • DRF 将 PUT 与现有记录合并。 这会导致使用当前数据库 ID 填充缺失的版本 ID
  • 由于这始终提供匹配,因此省略此变量将始终破坏跨 DRF 通信的乐观并发系统
  • 〜没有简单的选项(例如使字段“必填”)来确保每次都提交数据。〜(编辑:您可以通过将其设为必填来解决该问题,如本评论所示

重现步骤

  1. 在模型上设置乐观并发字段
  2. 创建一个新实例并多次更新(以确保您不再拥有默认版本号)
  3. 通过 DRF 提交更新 (PUT),不包括版本 ID

    预期行为

缺少的版本 ID 不应与数据库匹配并导致并发问题。

实际行为

缺少的版本 ID 由 DRF 用当前 ID 填充,因此并发检查通过。

Enhancement

所有68条评论

好的,我不能保证我能够立即查看这张非常深入的票,因为即将发布的 3.4 版本具有优先权。 但是感谢您提供如此详细,经过深思熟虑的问题。 这很可能会以几周为单位,而不是几天或几个月。 如果您取得任何进展,自己有任何进一步的想法,请更新工单并随时通知我们。

行。 我很确定我的问题是两个因素的结合:

  1. DRF不需要 PUT 中的字段(即使它在模型中是必需的),因为它有一个默认值 (version=0)
  2. DRF将 PUT 字段与当前对象合并(不注入默认值)

因此,DRF 使用当前(数据库)值并打破并发控制。 问题的后半部分与#3648 中的讨论(也在上面引用)有关,并且#1445 中的(3.x 之前的)讨论似乎仍然相关。

我希望一个默认行为不正常的具体(并且越来越普遍)案例足以重新讨论关于 ModelSerializer 的“理想”行为。 显然,我对 DRF 的了解只有一英寸,但我的直觉是以下行为适用于必填字段和 PUT:

  • 使用非部分序列化程序时,我们应该接收值,使用默认值,或者(如果没有可用的默认值)引发验证错误。 模型范围的验证应仅适用于输入/默认值。
  • 使用部分序列化程序时,我们应该接收值或回退到当前值。 模型范围的验证应适用于该组合数据。
  • 我相信当前的“非部分”序列化器确实是准部分的:

    • 对于必填且没有默认值的字段,它是非部分的

    • 它是必需的并且具有默认值的字段的部分(因为不使用默认值)

    • 对于不需要的字段,它是部分的

我们无法更改上面的项目符号 (1) 或默认值变得无用(即使我们知道默认值,我们也需要输入)。 这意味着我们必须通过更改上面的#2 来解决这个问题。 我同意您在 #2683 中的论点:

模型默认值是模型默认值。 序列化程序应该忽略该值并将责任交给Model.object.create()来处理。

为了与关注点分离保持一致,更新应该创建一个实例(将所有默认值委托给模型)并将提交的值应用于该新实例。 这会导致 #3648 中请求的行为。

尝试描述迁移路径有助于突出当前行为的奇怪程度。 最终目标是

  1. 修复 ModelSerializer,
  2. 为这个准部分状态添加一个标志,并且
  3. 将该标志设为默认值(为了向后兼容)

那面旗帜叫什么名字? 当前的模型序列化器实际上是一个部分序列化器,它(有点随意)需要满足条件required==True and default==None的字段。 我们不能明确地使用partial标志而不破坏向后兼容性,因此我们需要一个新的(希望是临时的)标志。 我只剩下quasi_partial ,但我无法表达任意要求required==True and default==None是为什么我很清楚应该紧急弃用这种行为。

您可以在序列化程序的 Meta 中添加extra_kwargs ,使version成为必填字段。

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

谢谢@anoopmalev。 这将使我留在生产部门。

在“睡在上面”之后,我意识到有一个额外的皱纹。 我所说的一切都应该适用于序列化程序的字段。 如果序列化程序中不包含某个字段,则不应对其进行修改。 通过这种方式,所有 serilaizers 都是(并且应该是)非包含字段的部分。 这比我上面的“创建一个新实例”要复杂一些。

我认为这个问题需要简化为一个更受约束的提案才能向前推进。
在目前的状态下似乎广泛可行。
现在我要关闭这个 - 如果有人可以将其简化为所需行为的简洁、可操作的陈述,那么我们可以重新考虑。 在那之前,我认为它只是广泛的。

这是一个简洁的建议......对于非部分序列化程序:

  1. 对于未在序列化程序中(隐式或显式)列出或标记为只读的任何字段,保留现有值
  2. 对于所有其他字段,请使用第一个可用选项:

    1. 填充提交的值

    2. 填充默认值,包括blank和/或null隐含的值

    3. 引发异常

为清楚起见,验证是在此过程的最终产品上运行的。

即,您想在没有模型默认值的任何序列化程序字段上设置required=True以进行更新?

我说对了吗?

是的(还有更多)。 这就是我理解partial (所有字段可选)与non-partial (所有字段必需)区别的方式。 non-partial序列化程序唯一不需要字段的情况是存在默认值(狭义或广义定义)_因为如果没有提供值,序列化程序可以使用该默认值。_

斜体部分是 DRF 目前没有做的,也是我提案中更重要的变化。 当前的实现只是跳过了该字段。

我有第二个提议,但它真的是一个单独的问题,你想对“默认”的想法有多慷慨。 当前的行为是“严格的”,因为只有default被视为这样。 如果您_真的_ 想要减少所需数据的数量,您也可以将blank=True字段设为可选...假设缺少的值是空白值。

@claytondaley我以这种方式使用带有 DRF 的 OOL 2x:

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 系统所期望的行为。

@克莱顿达利

您已将 OOL 硬编码到序列化程序中。

我有吗? 除了现场要求之外,您是否在我的序列化程序中找到了任何 OOL 逻辑?

这是错误的地方,因为您有竞争条件。

对不起,我只是看不到这里的比赛条件。

我正在使用 django-concurrency 将 OOL 逻辑放入保存操作(它所属的位置)。

我也在使用django-concurrency :) 但那是模型级别,而不是序列化程序。 在序列化程序级别上,您只需要:

  • 确保始终需要 _version 字段(何时需要)
  • 确保您的序列化程序知道如何处理 OOL 错误(这部分我已经省略了)
  • 确保您的 apiview 知道如何处理 OOL 错误并使用可能的差异上下文引发 HTTP 409

实际上,我没有使用django-concurrency ,因为 auto 标记为“不会修复”的问题:当使用obj.save(update_fields=['one', 'two', 'tree'])时它绕过 OOL,我发现这是不好的做法,所以我分叉了包。

这是我之前提到的序列化程序缺少的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 字段始终是正确的。

顺便说一句,事实证明,我从https://github.com/gavinwahl/django-optimistic-lock而不是从django-concurrency借用了模型 lvl 代码,这几乎没有理由变得复杂。

...所以错误是“非部分序列化程序错误地将某些字段设置为不需要”。 那是替代方案。 因为这是非部分序列化程序做出的(隐式)承诺。

可以引用它

默认情况下,序列化程序必须为所有必填字段传递值,否则它们会引发验证错误。

这没有说明需要(除非提供了默认值)。

(我知道我在谈论两个不同的级别,但是如果 ModelSerializer 不对该决定负责,它不应该取消要求字段)

我想我已经失去了你的意思..

(我知道我在谈论两个不同的级别,但是如果 ModelSerializer 不对该决定负责,它不应该取消要求字段)

那有什么问题?

好的,让我尝试不同的角度。

  • 假设我有一个覆盖模型中所有字段的非部分模型序列化器(编辑:所有默认值)。

具有相同数据的 CREATE 或 UPDATE 是否应该生成不同的对象(减去 ID)

您能否使用一些非常简单的模型和序列化程序以及显示失败/预期行为的几行来描述您的想法?

明天我会整理一些东西,因为这里已经很晚了......但是我越深入,#3648 对非部分序列化程序的意义就越大。 同时,为什么 ModelSerializer 不需要模型中的所有字段? 也许你的理由和我的不一样。

ModelSerializer 检查有界模型并决定是否需要它,不是吗?

我不是说机械地如何。 非部分序列化器的基本假设是需要所有东西(上面引用)。 如果get_field_kwargs会偏离这个假设(特别是,这里),它应该有一个很好的理由。 那是什么原因?

我首选的答案是我一直给出的答案,“因为如果没有提交任何值,它可以使用该默认值”(但 DRF 必须实际使用默认值)。 我还缺少另一个答案吗? 为什么不需要具有默认值的字段的原因?

显然,我更喜欢“完整”的解决方案。 但是,我承认还有第二个答案。 默认情况下,我们可以要求这些字段。 这消除了(当前任意的)特殊情况。 它简化/减少了代码。 这是内部一致的。 它解决了我的担忧。

基本上,它使非部分序列化程序真正非部分。

现在我至少知道你的意思了。 在这种情况下,您是否检查过 ModelForm 的行为是什么? (不能自己在手机上执行此操作)

Django docs 说“空白”控制是否需要字段。 我建议你应该为这个问题单独开一张票,因为这张票包含很多不相关的评论。 在我看来,modelserializer 可能像 modelform: blank 选项控件一样工作,'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原则上同意。 但是预期的行为是什么?

  • 在创建时,我得到默认值(微不足道,每个人都同意这是正确的)
  • 更新时,我应该得到什么?

在我看来,我也应该在更新时获得默认值。 我不明白为什么非部分序列化程序在这两种情况下的行为会有所不同。 非部分意味着我正在发送“完整”记录。 因此应该替换完整的记录。

如果更新时未提供该值,我希望该值保持不变。 我明白了这一点,但是从我的 POV 来看,用默认值透明地覆盖是违反直觉的。

(如果有什么我认为对于所有字段来说,所有更新都是部分语义实际上可能会更好 - PUT 仍然是幂等的,这是重要的方面,但在给定当前行为的情况下更改可能会很尴尬)

我当然不会分享你的偏好。 我希望我的所有接口都是严格的,除非我故意不这样做。 但是,您的 PARTIAL 与 NON-PARTIAL 区别已经(理论上)提供了我们都想要的东西。

我相信 partial 的行为完全符合您的要求:

  • 更新是 100% 部分的
  • CREATE(我假设)对于defaultblank (逻辑异常)是部分的。 在所有其他情况下,模型/数据库约束绑定。

我只是想在非部分序列化程序中获得一致性。 如果您消除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。 但是,它们至少应该提供_are_ 兼容的模式——尤其是在提供REST 支持的包中。

我最初的论点来自第一原则,但RFC (第 4.3.4 节)特别指出(强调添加):

POST 和 PUT 方法之间的根本区别在于封闭表示的不同意图。 POST 请求中的目标资源旨在根据资源自身的语义处理封闭表示,而 PUT 请求中的封闭表示被定义为替换目标资源的状态。
...
允许对给定目标资源进行 PUT 的源服务器必须向包含 Content-Range 标头字段([RFC7233] 的第 4.2 节)的 PUT 请求发送 400(错误请求)响应,因为有效负载可能是部分内容那被错误地 PUT 作为一个完整的表示。 部分内容更新是可能的,方法是针对一个单独标识的资源,其状态与较大资源的一部分重叠,或者使用专门为部分更新定义的不同方法(例如,[RFC5789] 中定义的 PATCH 方法)

因此,PUT 永远不应该是部分的(另请参见此处)。 但是,关于 PUT 的部分也澄清了:

PUT 方法请求创建目标资源的状态或将其替换为请求消息有效负载中包含的表示定义的状态。 给定表示的成功 PUT 将表明对同一目标资源的后续 GET 将导致在 200(OK)响应中发送等效表示。

关于 GET 的观点(虽然不是强制性的)支持我的“妥协”解决方案。 虽然注入空白/默认值很方便,但它不会提供这种行为。 棺材上的钉子可能是这种解决方案最大限度地减少了混乱,因为不会有任何缺失的领域引起怀疑。

显然,PATCH 是部分更新的指定选项,但它被描述为“一组指令”而不仅仅是部分 PUT,所以它总是让我有点坐立不安。 关于 POST (4.3.3) 的部分实际上指出:

POST 方法请求目标资源根据资源自己的特定语义处理请求中包含的表示。 例如,POST 用于以下功能(以及其他功能):

  • 向数据处理过程提供数据块,例如输入到 HTML 表单中的字段;

...

  • 将数据附加到资源的现有表示中。

我认为使用 POST 进行部分更新是有争议的,因为:

  • 从概念上讲,修改数据与追加没有什么不同
  • POST 允许使用自己的规则,因此这些规则可以是部分更新
  • 通过 ID 的存在可以很容易地将此​​操作与 CREATE 区分开来

即使 DRF 不希望完全合规,我们也需要一个与规范 PUT 操作兼容的序列化程序(即替换整个对象)。 最简单(并且显然最不容易混淆)的答案是要求所有字段。 它还建议默认情况下 PUT 应该是非部分的,并且部分更新应该使用不同的关键字(PATCH 甚至 POST)。

我想我在将我们的应用程序迁移到 drf3.4.x 时遇到了我的第一个 PUT 问题 :)

<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 与 NON-PARTIAL 的实施水平错误:

  • 我们有部分和非部分序列化器。 这种方法意味着我们需要多个序列化器来支持所有模式的规范行为。
  • 我们实际上需要部分与非部分验证(或类似的东西)。 不同的 REST 模式需要向序列化器请求不同的验证模式。

为了提供关注点分离,序列化程序不应该知道 REST 模式,因此它不能作为第 3 方序列化程序实现(我怀疑,序列化程序甚至可以访问该模式)。 相反,DRF 应该向序列化程序传递一条额外的信息(对于PUT $ 来说,大约是replace=True #$)。 序列化程序可以决定如何实现这一点(需要所有字段或注入默认值)。

显然,这只是一个粗略的提议,但也许会打破僵局。

此外,PUT-as-create 本身就是一个有效的操作,因此,如果它与 POST 具有不同的“必需”语义,那就太奇怪了。

我同意您可以使用 PUT 创建,但我不同意语义是相同的。 PUT适用于特定资源:

PUT 方法请求创建目标资源的状态或将其替换为请求消息有效负载中包含的表示定义的状态。

因此,我相信 create 语义实际上是不同的:

  • 邮政to /citizen/期望生成一个 SSN(社会安全号码)
  • to /citizen/<SSN>更新特定 SSN 的数据。 如果该 SSN 上没有数据,则会导致创建。

因为 PUT 的 URI 中必须包含“id”,所以可以按需要处理。 相比之下,“id”在 POST 中是可选的。

因为 PUT 的 URI 中必须包含“id”,所以可以按需要处理。 相比之下,“id”在 POST 中是可选的。

的确。 我特别指的是“使 PUT 严格要求 _all_ 字段”的提议更改将意味着 PUT-as-create 将具有与 POST-as-create wrt 不同的行为。 是否需要字段。

话虽如此,我正在考虑选择 PUT-is-strict 行为的价值。

(强制在这种情况下严格要求 _all_ 字段,强制在 PATCH 中要求 _no_ 字段,并为 POST 使用required=标志)

单个序列化程序如何为所有 REST 模式提供规范行为?

鉴于序列化程序的实例化方式,我们可以区分创建、更新和部分更新,所以我认为这不是问题。

您已经指出可以使用PUTPOST create 它们具有不同的语义和不同的要求,因此create需要与 REST 模式无关。 我认为这种区别确实是is_valid的一部分。 我们要求特定的验证模式:

  • 没有现场存在验证 (PATCH)
  • 基于required标志的验证 (POST)
  • 严格的现场存在验证 (PUT)

通过将特定于关键字的逻辑排除在 CRUD 操作之外,我们还减少了序列化程序和 DRF 之间的耦合。 如果验证模式是可配置的,那么它们将是完全通用的(即使我们只为 3 个关键字实现了 3 个特定案例)。

你很好地证明了我的这个功能,在那里。 :)

调用 .is_valid() 时的不同“验证模式”是一场不会发生的剧变。

我们_可以_考虑现有的 'partial=True' 单位 kwarg 的 'complete=True' 对应物。 这很容易适应当前的工作方式,并且仍然支持“严格领域”的情况。

序列化器是解决这个问题的正确地方吗? 此要求与 REST 关键字紧密耦合,因此也许这是实施它的正确位置。 为了支持这种方法,序列化程序只需要公开它接受作为输入的字段列表,

更重要的是......是否有关于 Django 在某处分离(分配)关注点的好讨论? 我无法将自己限制为对 Django 友好的答案,因为我不知道诸如“为什么是序列化的验证部分”之类的问题的答案。 1.9 的序列化文档甚至没有提到验证。 而且,严格按照第一原则,它看起来像:

  1. 该模型应负责验证内部一致性和
  2. “视图”(在本例中为 REST 模式处理器)应负责执行与该视图相关的业务规则(如 RFC)。

如果验证的责任消失,序列化程序可以是 100% 部分的(默认情况下)并专门用于“只读”等 I/O 规则。 以这种方式构建的 ModelSerializer 将支持多种视图。

序列化器是解决这个问题的正确地方吗?

是的。

1.9 的序列化文档甚至没有提到验证。

Django 的内置序列化对 Web APIS 没有用处,它实际上仅限于转储和加载固定装置。

你比我更了解 Django 和 DRF 的架构假设,所以我必须听从你的意见。 当然init kwarg 对它有正确的感觉......“按需”重新配置序列化程序。 唯一的限制是它们不能“即时”重新配置,但我认为这些实例是一次性的,所以这不是一个重大问题。

我现在要取消里程碑。 v3.7之后我们可以重新评估

由你们决定,但我想确保你们清楚这不是添加并发支持的票。 真正的问题是单个序列化程序无法正确验证当前架构中的 PUT 和 POST。 并发只是提供了“失败的测试”。

TL;DR 您可以从Tom 提出的修复开始了解为什么此问题被阻止。

总之,建议的解决方案是使PUT请求所需的所有字段。 这种方法有(至少)两个问题:

  1. 序列化程序考虑的是动作而不是 HTTP 方法,因此不存在一对一的映射。 最明显的例子是create因为它由PUTPOST共享。 请注意,create-by- PUT默认禁用,因此建议的修复可能总比没有好。
  2. 我们不需要要求PUT中的所有字段(#3648、#4703 共享的情绪)。 如果一个 nillable 字段不存在,我们知道它可以是 None。 如果缺少具有默认值的字段,我们知道我们可以使用默认值。 PUT实际上与POST具有相同的(模型派生的)字段要求。

真正的问题是我们如何处理丢失的数据和#3648、#4703 中的基本建议,这里仍然是正确的解决方案。 如果我们引入像if_missing_use_default这样的概念,我们可以支持所有的 HTTP 模式(包括按PUT创建)。 我最初的提议将其作为partial的替代品,但将其视为正交概念更容易(并且可能是必要的)。

如果我们引入一个类似 if_missing_use_default 的概念。

没有什么可以阻止任何人实现这一点,或者将严格的“要求所有字段”作为基本序列化程序类,并将其包装为第三方库。

我的观点是,严格的“要求所有字段”模式也可能能够使其成为核心,这是非常明显的行为,我明白为什么这很有用。

我不相信“允许字段是可选的,但替换所有内容,如果它们存在则使用模型默认值” - 这似乎会呈现一些非常违反直觉的行为(例如“created_at”字段,自动结束更新自己)。 如果我们想要更严格的行为,我们就应该有更严格的行为。

无论哪种方式,解决此问题的正确方法是将其验证为第三方包,然后更新我们的文档以便我们可以链接到它。

或者,如果您确信我们缺少用户真正需要的核心行为,那么欢迎您提出拉取请求,更新行为和文档,这样我们就可以非常快速地评估优点具体方式。

很高兴将拉取请求作为这一点的起点,甚至更乐意包含一个展示这种行为的第三方包。

接近具有 PUT-is-strict 行为选项的价值。

这仍然成立。 我认为我们可以在核心中考虑这一方面,如果有人足够关心它以按照这些思路提出拉取请求。 它需要是一个可选的行为。

这似乎会呈现一些非常违反直觉的行为(例如,“created_at”字段,最终会自动更新自己)。

created_at字段应该是read_only (或从序列化程序中排除)。 在这两种情况下,它都不会改变(正常的序列化程序行为)。 在序列化程序中字段不是只读的反直觉情况下,您将获得自动更改它的反直觉行为。

很高兴将拉取请求作为这一点的起点,甚至更乐意包含一个展示这种行为的第三方包。

绝对地。 “使用默认值”变体是第 3 方包的理想情况,因为更改是对现有行为(一种方法)的微不足道的包装,并且(如果您购买默认参数)适用于所有非部分序列化程序。

tomchristie 4 小时前关闭

也许您会考虑添加“PR 欢迎”或“第 3 方插件”之类的标签,并保留此类有效/​​已确认的问题。 我经常搜索未解决的问题,以查看是否已报告问题以及解决问题的进展情况。 我将关闭的问题视为“无效”或“已修复”。 将一些“有效但已关闭”的问题混入数以千计的无效/已修复问题中不会引起有效的搜索(即使您知道它们可能存在)。

也许你会考虑添加一个标签,比如“PR Welcome”或“3rd Party Plugin”

这很合理,但我们希望我们的问题跟踪器能够反映项目本身的积极或可操作的工作。

对我们来说,努力将我们的问题保持在严格的范围内是非常重要的。 改变优先级可能意味着我们有时会选择重新打开我们之前关闭的问题。 现在我认为这已经脱离了“核心团队希望在不久的将来解决这个问题”。

如果它反复出现,并且仍然没有第三方解决方案,那么也许我们会重新评估它。

将此类有效/​​已确认的问题保持开放。

关于问题管理风格的更多背景信息 - https://www.dabapps.com/blog/sustainable-open-source-management/

此页面是否有帮助?
0 / 5 - 0 等级