#概要
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で更新したいケース
-
データ投入
PUT http://localhost:8080/employees/234
request body {"employeeId": "234","name": "Kosuke","address": "Yokohama"} -
データ確認
GET http://localhost:8080/employees/234
⇒response body {"employeeId": "234","name": "Kosuke","address": "Yokohama"} -
部分更新(null値を設定)
PATCH http://localhost:8080/employees/234
request body {"employeeId": "234","name": null,"address": "Kawasaki"} -
nullで更新できた
GET http://localhost:8080/employees/234- ⇒response body {"employeeId": "234","name": null,"address": "Kawasaki"}
##更新したくないケース
-
データ投入
PUT http://localhost:8080/employees/234
request body {"employeeId": "234","name": "Kosuke","address": "Yokohama"} -
データ確認
GET http://localhost:8080/employees/234
⇒response body {"employeeId": "234","name": "Kosuke","address": "Yokohama"} -
部分更新(fieldなし)
PATCH http://localhost:8080/employees/234
request body {"employeeId": "234","address": "Kawasaki"} -
更新されないケース
GET http://localhost:8080/employees/234- ⇒response body {"employeeId": "234","name": "Kosuke","address": "Kawasaki"}
#サンプルコード
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);
}
}
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);
}
}
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() {
}
}
package com.example.demo.domain;
import java.util.Optional;
public interface EmployeeRepository {
Optional<Employee> findOneById(String employeeId);
Optional<Employee> put(String employeeId, Employee employee);
}
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"
}
]
}
}
]
}
]
}
]
}