programing

스프링에 기반한 강력한 유형의 언어로 PATCH를 적절하게 실행하는 방법 -

bestprogram 2023. 4. 2. 11:59

스프링에 기반한 강력한 유형의 언어로 PATCH를 적절하게 실행하는 방법 -

내가 아는 바로는:

  • PUT(바꾸기 - 개체 전체 표시(바꾸기)
  • PATCH- update (update - "update object" (업데이트

봄의 HTTP입니다..PATCH(를 들어, (예를 들면) 어떤까지.api/user) DTO에 @RequestBody뭇매를 맞다

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

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

그런 다음 이 클래스의 개체를 사용하여 사용자 개체를 업데이트(패치)합니다.

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

의문점은 클라이언트(예: 웹 앱)가 자산을 클리어하고 싶다면 어떻게 해야 한다는 것입니다.나는 그런 변화를 무시할 것이다.

사용자가 속성을 클리어하고 싶은 경우(그가 의도적으로 null을 전송한 경우) 또는 변경을 원하지 않는 경우 어떻게 알 수 있습니까?어느 경우든 내 목적에서는 무효가 될 것이다.

여기에는 다음 두 가지 옵션이 있습니다.

  • 속성을 삭제할 경우 빈 문자열을 보내야 한다는 클라이언트의 의견에 동의합니다(단, 날짜 및 문자열 이외의 유형은 어떻게 됩니까?).
  • DTO 매핑 사용을 중지하고 간단한 맵을 사용합니다. 그러면 필드가 비어 있는지 확인할 수 있습니다.럼럼본 ?증 ?? ??? 용 i i i i를 쓴다.@Valid지금 당장.

REST 및 모든 모범 사례와 조화를 이루면서 이러한 사례를 어떻게 적절하게 처리해야 하는가?

편집:

라고 수 PATCH요, 이런 예에서는 안 돼요.PUT사용자를 갱신합니다.그러나 모델 변경(예: 새 속성 추가)은 어떻습니까?사용자가 변경될 때마다 API(또는 사용자 엔드포인트만) 버전을 만들어야 합니다.예를 들어, 저는api/v1/user「」를 수신하는 PUT본문 및 "" "" " " " " " " " " " 를 사용합니다.api/v2/user「」를 수신하는 PUT새로운 요청 기관과 함께.그게 해결책이 아닌 것 같고PATCH유가있있 있있있다다

TL;DR

패치는 내가 생각해낸 작은 라이브러리이다. 적절하게 처리하는데 필요한 주요 보일러 플레이트 코드를 관리하는PATCH ,,, : :

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)
    }
}

심플한 솔루션

요청은 자원에 적용되는 변경을 나타내기 때문에 명시적으로 모델링해야 합니다.

가지 옛것을 입니다.Map<String,Any?> 곳에서나key클라이언트에 의해 송신된 것은, 자원의 대응하는 어트리뷰트의 변경을 나타냅니다.

@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)
}

단, 위의 내용은 매우 쉽게 이해할 수 있습니다.

  • 요청 값의 검증이 없습니다.

도메인 레이어 오브젝트에 검증 주석을 도입함으로써 위의 문제를 완화할 수 있습니다.이것은 간단한 시나리오에서는 매우 편리하지만 도메인 객체의 상태나 변경을 실행하는 주체의 역할에 따라 조건부 검증을 도입하자마자 실용적이지 않은 경향이 있습니다.더 중요한 것은 제품이 한동안 존속하고 새로운 검증 규칙이 도입된 후에도 여전히 사용자 편집 컨텍스트 이외의 컨텍스트에서 엔티티를 업데이트할 수 있도록 허용하는 것이 매우 일반적입니다.도메인 계층에 불변성을 적용하되 검증을 가장자리에 두는 이 더 실용적인 것으로 보입니다.

  • 잠재적으로 많은 장소에서 매우 유사할 것이다.

이는 실제로 매우 쉽게 해결할 수 있으며, 80%의 경우 다음이 작동합니다.

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?>:

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

또한 인터페이스를 사용하여 다음과 같이 요청에 없는 속성과 관련된 오류를 필터링할 수 있습니다.

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)
            }
        }
    }
}

분명히 우리는 내가 아래와 같이 한 개발을 능률화할 수 있다.

가장 심플한 솔루션

위에서 설명한 것을 사용하기 쉬운 라이브러리로 정리하는 것이 타당하다고 생각했습니다.패치한 것을 봐 주세요.패치가 적용되면 선언적 검증과 함께 강력한 유형의 요청 입력 모델을 사용할 수 있습니다.설정을 Import하기만 하면 됩니다.@Import(PatchyConfiguration::class)를 구현합니다.PatchyRequest인터페이스를 사용합니다.

추가 정보

저도 같은 문제가 있었습니다만, 제 경험/해결 방법을 소개하겠습니다.

패치를 그대로 도입하는 것이 좋습니다.그러면

  • 키에 값이 있다> 값이 설정되어 있다.
  • 키가 빈 문자열과 함께 존재함> 빈 문자열이 설정되어 있다.
  • 키가 null 값으로 존재함> 필드가 null로 설정되어 있다.
  • 키가 존재하지 않는다> 그 키의 값은 변경되지 않는다.

그렇게 하지 않으면 곧 이해하기 어려운 api를 얻을 수 있습니다.

그래서 당신의 첫 번째 선택지는 포기하겠습니다.

속성을 삭제할 경우 빈 문자열을 보내야 한다는 클라이언트의 의견에 동의합니다(단, 날짜 및 문자열 이외의 유형은 어떻게 됩니까?).

제 생각에 두 번째 옵션은 사실 좋은 선택입니다.그리고 그것이 우리가 한 일이기도 하다.

이 옵션에서 검증 속성을 사용할 수 있는지 잘 모르겠습니다만, 이 검증이 도메인 계층에 있지 않아야 합니까?이로 인해 나머지 계층에서 처리되어 잘못된 요청으로 변환되는 도메인에서 예외가 발생할 수 있습니다.

하나의 어플리케이션에서는 다음과 같이 처리했습니다.

class PatchUserRequest {
  private boolean containsName = false;
  private String name;

  private boolean containsEmail = false;
  private String email;

  @Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
  void setName(String name) {
    this.containsName = true;
    this.name = name;
  }

  boolean containsName() {
    return containsName;
  }

  String getName() {
    return name;
  }
}
...

json deserializer는 PatchUserRequest를 인스턴스화하지만 존재하는 필드의 setter 메서드만 호출합니다.따라서 누락된 필드의 부울 포함은 false인 채로 유지됩니다.

다른 앱에서는 같은 원리로 조금 다른 것을 사용했습니다.(이쪽이 더 좋습니다)

class PatchUserRequest {
  private static final String NAME_KEY = "name";

  private Map<String, ?> fields = new HashMap<>();;

  @Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
  void setName(String name) {
    fields.put(NAME_KEY, name);
  }

  boolean containsName() {
    return fields.containsKey(NAME_KEY);
  }

  String getName() {
    return (String) fields.get(NAME_KEY);
  }
}
...

PatchUserRequest가 Map을 확장하도록 허용하여 동일한 작업을 수행할 수도 있습니다.

또 다른 옵션은 json deserializer를 직접 작성하는 것일 수 있지만, 저는 그것을 직접 사용해 본 적이 없습니다.

이러한 예에서는 PATCH를 사용하지 않고 PUT을 사용하여 사용자를 업데이트해야 한다고 말할 수 있습니다.

나는 이것에 동의하지 않는다.PATCH & PUT도 말씀하신 대로 사용하고 있습니다.

  • PUT - 전체 표현으로 개체를 업데이트합니다(바꾸기).
  • 패치 - 지정된 필드만 사용하여 개체를 업데이트합니다(업데이트).

지적하신 바와 같이 주요 문제는 명시적 눌과 암묵적 눌을 구별하는 여러 개의 눌 유사 값이 없다는 것입니다.이 질문에 Kotlin이라는 태그를 붙였기 때문에 위임된 속성 및 속성 참조를 사용하는 솔루션을 생각해 내려고 했습니다.Spring Boot에서 사용하는 잭슨과 투과적으로 동작하는 것이 중요한 제약사항입니다.

이 방법은 위임된 속성을 사용하여 명시적으로 null로 설정된 필드를 자동으로 저장하는 것입니다.

먼저 대리인을 정의합니다.

class ExpNull<R, T>(private val explicitNulls: MutableSet<KProperty<*>>) {
    private var v: T? = null
    operator fun getValue(thisRef: R, property: KProperty<*>) = v
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T) {
        if (value == null) explicitNulls += property
        else explicitNulls -= property
        v = value
    }
}

이것은 속성의 프록시처럼 동작하지만 null 속성을 지정된 위치에 저장합니다.MutableSet.

이제 네 안에DTO:

class User {
    val explicitNulls = mutableSetOf<KProperty<*>>() 
    var name: String? by ExpNull(explicitNulls)
}

용도는 다음과 같습니다.

@Test fun `test with missing field`() {
    val json = "{}"

    val user = ObjectMapper().readValue(json, User::class.java)
    assertTrue(user.name == null)
    assertTrue(user.explicitNulls.isEmpty())
}

@Test fun `test with explicit null`() {
    val json = "{\"name\": null}"

    val user = ObjectMapper().readValue(json, User::class.java)
    assertTrue(user.name == null)
    assertEquals(user.explicitNulls, setOf(User::name))
}

이건 잭슨이 분명히 전화했으니까user.setName(null)두 번째 케이스에서는 콜을 생략합니다.

물론 DTO가 구현해야 하는 인터페이스에 조금 더 고급스럽게 몇 가지 메서드를 추가할 수 있습니다.

interface ExpNullable {
    val explicitNulls: Set<KProperty<*>>

    fun isExplicitNull(property: KProperty<*>) = property in explicitNulls
}

그래서 수표가 좀 더 좋아지죠user.isExplicitNull(User::name).

가지 에서 하는 은 '어플리케이션'을 만드는 입니다.OptionalInput할 수 클래스:

class OptionalInput<T> {

    private boolean _isSet = false

    @Valid
    private T value

    void set(T value) {
        this._isSet = true
        this.value = value
    }

    T get() {
        return this.value
    }

    boolean isSet() {
        return this._isSet
    }
}

그런 다음 요청 클래스에서 다음을 수행합니다.

class PatchUserRequest {

    @OptionalInputLength(max = 100L)
    final OptionalInput<String> name = new OptionalInput<>()

    void setName(String name) {
        this.name.set(name)
    }
}

하려면 , 「」를 합니다.@OptionalInputLength.

사용방법:

void update(@Valid @RequestBody PatchUserRequest request) {
    if (request.name.isSet()) {
        // Do the stuff
    }
}

메모: 코드는 다음 위치에 기재되어 있습니다.groovy은 이미 몇 가지 저는 이미 몇 가지 API에 대해 이 방법을 사용했는데, 꽤 잘 작동하고 있는 것 같습니다.

언급URL : https://stackoverflow.com/questions/36907723/how-to-do-patch-properly-in-strongly-typed-languages-based-on-spring-example