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

#概要
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"}



#サンプルコード

```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値と提供されていない値を区別する方法](https://www.it-swarm-ja.com/ja/java/spring-rest-controller%E3%81%AE%E9%83%A8%E5%88%86%E7%9A%84%E3%81%AA%E6%9B%B4%E6%96%B0%E3%81%A7null%E5%80%A4%E3%81%A8%E6%8F%90%E4%BE%9B%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E5%80%A4%E3%82%92%E5%8C%BA%E5%88%A5%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/827265839/)
[PUT vs. PATCH ~Kotlinでの部分更新はどちらを選ぶべきか？](https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-06-put-vs-patch/)
[RESTでのパッチとnull](https://stackoverflow.com/questions/49647682/patch-in-rest-and-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"
                  }
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
```