如何基于Spring在强类型语言中正确执行PATCH-示例 [英] How to do PATCH properly in strongly typed languages based on Spring - example

查看:96
本文介绍了如何基于Spring在强类型语言中正确执行PATCH-示例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

据我所知:

  • PUT-更新对象的整体表示形式(替换)
  • PATCH-仅使用给定字段更新对象(更新)
  • PUT - update object with its whole representation (replace)
  • PATCH - update object with given fields only (update)

我正在使用Spring来实现一个非常简单的HTTP服务器.当用户想要更新其数据时,他需要对某个端点(例如:api/user)进行HTTP PATCH设置.他的请求正文通过@RequestBody映射到DTO,如下所示:

I'm using Spring to implement a pretty simple HTTP server. When a user wants to update his data he needs to make a HTTP PATCH to some endpoint (let's say: api/user). His request body is mapped to a DTO via @RequestBody, which looks like this:

class PatchUserRequest {
    @Email
    @Length(min = 5, max = 50)
    var email: String? = null

    @Length(max = 100)
    var name: String? = null
    ...
}

然后,我使用此类的对象来更新(修补)用户对象:

Then I use an object of this class to update (patch) the user object:

fun patchWithRequest(userRequest: PatchUserRequest) {
    if (!userRequest.email.isNullOrEmpty()) {
        email = userRequest.email!!
    }
    if (!userRequest.name.isNullOrEmpty()) {
        name = userRequest.name
    }    
    ...
}

我的疑问是:如果客户端(例如Web应用程序)想要清除财产该怎么办?我会忽略这样的变化.

My doubt is: what if a client (web app for example) would like to clear a property? I would ignore such a change.

我怎么知道,如果用户想清除某个属性(他故意向我发送空值),或者只是不想更改它?在这两种情况下,我的对象都为null.

我在这里可以看到两个选项:

I can see two options here:

  • 与客户达成一致,如果他想删除某个属性,他应该给我发送一个空字符串(但是日期和其他非字符串类型呢?)
  • 停止使用DTO映射,并使用一个简单的映射,这将使我检查字段是否为空或根本没有给出.那么请求主体验证又如何呢?我现在使用@Valid.

应如何与REST和所有良好实践相协调地妥善处理此类案件?

How should such cases should be properly handled, in harmony with REST and all good practices?

可以说在这样的示例中不应该使用PATCH,我应该使用PUT来更新我的用户.但是,如何进行模型更改(例如添加新属性)?每次更改用户后,我都必须对API(或仅对用户端点)进行版本控制.例如.我将拥有api/v1/user端点,该端点接受带有旧请求正文的PUT,以及api/v2/user端点,该端点接受带有新请求正文的PUT.我想这不是解决方案,PATCH存在是有原因的.

One could say that PATCH shouldn't be used in such an example, and I should use PUT to update my User. But what about model changes (e.g. adding a new property)? I would have to version my API (or the user endpoint alone) after every User change. E.g. I would have api/v1/user endpoint which accepts PUT with an old request body, and api/v2/user endpoint which accepts PUT with a new request body. I guess it's not the solution and PATCH exists for a reason.

推荐答案

TL; DR

patchy 是我提出的一个很小的库注意在Spring中正确处理PATCH所需的主要样板代码,即:

TL;DR

patchy is a tiny library I've come up with that takes care of the major boilerplate code needed to properly handle PATCH in Spring i.e.:

class Request : PatchyRequest {
    @get:NotBlank
    val name:String? by { _changes }

    override var _changes = mapOf<String,Any?>()
}

@RestController
class PatchingCtrl {
    @RequestMapping("/", method = arrayOf(RequestMethod.PATCH))
    fun update(@Valid request: Request){
        request.applyChangesTo(entity)
    }
}

简单的解决方案

由于 PATCH 请求表示要应用于资源的更改,因此我们需要对其建模明确地.

Simple solution

Since PATCH request represent changes to be applied to the resource we need to model it explicitly.

一种方法是使用普通的Map<String,Any?>,其中客户端提交的每个key都代表对资源相应属性的更改:

One way is to use a plain old Map<String,Any?> where every key submitted by a client would represent a change to the corresponding attribute of the resource:

@RequestMapping("/entity/{id}", method = arrayOf(RequestMethod.PATCH))
fun update(@RequestBody changes:Map<String,Any?>, @PathVariable id:Long) {
    val entity = db.find<Entity>(id)
    changes.forEach { entry ->
        when(entry.key){
            "firstName" -> entity.firstName = entry.value?.toString() 
            "lastName" -> entity.lastName = entry.value?.toString() 
        }
    }
    db.save(entity)
}

上面的内容很容易理解:

The above is very easy to follow however:

  • 我们没有对请求值的验证

可以通过在域层对象上引入验证注释来缓解上述问题.尽管这在简单的场景中非常方便,但是一旦我们引入在域上强制不变式似乎更为实用.层,但将验证保留在边缘.

The above can be mitigated by introducing validation annotations on the domain layer objects. While this is very convenient in simple scenarios it tends to be impractical as soon as we introduce conditional validation depending on the state of the domain object or on the role of the principal performing a change. More importantly after the product lives for a while and new validation rules are introduced it's pretty common to still allow for an entity to be update in non user edit contexts. It seems to be more pragmatic to enforce invariants on the domain layer but keep the validation at the edges.

  • 在很多地方都非常相似

这实际上很容易解决,在80%的情况下,以下方法会起作用:

This is actually very easy to tackle and in 80% of cases the following would work:

fun Map<String,Any?>.applyTo(entity:Any) {
    val entityEditor = BeanWrapperImpl(entity)
    forEach { entry ->
        if(entityEditor.isWritableProperty(entry.key)){
            entityEditor.setPropertyValue(entry.key, entityEditor.convertForProperty(entry.value, entry.key))
        }
    }
}

验证请求

由于 Kotlin中的委派属性,围绕它构建包装器非常容易Map<String,Any?>:

Validating the request

Thanks to delegated properties in Kotlin it's very easy to build a wrapper around Map<String,Any?>:

class NameChangeRequest(val changes: Map<String, Any?> = mapOf()) {
    @get:NotBlank
    val firstName: String? by changes
    @get:NotBlank
    val lastName: String? by changes
}

并使用

And using Validator interface we can filter out errors related to attributes not present in the request like so:

fun filterOutFieldErrorsNotPresentInTheRequest(target:Any, attributesFromRequest: Map<String, Any?>?, source: Errors): BeanPropertyBindingResult {
    val attributes = attributesFromRequest ?: emptyMap()
    return BeanPropertyBindingResult(target, source.objectName).apply {
        source.allErrors.forEach { e ->
            if (e is FieldError) {
                if (attributes.containsKey(e.field)) {
                    addError(e)
                }
            } else {
                addError(e)
            }
        }
    }
}

显然,我们可以使用 HandlerMethodArgumentResolver ,我在下面做了.

Obviously we can streamline the development with HandlerMethodArgumentResolver which I did below.

我认为将上述内容包装到一个易于使用的库中是有道理的-看哪 patchy .有了 patchy ,就可以拥有一个强类型的请求输入模型以及声明式验证.您要做的就是导入配置@Import(PatchyConfiguration::class)并在模型中实现PatchyRequest接口.

I thought that it would make sense to wrap what've described above into a simple to use library - behold patchy. With patchy one can have a strongly typed request input model along with declarative validations. All you have to do is to import the configuration @Import(PatchyConfiguration::class) and implement PatchyRequest interface in your model.

  • Spring Sync
  • fge/json-patch

这篇关于如何基于Spring在强类型语言中正确执行PATCH-示例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆