В Java REST API, используя PATCH vs PUT для обновления сущности


Я собираюсь начать разработку нового REST api на Java. Мой вопрос касается использования патча-почему?

Допустим, у нас есть объект с именем Address.java

public class Address {

    @Id
    private Long id

    @NotNull
    private String line1;

    private String line2;       //optional

    @NotNull
    private String city;

    @NotNull
    private String state;   
}

Чтобы создать новый адрес, я бы сделал такой http-запрос:

POST http://localhost:8080/addresses

Со следующей просьбой:

{
    "line1" : "mandatory Address line 1",
    "line2" : "optional  Address line 2",
    "city"  : "mandatory City",
    "state" : "cd"
}

Предположим, что созданная запись имеет идентификатор 1

Соответствующий @ RestController AddressResource.java будет иметь этот метод:

@PostMapping(value = "/addresses")
public ResponseEntity<Address> create(@valid Address newAddress) {
    addressRepo.save(newAddress);
}

@valid гарантирует, что сущность допустимо перед сохранением данных в таблице.

Теперь предположим, что я переезжаю из своей квартиры наверху в дом дальше по улице. Если я использую патч, он становится
PATCH http://localhost:8080/addresses/1

С полезной нагрузкой запроса:

{
    "line1" : "1234 NewAddressDownTheStreet ST",
    "line2" : null
}

Соответствующий метод @ RestController будет иметь вид:

@PatchMapping(value = "/addresses/{id}")
public ResponseEntity<Address> patchAddress(@PathVariable Long id, Address partialAddress) 
{
    Address dbAddress = addressRepo.findOne(id);
    if (partialAddress.getLine1() != null) {
        dbAddress.setLine1(partialAddress.getLine1());
    }
    if (partialAddress.getLine2() != null) {
        dbAddress.setLine2(partialAddress.getLine2());
    }
    if (partialAddress.getCity() != null) {
        dbAddress.setCity(partialAddress.getCity());
    }
    if (partialAddress.getState() != null) {
        dbAddress.setState(partialAddress.getState());
    }

    addressRepo.save(dbAddress)
}

Теперь, если вы запросите таблицу, не будет ли мой адрес ?

"line1" : "1234 NewAddressDownTheStreet ST",
"line2" : "optional  Address line 2",       <-- INCORRECT. Should be null.
"city"  : "mandatory City",
"state" : "cd"

Как можно видеть, вышеуказанные обновления приводят к неправильному значению для строки 2. Это происходит потому, что в java все переменные экземпляра в адресе класс инициализируется нулем (или начальными значениями по умолчанию, если они являются примитивами) при создании экземпляра класса. Таким образом, нет никакого способа отличить строку 2, изменяемую на null от значения по умолчанию.

Вопрос 1) Есть ли стандартный способ обойти это?


Еще один недостаток заключается в том, что я не могу использовать @Valid аннотацию для проверки запроса в точке входа - потому что это только часть. Таким образом, неверные данные могут попасть в систему.

Для например, представьте, что существует дополнительное поле со следующим определением:

@Min(0) 
@Max(100)
private Integer lengthOfResidencyInYears, 

И пользователь случайно набрал 190 (когда они на самом деле означали 19 лет), он не потерпит неудачу.


Вместо PATCH, если бы я использовал PUT, клиент должен был бы отправить полный объект адреса. Это имеет то преимущество, что я могу использовать @Valid, чтобы убедиться, что адрес действительно действителен


Если исходить из предпосылки, что GET всегда должен быть выполнен до выполнения каких-либо обновлений, то почему разве одно использование не положило бы на патча? Я что-то упустил?

В сторону

Мой вывод заключается в том, что разработчики, использующие динамически типизированные языки, являются сторонниками использования патча, поскольку я не вижу никакой пользы в использовании его из статически типизированного языка Java или C#. Это только кажется, что добавляет больше сложности.
1 6

1 ответ:

Использование PATCH для загрузки измененной версии существующего объекта почти всегда проблематично именно по той причине, которую вы описали. Если вы хотите использовать PATCH с JSON, я настоятельно предлагаю вам следовать либо RFC 6902, либо RFC 7396. Я не буду говорить с 7396, потому что я не очень хорошо знаком с ним, но следовать 6902 вы бы определили отдельный ресурс для операций PATCH. В приведенном примере это будет выглядеть так:

PATCH http://localhost:8080/addresses/1
[
    { "op": "replace", "path": "/line1", "value": "1234 NewAddressDownTheStreet ST" },
    { "op": "remove", "path": "/line2" }
]

Затем вы будете обрабатывать это, создание нового объекта сущности, который запустился в текущем состоянии сервера и применил изменения в PATCH. Запустите проверку для нового объекта сущности. Если он пройдет, переместите его на уровень данных. Если это не удается, верните код ошибки.

Если PUT не добавляет слишком много накладных расходов, это хорошая идея. Идемпотенция-это хорошая вещь. Компромисс заключается в том, что вы передаете больше данных по проводу. Если ваш ресурс невелик и к нему редко обращаются, это, возможно, не так уж и важно. Если ваш ресурс большой и доступен часто, что может начать добавлять значительные накладные расходы. Конечно, мы не можем сказать вам переломный момент.

Похоже, что вы также полностью привязали модель ресурсов к модели базы данных. Хороший дизайн таблиц базы данных и хороший дизайн ресурсов часто выглядят очень по-разному для нетривиальных проектов. Я понимаю, что многие фреймворки подталкивают вас в этом направлении, но вы можете захотеть, если вы серьезно не рассматривали возможность их разъединения.