programing

Spring Rest 컨트롤러의 부분 업데이트에 대한 null 값과 제공되지 않은 값을 구별하는 방법

newnotes 2023. 3. 26. 11:45
반응형

Spring Rest 컨트롤러의 부분 업데이트에 대한 null 값과 제공되지 않은 값을 구별하는 방법

Spring Rest Controller에서 PUT 요청 메서드로 엔티티를 부분적으로 업데이트할 때 null 값과 제공되지 않은 값을 구분하려고 합니다.

다음 엔티티를 예로 들어 보겠습니다.

@Entity
private class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /* let's assume the following attributes may be null */
    private String firstName;
    private String lastName;

    /* getters and setters ... */
}

내 개인 저장소(Spring Data):

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}

사용하는 DTO:

private class PersonDTO {
    private String firstName;
    private String lastName;

    /* getters and setters ... */
}

My Spring Rest Controller:

@RestController
@RequestMapping("/api/people")
public class PersonController {

    @Autowired
    private PersonRepository people;

    @Transactional
    @RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
    public ResponseEntity<?> update(
            @PathVariable String personId,
            @RequestBody PersonDTO dto) {

        // get the entity by ID
        Person p = people.findOne(personId); // we assume it exists

        // update ONLY entity attributes that have been defined
        if(/* dto.getFirstName is defined */)
            p.setFirstName = dto.getFirstName;

        if(/* dto.getLastName is defined */)
            p.setLastName = dto.getLastName;

        return ResponseEntity.ok(p);
    }
}

누락된 속성이 있는 요청

{"firstName": "John"}

되는 동작: update 예상예 expected : expected데 expected expectedfirstName= "John"lastName★★★★★★★★★★★★★★★★★★」

null 속성이 있는 요청

{"firstName": "John", "lastName": null}

되는 동작: update 예상예 expected : expected데 expected expectedfirstName="John"를 설정합니다.lastName=null.

는 이 두 할 수 . 이 두 경우를 할 수 없기 때문이다. 왜냐하면lastName에서는, 항상 DTO 로 설정됩니다.null잭슨 형

주의: REST 베스트 프랙티스(RFC 6902)에서는 부분 업데이트에는 PUT 대신 PATC를 사용할 것을 권장하고 있습니다만, 특정 시나리오에서는 PUT를 사용해야 합니다.

다른 옵션은 java.util을 사용하는 것입니다.선택적.

import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Optional;

@JsonInclude(JsonInclude.Include.NON_NULL)
private class PersonDTO {
    private Optional<String> firstName;
    private Optional<String> lastName;
    /* getters and setters ... */
}

firstName이 설정되지 않은 경우 값은 null이 되며 @JsonInclude 주석에서는 무시됩니다.그렇지 않으면 요청 개체에 암묵적으로 설정되어 있는 경우 firstName은 늘이 아니라 firstName.get()이 됩니다.솔루션 @laffuste에 링크되어 있는 것을 다른 코멘트에서 발견했습니다(Garretwilson의 초기 코멘트는 효과가 없었습니다).

또한 Jackson의 ObjectMapper를 사용하여 엔티티에 DTO를 매핑할 수 있으며 요청 개체에서 전달되지 않은 속성은 무시됩니다.

import com.fasterxml.jackson.databind.ObjectMapper;

class PersonController {
    // ...
    @Autowired
    ObjectMapper objectMapper

    @Transactional
    @RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
    public ResponseEntity<?> update(
            @PathVariable String personId,
            @RequestBody PersonDTO dto
    ) {
        Person p = people.findOne(personId);
        objectMapper.updateValue(p, dto);
        personRepository.save(p);
        // return ...
    }
}

java.util을 사용한DTO 검증옵션도 조금 다릅니다.여기에 기재되어 있습니다만, 찾는 데 시간이 걸렸습니다.

// ...
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
// ...
private class PersonDTO {
    private Optional<@NotNull String> firstName;
    private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
    /* getters and setters ... */
}

이 경우 firstName은 전혀 설정되지 않을 수 있지만 PersonDTO가 검증되면 null로 설정되지 않을 수 있습니다.

//...
import javax.validation.Valid;
//...
public ResponseEntity<?> update(
        @PathVariable String personId,
        @RequestBody @Valid PersonDTO dto
) {
    // ...
}

또, Optional의 사용에 대해서도 크게 논의되고 있는 것 같습니다.또한, Lombok의 유지보수는 Optional을 지원하지 않습니다(를 들면, 이 질문을 참조해 주세요.이것은 롬복 사용을 의미합니다.데이터/롬복Optional 필드의 setter with restraints가 있는 클래스의 setter는 동작하지 않습니다(이것은 제약조건을 그대로 가진 setter를 작성하려고 시도합니다).따라서 @Setter/@Data를 사용하면 setter와 member 변수 모두 제약조건이 설정되어 있기 때문에 예외가 느려집니다.Optional 파라미터 없이Setter를 쓰는 것도 좋을 것 같습니다.예를 들어 다음과 같습니다.

//...
import lombok.Getter;
//...
@Getter
private class PersonDTO {
    private Optional<@NotNull String> firstName;
    private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;

    public void setFirstName(String firstName) {
        this.firstName = Optional.ofNullable(firstName);
    }
    // etc...
}

DTO를 변경하거나 설정자를 커스터마이즈하지 않는 더 나은 옵션이 있습니다.

여기에는 다음과 같이 Jackson이 데이터를 기존 데이터 개체와 병합할 수 있도록 하는 작업이 포함됩니다.

MyData existingData = ...
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);

MyData mergedData = readerForUpdating.readValue(newData);    

newData will 、 will will 、 in will in in in in の 데이터는 .existingData 그 그 에 ,, 드, 드, 드, 필, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is, is,null.

데모 코드:

    ObjectMapper objectMapper = new ObjectMapper();
    MyDTO dto = new MyDTO();

    dto.setText("text");
    dto.setAddress("address");
    dto.setCity("city");

    String json = "{\"text\": \"patched text\", \"city\": null}";

    ObjectReader readerForUpdating = objectMapper.readerForUpdating(dto);

    MyDTO merged = readerForUpdating.readValue(json);

:{"text": "patched text", "address": "address", "city": null}

:text ★★★★★★★★★★★★★★★★★」city되어 있다city is금 is is이다null및 that )는 다음과 같습니다.address자자남남남남다다

Spring Rest 컨트롤러에서는 이를 위해 Spring deserialize가 아닌 원래 JSON 데이터를 가져와야 합니다.따라서 다음과 같이 엔드포인트를 변경합니다.

@Autowired ObjectMapper objectMapper;

@RequestMapping(path = "/{personId}", method = RequestMethod.PATCH)
public ResponseEntity<?> update(
        @PathVariable String personId,
        @RequestBody JsonNode jsonNode) {

   RequestDTO existingData = getExistingDataFromSomewhere();

   ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
   
   RequestDTO mergedData = readerForUpdating.readValue(jsonNode);

   ...
}

잭슨의 작성자가 권장하는 대로 부울 플래그를 사용합니다.

class PersonDTO {
    private String firstName;
    private boolean isFirstNameDirty;

    public void setFirstName(String firstName){
        this.firstName = firstName;
        this.isFirstNameDirty = true;
    }

    public String getFirstName() {
        return firstName;
    }

    public boolean hasFirstName() {
        return isFirstNameDirty;
    }
}

사실, 검증을 무시하면 이렇게 문제를 해결할 수 있습니다.

   public class BusDto {
       private Map<String, Object> changedAttrs = new HashMap<>();

       /* getter and setter */
   }
  • 먼저, 당신의 dto를 위한 슈퍼클래스를 쓰세요. BusDto처럼요.
  • 다음으로 dto를 변경하여 슈퍼클래스를 확장하고 dto의 set 메서드를 변경하여 속성 이름과 값을 changedAttrs로 설정합니다(null 또는 null이 아닌 Atribut의 값이 있는 경우 스프링이 세트를 호출하기 때문입니다).
  • 셋째, 지도를 가로 질러라.

저는 같은 문제를 해결하려고 노력했습니다. 사용하기 쉬웠다JsonNodeDTO를 사용하다이렇게 하면 제출된 것만 얻을 수 있습니다.

쓰다, 이렇게 써야 요.MergeService실제 일을 하는 건 당신 자신이에요. 빈 래퍼만 사용할 Jackson(Json)을 사용할 수 .readForUpdate이치노

실제로는 다른 노드 타입을 사용하고 있습니다.이는 '표준 폼 제출' 및 기타 서비스 콜과 동일한 기능이 필요하기 때문입니다.은 '내부거래'라는 것 거래 .EntityService

★★★★★★★★★★★★★★★★★.MergeService는 속성,및을 직접 : ) 、 [ ] 、 [ ] 、 [ ] 、 [ ] 、 [ ] 、 [ ] 、 [ ] 、 [ ] 。

나에게 가장 문제가 된 부분은 목록/세트 요소 내의 변화와 목록/세트 변경 또는 치환을 구별하는 것이었습니다.

또한 일부 속성을 다른 모델(내 경우 JPA 엔티티)에 대해 검증해야 하므로 검증이 쉽지 않습니다.

편집 - 일부 매핑 코드(의사 코드):

class SomeController { 
   @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public void save(
            @PathVariable("id") final Integer id,
            @RequestBody final JsonNode modifications) {
        modifierService.applyModifications(someEntityLoadedById, modifications);
    }
}

class ModifierService {

    public void applyModifications(Object updateObj, JsonNode node)
            throws Exception {

        BeanWrapperImpl bw = new BeanWrapperImpl(updateObj);
        Iterator<String> fieldNames = node.fieldNames();

        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            Object valueToBeUpdated = node.get(fieldName);
            Class<?> propertyType = bw.getPropertyType(fieldName);
            if (propertyType == null) {
               if (!ignoreUnkown) {
                    throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass());
                }
            } else if (Map.class.isAssignableFrom(propertyType)) {
                    handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else if (Collection.class.isAssignableFrom(propertyType)) {
                    handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else {
                    handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects);
            }
        }
    }
}

답변하기엔 너무 늦었을 수도 있지만, 다음과 같이 할 수 있습니다.

  • 기본적으로는 'null' 값을 설정하지 마십시오.설정 해제할 필드를 쿼리 매개 변수를 통해 명시적 목록을 제공합니다.이러한 방법으로 엔티티에 대응하는 JSON을 송신할 수 있어 필요에 따라서 필드를 설정할 수 있습니다.

  • 사용 사례에 따라 일부 엔드포인트는 명시적으로 모든 null 값을 설정되지 않은 작업으로 처리할 수 있습니다.패치 적용에는 다소 위험하지만 상황에 따라서는 옵션이 될 수 있습니다.

또 다른 해결책은 요청 본문을 필수적으로 역직렬화하는 것입니다.이를 통해 사용자가 제공한 필드를 수집하고 선택적으로 검증할 수 있습니다.

따라서 DTO는 다음과 같습니다.

public class CatDto {
    @NotBlank
    private String name;

    @Min(0)
    @Max(100)
    private int laziness;

    @Max(3)
    private int purringVolume;
}

컨트롤러는 다음과 같습니다.

@RestController
@RequestMapping("/api/cats")
@io.swagger.v3.oas.annotations.parameters.RequestBody(
        content = @Content(schema = @Schema(implementation = CatDto.class)))
// ^^ this passes your CatDto model to swagger (you must use springdoc to get it to work!)
public class CatController {
    @Autowired
    SmartValidator validator; // we'll use this to validate our request

    @PatchMapping(path = "/{id}", consumes = "application/json")
    public ResponseEntity<String> updateCat(
            @PathVariable String id,
            @RequestBody Map<String, Object> body
            // ^^ no Valid annotation, no declarative DTO binding here!
    ) throws MethodArgumentNotValidException {
        CatDto catDto = new CatDto();
        WebDataBinder binder = new WebDataBinder(catDto);
        BindingResult bindingResult = binder.getBindingResult();
        List<String> patchFields = new ArrayList<>();

        binder.bind(new MutablePropertyValues(body));
        // ^^ imperatively bind to DTO
        body.forEach((k, v) -> {
            patchFields.add(k);
            // ^^ collect user provided fields if you need
            validator.validateValue(CatDto.class, k, v, bindingResult);
            // ^^ imperatively validate user input
        });
        if (bindingResult.hasErrors()) {
            throw new MethodArgumentNotValidException(null, bindingResult);
            // ^^ this can be handled by your regular exception handler
        }
        // Here you can do normal stuff with your catDto.
        // Map it to cat model, send to cat service, whatever.
        return ResponseEntity.ok("cat updated");
    }
}

옵션도 필요 없고, 의존도도 없습니다.정상적인 검증은 유효하고, 스웨거도 좋아 보입니다.유일한 문제는 중첩된 개체에 대한 적절한 병합 패치를 얻을 수 없다는 것입니다. 그러나 대부분의 경우 이 패치도 필요하지 않습니다.

아마도 늦었지만 다음 코드는 null 값과 제공되지 않은 값을 구분하는 데 도움이 됩니다.

if(dto.getIban() == null){
  log.info("Iban value is not provided");
}else if(dto.getIban().orElse(null) == null){
  log.info("Iban is provided and has null value");
}else{
  log.info("Iban value is : " + dto.getIban().get());
}

언급URL : https://stackoverflow.com/questions/38424383/how-to-distinguish-between-null-and-not-provided-values-for-partial-updates-in-s

반응형