0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WebAPIのPATCHで部分更新するときにnullの扱いに困ったので調べた。

Last updated at Posted at 2021-06-25

#概要
WebAPIのPATCHで部分更新するときにnullの扱いに困ったので調べた。

#実現したいこと

  • patchで下記jsonを投入したら、field2がnullで更新される
"field1":"value1"
"field2":null
"field3":"value3"
  • patchで下記jsonを投入したら、field2は更新されない
"field1":"value1"
"field3":"value3"

#結論
JacksonのJsonNodeを使うといい感じだった。
該当箇所は下記サンプルコードのGetEmployeeUsecase.javaの★部分を参照ください。

#下記サンプルコードの実行結果説明
##nullで更新したいケース

##更新したくないケース

#サンプルコード

EmployeeController.java
package com.example.demo.presentation;

import java.io.IOException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.domain.Employee;
import com.example.demo.usecase.GetEmployeeUsecase;
import com.fasterxml.jackson.databind.JsonNode;

@RestController
public class EmployeeController {
    @Autowired
    GetEmployeeUsecase getUsecase;
    @GetMapping("/employees/{employeeId}")
    Optional<Employee> getOne(@PathVariable final String employeeId) {
        return this.getUsecase.getEmployee(employeeId);
    }
    @PutMapping("/employees/{employeeId}")
    Optional<Employee> put(@PathVariable final String employeeId, @RequestBody final Employee employee) {
        return this.getUsecase.putEmployee(employeeId, employee);
    }
    @PatchMapping("/employees/{employeeId}")
    Optional<Employee> patch(@PathVariable final String employeeId, @RequestBody final JsonNode jsonNode)
            throws IOException {
        return this.getUsecase.patchEmployee(employeeId, jsonNode);
    }
}
GetEmployeeUsecase.java
package com.example.demo.usecase;

import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.domain.Employee;
import com.example.demo.domain.EmployeeRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

@Service
public class GetEmployeeUsecase {
    @Autowired
    EmployeeRepository repository;
    @Autowired
    ObjectMapper objectMapper;
    public Optional<Employee> getEmployee(final String employeeId) {
        return this.repository.findOneById(employeeId);
    }
    public Optional<Employee> putEmployee(final String employeeId, final Employee employee) {
        return this.repository.put(employeeId, employee);
    }
    public Optional<Employee> patchEmployee(final String employeeId, final JsonNode jsonNode) throws IOException {
        final Employee existingEmployee = this.repository.findOneById(employeeId)
                .orElseThrow(() -> new NoSuchElementException("そんなデータありません。" + employeeId));
        final ObjectReader readerForUpdating = this.objectMapper.readerForUpdating(existingEmployee); //★
        final Employee updatedEmployee = readerForUpdating.readValue(jsonNode);  //★
        return this.repository.put(employeeId, updatedEmployee);
    }
}
Employee.java
package com.example.demo.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@Getter
@AllArgsConstructor
@ToString
public class Employee {
    private String employeeId;
    private String name;
    private String address;
    @SuppressWarnings("unused")
    private Employee() {
    }
}
EmployeeRepository.java
package com.example.demo.domain;

import java.util.Optional;

public interface EmployeeRepository {
    Optional<Employee> findOneById(String employeeId);
    Optional<Employee> put(String employeeId, Employee employee);
}
TempEmployeeRepositoryImp.java
package com.example.demo.infrastructure;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.example.demo.domain.Employee;
import com.example.demo.domain.EmployeeRepository;

@Repository
public class TempEmployeeRepositoryImp implements EmployeeRepository {
    private final Map<String, Employee> db;
    public TempEmployeeRepositoryImp() {
        this.db = new HashMap<>();
        //初期データとして
        this.db.put("123", new Employee("123", "Taro", "Tokyo"));
    }
    @Override
    public Optional<Employee> findOneById(final String employeeId) {
        final Optional<Entry<String, Employee>> entry =
                this.db.entrySet().stream().filter(e -> e.getKey().equals(employeeId)).findFirst();
        return entry.isPresent() ? Optional.of(entry.get().getValue()) : Optional.empty();
    }
    @Override
    public Optional<Employee> put(final String employeeId, final Employee employee) {
        this.db.put(employeeId, employee);
        return findOneById(employeeId);
    }
}

##参考にしたサイト
Spring Rest Controllerの部分的な更新でnull値と提供されていない値を区別する方法
PUT vs. PATCH ~Kotlinでの部分更新はどちらを選ぶべきか?
RESTでのパッチとnull

##サンプルコードを実行しているローカルサーバのAPIにコマンドを投入するためのTalend API Testerのデータ
Talende API testerでインポートして使ってください。

{
  "version": 6,
  "entities": [
    {
      "entity": {
        "type": "Project",
        "id": "896b5b42-e113-44ed-a87c-e829c1a9f1f2",
        "name": "sandbox"
      },
      "children": [
        {
          "entity": {
            "type": "Service",
            "id": "16104e11-a3fd-4b99-b641-a9aaa4c9313d",
            "name": "employee"
          },
          "children": [
            {
              "entity": {
                "type": "Request",
                "method": {
                  "link": "http://tools.ietf.org/html/rfc7231#section-4.3.1",
                  "name": "GET"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/123"
                },
                "id": "58dd290d-28be-411d-a512-ad37c429ce37",
                "name": "get123",
                "headers": []
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "link": "http://tools.ietf.org/html/rfc7231#section-4.3.1",
                  "name": "GET"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "7c2bc90e-ebf6-447c-8856-3e5a001c66dc",
                "name": "get234",
                "headers": []
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "requestBody": true,
                  "link": "http://tools.ietf.org/html/rfc5789",
                  "name": "PATCH"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text",
                  "textBody": "{\n  \"employeeId\": \"234\",\n  \"name\": null,\n  \"address\":\"Kawasaki\"\n}"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "1be96753-140c-4b9f-9a2e-637f07ee8202",
                "name": "patch234_name_is_null",
                "headers": [
                  {
                    "enabled": true,
                    "name": "Content-Type",
                    "value": "application/json"
                  }
                ]
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "requestBody": true,
                  "link": "http://tools.ietf.org/html/rfc5789",
                  "name": "PATCH"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text",
                  "textBody": "{\n  \"employeeId\": \"234\",\n  \"address\":\"Kawasaki\"\n}"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "0cacdf69-d3e3-4e8d-a647-b60bfab62431",
                "name": "patch234_no_name_records",
                "headers": [
                  {
                    "enabled": true,
                    "name": "Content-Type",
                    "value": "application/json"
                  }
                ]
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "requestBody": true,
                  "link": "http://tools.ietf.org/html/rfc7231#section-4.3.4",
                  "name": "PUT"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text",
                  "textBody": "{\n  \"employeeId\": \"234\",\n  \"name\": \"Kosuke\",\n  \"address\": \"Yokohama\"\n}"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "dc172707-8972-4ee2-942c-ee516a6f7e3e",
                "name": "put234",
                "headers": [
                  {
                    "enabled": true,
                    "name": "Content-Type",
                    "value": "application/json"
                  }
                ]
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "requestBody": true,
                  "link": "http://tools.ietf.org/html/rfc7231#section-4.3.4",
                  "name": "PUT"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text",
                  "textBody": "{\n  \"employeeId\": \"234\",\n  \"name\": null\n}"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "9b8e5195-b0a1-4beb-b85d-1f016838ede7",
                "name": "put234_name_is_null",
                "headers": [
                  {
                    "enabled": true,
                    "name": "Content-Type",
                    "value": "application/json"
                  }
                ]
              }
            },
            {
              "entity": {
                "type": "Request",
                "method": {
                  "requestBody": true,
                  "link": "http://tools.ietf.org/html/rfc7231#section-4.3.4",
                  "name": "PUT"
                },
                "body": {
                  "formBody": {
                    "overrideContentType": true,
                    "encoding": "application/x-www-form-urlencoded",
                    "items": []
                  },
                  "bodyType": "Text",
                  "textBody": "{\n  \"employeeId\": \"234\"\n}"
                },
                "uri": {
                  "query": {
                    "delimiter": "&",
                    "items": []
                  },
                  "scheme": {
                    "name": "http",
                    "version": "V11"
                  },
                  "host": "localhost:8080",
                  "path": "/employees/234"
                },
                "id": "5062de22-ec26-46f1-bc8b-9f2319e07c03",
                "name": "put234_no_name_records",
                "headers": [
                  {
                    "enabled": true,
                    "name": "Content-Type",
                    "value": "application/json"
                  }
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?