복붙노트

[SPRING] Spring MVC PATCH 메소드 : 부분 업데이트

SPRING

Spring MVC PATCH 메소드 : 부분 업데이트

Spring MVC + Jackson을 사용하여 REST 서비스를 빌드하는 프로젝트가있다. 다음 자바 엔티티가 있다고 가정 해 보겠습니다.

public class MyEntity {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;
    //getter & setters
}

때로는 부울 값을 업데이트하기를 원하며 큰 문자열로 전체 객체를 보내는 것이 단순한 부울을 업데이트하는 좋은 방법이라고 생각하지 않습니다. 그래서 업데이트 할 필요가있는 필드 만 보내기 위해 PATCH HTTP 메서드를 사용하는 것을 고려했습니다. 그래서, 내 컨트롤러에 다음과 같은 메서드를 선언합니다 :

@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
    //calling a service to update the entity
}

문제는 다음과 같습니다. 어떤 필드를 업데이트해야하는지 어떻게 알 수 있습니까? 예를 들어, 클라이언트가 단지 부울을 업데이트하기 원한다면 빈 "aVeryBigString"이있는 객체를 얻습니다. 사용자가 부울을 업데이트하려고하지만 문자열을 비우지 않으려한다는 것을 어떻게 알 수 있습니까?

나는 사용자 정의 URL을 구축하여 문제를 "해결"했습니다. 예를 들어 POST / myentities / 1 / aboolean / true URL 만 부울을 업데이트 할 수있는 메서드에 매핑됩니다. 이 솔루션의 문제점은 REST를 준수하지 않는다는 것입니다. 나는 100 % REST와 호환되기를 원하지 않지만 각 필드를 업데이트하기 위해 사용자 정의 URL을 제공하는 것에 익숙하지 않습니다 (특히 여러 필드를 업데이트 할 때 문제가 발생하는 경우).

또 다른 솔루션은 "MyEntity"를 여러 리소스로 분할하여 이러한 리소스를 업데이트하는 것이지만 이해할 수없는 것 같습니다. "MyEntity"는 일반 리소스이며 다른 리소스로 구성되지 않습니다.

그렇다면이 문제를 해결하는 우아한 방법이 있습니까?

해결법

  1. ==============================

    1.boolean을 Boolean으로 변경하고 업데이트하지 않으려는 모든 필드에 null 값을 할당 할 수 있습니다. null이 아닌 유일한 값은 업데이트 할 필드 클라이언트를 정의합니다.

    boolean을 Boolean으로 변경하고 업데이트하지 않으려는 모든 필드에 null 값을 할당 할 수 있습니다. null이 아닌 유일한 값은 업데이트 할 필드 클라이언트를 정의합니다.

  2. ==============================

    2.이것은 매우 늦을 수 있지만, 같은 문제를 겪는 초보자와 사람들을 위해서, 나에게 나만의 해결책을 알려줄 수 있도록하자.

    이것은 매우 늦을 수 있지만, 같은 문제를 겪는 초보자와 사람들을 위해서, 나에게 나만의 해결책을 알려줄 수 있도록하자.

    과거의 프로젝트에서는 단순하게 만들기 위해 기본 Java Map 만 사용합니다. 클라이언트가 null로 명시 적으로 설정 한 null 값을 포함하여 모든 새 값을 캡처합니다. 이 시점에서 도메인 모델과 동일한 POJO를 사용할 때와 달리 Java 속성을 null로 설정해야하는지 판단하기 쉽습니다. 클라이언트가 null로 설정 한 필드를 구분할 수 없으며 이는 업데이트에 포함되지 않지만 기본적으로 null입니다.

    또한 업데이트 할 레코드의 ID를 보내도록 http 요청을 요구해야하며 패치 데이터 구조에 포함시키지 마십시오. 내가 한 일은 경로 변수로 URL에 ID를 설정하고 패치 데이터로 패치 데이터를 설정합니다. 그런 다음 ID로 도메인 모델을 통해 레코드를 얻은 다음 HashMap을 사용하여 매퍼 서비스 또는 유틸리티를 사용하여 해당 도메인 모델에 대한 변경 사항을 패치 할 수 있습니다.

    최신 정보

    이러한 종류의 일반 코드를 사용하여 서비스에 대한 추상 수퍼 클래스를 만들 수 있습니다. Java Generics를 사용해야합니다. 이것은 가능한 구현의 한 부분 일뿐입니다. 아이디어를 얻길 바랍니다. 또한 Orika 또는 Dozer와 같은 매퍼 프레임 워크를 사용하는 것이 좋습니다.

    public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
        @Autowired
        private MapperService mapper;
    
        @Autowired
        private BaseRepo<Entity> repo;
    
        private Class<DTO> dtoClass;
    
        private Class<Entity> entityCLass;
    
        public AbstractService(){
           entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
           dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
        }
    
        public DTO patch(Long id, Map<String, Object> patchValues) {
            Entity entity = repo.get(id);
            DTO dto = mapper.map(entity, dtoClass);
            mapper.map(patchValues, dto);
            Entity updatedEntity = toEntity(dto);
            save(updatedEntity);
            return dto;
        }
    }
    
  3. ==============================

    3.이렇게하는 올바른 방법은 JSON PATCH RFC 6902에서 제안 된 방식입니다.

    이렇게하는 올바른 방법은 JSON PATCH RFC 6902에서 제안 된 방식입니다.

    요청 예는 다음과 같습니다.

    PATCH http://example.com/api/entity/1 HTTP/1.1
    Content-Type: application/json-patch+json 
    
    [
      { "op": "replace", "path": "aBoolean", "value": true }
    ]
    
  4. ==============================

    4.패치의 요점은 당신이 전체 엔티티 표현을 보내지 않는다는 것입니다. 따라서 빈 문자열에 대한 여러분의 의견을 이해할 수 없습니다. 다음과 같은 일종의 간단한 JSON을 처리해야한다.

    패치의 요점은 당신이 전체 엔티티 표현을 보내지 않는다는 것입니다. 따라서 빈 문자열에 대한 여러분의 의견을 이해할 수 없습니다. 다음과 같은 일종의 간단한 JSON을 처리해야한다.

    { aBoolean: true }
    

    이를 지정된 자원에 적용하십시오. 아이디어는 원하는 자원 상태와 현재 자원 상태의 차이입니다.

  5. ==============================

    5.Spring은 이미 가지고있는 것과 같은 문제 때문에 PATCH를 사용하여 객체를 패치 할 수 없다 : JSON 디시리얼라이저는 Null 필드로 Java POJO를 생성한다.

    Spring은 이미 가지고있는 것과 같은 문제 때문에 PATCH를 사용하여 객체를 패치 할 수 없다 : JSON 디시리얼라이저는 Null 필드로 Java POJO를 생성한다.

    이는 엔티티에 패치를 적용하기위한 로직을 제공해야한다는 것을 의미합니다 (즉, PATCH는 사용하지만 POST는 사용하지 않는 경우에만 해당).

    기본이 아닌 유형 또는 일부 규칙 (빈 문자열은 모두에게 적용되지 않는 null)을 사용하거나 재정의 된 값을 정의하는 추가 매개 변수를 제공해야한다는 것을 알고 있습니다. 마지막으로 잘 작동합니다. 자바 스크립트 애플리케이션은 서버에 나열된 JSON 본문과 함께 어떤 필드가 변경되고 전송되었는지를 알고 있습니다. 예를 들어 필드 설명의 이름이 JSON 본문에서 변경 (패치)되었지만 지정되지 않은 경우 해당 필드 설명이 무효화되었습니다.

  6. ==============================

    6.조금 돌아서 파기 한 후에 Spring MVC DomainObjectReader가 현재 사용하고있는 것과 같은 솔루션을 사용하여 수용 가능한 솔루션을 발견했습니다. 참고 자료 : JsonPatchHandler

    조금 돌아서 파기 한 후에 Spring MVC DomainObjectReader가 현재 사용하고있는 것과 같은 솔루션을 사용하여 수용 가능한 솔루션을 발견했습니다. 참고 자료 : JsonPatchHandler

    @RepositoryRestController
    public class BookCustomRepository {
        private final DomainObjectReader domainObjectReader;
        private final ObjectMapper mapper;
    
        private final BookRepository repository;
    
    
        @Autowired
        public BookCustomRepository(BookRepository bookRepository, 
                                    ObjectMapper mapper,
                                    PersistentEntities persistentEntities,
                                    Associations associationLinks) {
            this.repository = bookRepository;
            this.mapper = mapper;
            this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
        }
    
    
        @PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
        public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {
    
            Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
            Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
            repository.save(patched);
    
            return ResponseEntity.noContent().build();
        }
    
    }
    
  7. ==============================

    7.업데이트 된 필드로 구성된 객체를 보낼 수 없습니까?

    업데이트 된 필드로 구성된 객체를 보낼 수 없습니까?

    스크립트 호출 :

    var data = JSON.stringify({
                    aBoolean: true
                });
    $.ajax({
        type: 'patch',
        contentType: 'application/json-patch+json',
        url: '/myentities/' + entity.id,
        data: data
    });
    

    스프링 MVC 컨트롤러 :

    @PatchMapping(value = "/{id}")
    public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
    {
        // updates now only contains keys for fields that was updated
        return ResponseEntity.ok("resource updated");
    }
    

    컨트롤러의 경로 멤버에서 업데이트 맵의 키 / 값 쌍을 반복합니다. 위의 예에서 "aBoolean"키는 값 true를 유지합니다. 다음 단계는 엔티티 설정자를 호출하여 값을 실제로 할당하는 것입니다. 그러나 그것은 다른 종류의 문제입니다.

  8. ==============================

    8.나는이 문제를 해결했다. 왜냐하면 나는 서비스를 바꿀 수 없기 때문이다.

    나는이 문제를 해결했다. 왜냐하면 나는 서비스를 바꿀 수 없기 때문이다.

    public class Test {
    
    void updatePerson(Person person,PersonPatch patch) {
    
        for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
            switch (updatedField){
    
                case firstname:
                    person.setFirstname(patch.getFirstname());
                    continue;
                case lastname:
                    person.setLastname(patch.getLastname());
                    continue;
                case title:
                    person.setTitle(patch.getTitle());
                    continue;
            }
    
        }
    
    }
    
    public static class PersonPatch {
    
        private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();
    
        public List<PersonPatchField> updatedFields() {
            return updatedFields;
        }
    
        public enum PersonPatchField {
            firstname,
            lastname,
            title
        }
    
        private String firstname;
        private String lastname;
        private String title;
    
        public String getFirstname() {
            return firstname;
        }
    
        public void setFirstname(final String firstname) {
            updatedFields.add(PersonPatchField.firstname);
            this.firstname = firstname;
        }
    
        public String getLastname() {
            return lastname;
        }
    
        public void setLastname(final String lastname) {
            updatedFields.add(PersonPatchField.lastname);
            this.lastname = lastname;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(final String title) {
            updatedFields.add(PersonPatchField.title);
            this.title = title;
        }
    }
    

    잭슨은 값이 존재할 때만 불렀다. 그래서 세터가 불려진 것을 구할 수 있습니다.

  9. ==============================

    9.다음은 GSON을 사용하는 패치 명령에 대한 구현입니다.

    다음은 GSON을 사용하는 패치 명령에 대한 구현입니다.

    package de.tef.service.payment;
    
    import com.google.gson.*;
    
    class JsonHelper {
        static <T> T patch(T object, String patch, Class<T> clazz) {
            JsonElement o = new Gson().toJsonTree(object);
            JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
            JsonElement result = patch(o, p);
            return new Gson().fromJson(result, clazz);
        }
    
        static JsonElement patch(JsonElement object, JsonElement patch) {
            if (patch.isJsonArray()) {
                JsonArray result = new JsonArray();
                object.getAsJsonArray().forEach(result::add);
                return result;
            } else if (patch.isJsonObject()) {
                System.out.println(object + " => " + patch);
                JsonObject o = object.getAsJsonObject();
                JsonObject p = patch.getAsJsonObject();
                JsonObject result = new JsonObject();
                o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
                return result;
            } else if (patch.isJsonPrimitive()) {
                return patch;
            } else if (patch.isJsonNull()) {
                return patch;
            } else {
                throw new IllegalStateException();
            }
        }
    }
    

    구현은 중첩 구조에주의해야하기 때문에 재귀 적입니다. 병합을위한 키가 없기 때문에 병합되지 않습니다.

    "patch"JSON은 String에서 JsonElement로 직접 변환되며, 채워지지 않은 필드를 NULL로 채워진 필드와 다른 채로 유지하는 객체로 변환되지 않습니다.

  10. ==============================

    10.당신은 그것을 위해 Optional <>을 사용할 수 있습니다 :

    당신은 그것을 위해 Optional <>을 사용할 수 있습니다 :

    public class MyEntityUpdate {
        private Optional<String> aVeryBigString;
    }
    

    이렇게하면 다음과 같이 업데이트 개체를 검사 할 수 있습니다.

    if(update.getAVeryBigString() != null)
        entity.setAVeryBigString(update.getAVeryBigString().get());
    

    aVeryBigString 필드가 JSON 문서에 없으면 POJO aVeryBigString 필드는 null입니다. 그것이 JSON. 서에 있지만 널 (NULL) 값인 경우, POJO 필드는 랩핑 된 값 null이있는 옵션이됩니다. 이 솔루션을 사용하면 "업데이트 안 함"과 "세트 대 null"을 구별 할 수 있습니다.

  11. from https://stackoverflow.com/questions/17860520/spring-mvc-patch-method-partial-updates by cc-by-sa and MIT license