воскресенье, 9 марта 2014 г.

Let's have a REST, часть II

Теоретически, RESTful приложение может использовать любой stateless протокол для обмена сообщениями между клиентом и сервером. Практически, RESTful приложения базируются на протоколе HTTP, среди методов которого есть методы GET, POST, PUT и DELETE.

POST vs PUT

Изучая вопрос по статьям и форумам в Интернете, я встретил утверждения о том, что HTTP-метод PUT должен использоваться как для изменения, так и для создания ресурсов. В то же время, для создания ресурсов используется метод POST (см. Let's have a REST, часть I). В чем здесь дело? Когда и для чего нужно использовать PUT, а когда - POST?

Найти ответ помогла спецификация протокола HTTP (RFC 2616). Согласно RFC 2616, метод POST используется с URL, адресующим ресурс, являющийся в некотором смысле родителем или контейнером для того ресурса, который будет создан в результате обработки запроса POST:

The posted entity is subordinate to that URL in the same way that a file is subordinate to a directory containing it, a news article is subordinate to a newsgroup to which it is posted, or a record is subordinate to a database.

Ожидается, что сервер в ответ на запрос POST создаст объект из полученных данных и вернет URL созданного объекта.

Примеры URL для запросов POST:

http://<server>/books     POST    создать книгу
http://<server>/users     POST    создать пользователя

Тогда как ресурсы, представляющие конкретные книги и пользователей, адресуются при помощи уникальных идентификаторов:

http://<server>/books/33  GET     получить представление книги
http://<server>/users/7   GET     получить представление пользователя

Итак, HTTP-запрос POST на создание новой книги имеет URL http://<server>/books и содержит данные (представление, в терминах REST) для создания новой книги. Сервер, успешно обработав запрос, возвращает URL созданного ресурса http://<server>/books/34, где 34 - идентификатор нового ресурса.

Для создания другой книги нужно снова выполнить запрос POST c URL http://<server>/books и другим представлением. И сервер вернет URL нового созданного ресурса, возможно, http://<server>/books/35.

В отличие от HTTP-запроса с методом POST, запрос с методом PUT выполняется с URL конкретного ресурса, например, http://<server>/books/33 или http://<server>/users/7:

The PUT method requests that the enclosed entity be stored under the supplied Request-URL.

Изменение существующего на сервере ресурса - типичное использование запроса PUT. Однако, в случае отсутствия на сервере адресуемого ресурса, ресурс с таким адресом должен быть создан из данных запроса PUT!

Запрос PUT c URL http://<server>/books/33 изменит существующий ресурс - книгу с номером 33. А запрос PUT c URL http://<server>/books/45, адресующий несуществующий ресурс, приведет к созданию нового ресурса с идентификатором 45.

The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URL. The URL in a POST request identifies the resource that will handle the enclosed entity. ... In contrast, the URL in a PUT request identifies the entity enclosed with the request -- the user agent knows what URL is intended and the server MUST NOT attempt to apply the request to some other resource.

Решение о том, использовать ли метод PUT для создания ресурсов, остается за разработчиком приложения.

Идемпотентность

Заслуживает внимания, что спецификация протокола HTTP относит метод PUT, вместе с GET и DELETE, к идемпотентным методам. Свойство идемпотентности состоит в том, что побочный эффект выполнения N>0 идентичных запросов точно такой же, как от выполнения одного запроса.

Для GET это означает, что первое и последующие чтения (получение представления) одного и того же ресурса оказывают одно и то же воздействие на читаемый ресурс - никакого. Для DELETE это означает, что первый и последующие идентичные запросы подтверждают факт отсутствия ресурса на сервере. Нет разницы, был ли ресурс удален по первому запросу или не существовал на момент первого запроса.

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

Вам приходилось обновлять страницу в браузере после того, как вы отослали на сервер заполненную HTML-форму? Браузер предупреждает вас о том, что обновление страницы может вызвать побочные эффекты, и просит подтвердить ваше намерение. Это происходит потому, что метод POST, используемый для отправки HTML-формы, не является идемпотентным! А это значит, повторная отправка идентичного запроса может привести к нежелательным для пользователя результатам.

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

Например, первая отправка HTML-формы запросом POST с URL http://magazin/orders создает на сервере заказ, и сервер возвращает URL этого заказа http://magazin/orders/12345. Повторная отправка той же формы туда же создает на сервере другой заказ с точно таким же содержанием, как и первый, и сервер возвращает его URL http://magazin/orders/12346.

Чтобы избежать повторной отправки формы при обновлении страницы, имеет смысл в конце обработчика формы перенаправить (redirect) браузер на безопасный URL.

Хорошая аналогия = ограниченная аналогия

Имеем интерфейс RESTful приложения для работы с книгами:

http://<server>/books       GET     получение списка книг
http://<server>/books       POST    создание новой книги
http://<server>/books/<id>  GET     получение детальной информации о книге 
http://<server>/books/<id>  PUT     обновление или создание книги 
http://<server>/books/<id>  DELETE  удаление книги 

Может возникнуть соблазн провести аналогию с манипулированием данными в БД при помощи запросов SQL. Команды INSERT (с подзапросом), SELECT, UPDATE и DELETE могут воздействовать как на отдельную строку, так и на все строки таблицы. Так почему бы, проектируя интерфейс RESTful приложения, не предусмотреть также следующие запросы:

http://<server>/books       PUT     изменение всех книг
http://<server>/books       DELETE  удаление всех книг

Не стоит этого делать. Такие запросы a) расходятся с рекомендациями RFC 2616 и b) очень опасны. Обновление всех книг на сервере так, чтобы они стали одинаковы, и удаление всех книг - это не то, что вы захотите делать каждый день. Это операции, которые чреваты потерей данных и большими проблемами для системы, находящейся в эксплуатации.

Браузерное приложение vs web-сервис

Два типа RESTful приложений - это браузерные web-приложения и web-сервисы.

В браузерном web-приложении полученные с сервера HTML-представления ресурсов отображаются в браузере, и с ними напрямую работает пользователь. Переходы по гиперссылкам выполняются при помощи HTTP-метода GET, отправка HTML-форм - при помощи HTTP-метода POST, при этом в теле запроса передаются данные формы.

Язык HTML (в том числе, HTML5) не предусматривает использование HTTP-методов PUT и DELETE ни с гиперссылками, ни с формами. Поэтому запросы PUT и DELETE приходится имитировать в HTML-приложениях, претендующих на звание RESTful. Фреймворк Ruby on Rails делает это, передавая в запросе POST скрытый параметр _method, принимающий значения 'PUT' или 'DELETE'. Сервер выбирает обработчик запроса, анализируя значение этого параметра. (То есть, для диспетчеризации запроса серверу приходится заглядывать в тело запроса, что нарушает одно из требований модели REST - требование самоописательности запроса.)

Другой интересный практический момент - это получение разных представлений одной и той же сущности. Посмотрим на фрагмент RESTful инерфейса для работы с книгами:

http://<server>/books       GET    получение списка книг
http://<server>/books/<id>  GET    получение детальной информации о книге 

Запрос GET c URL http:////books/<id> позволяет получить представление книги, которое будет отображено как HTML-страница в браузере. Но что будет отображено в браузере: информация о книге, которую пользователь сможет только прочесть, но не изменить, или форма для редактирования информации о книге?

В разных контекстах может оказаться уместным одно или другое представление, то есть, они оба нужны и важны. Следовательно, для получения каждого из них необходим собственный URL, каждое из них - самостоятельный ресурс в терминах REST. Вот как принято идентифицировать ресурсы в Ruby on Rails приложениях:

/books              GET    представление списка книг, только-чтение
/books/new          GET    представление для создания книги, пустая HTML-форма
/books/<id>         GET    представление книги, только-чтение
/books/<id>/edit    GET    представление для изменения книги, HTML-форма, заполненная данными книги

Просто и красиво, не так ли?

Web-сервисы, в отличие от браузерных web-приложений, для реалиазции RESTful поведения не нуждаются в костылях, вроде скрытого поля _method в HTML-формах. Все методы HTTP находятся в распоряжении клиента web-сервиса.

Однако, web-сервисы находятся в натянутых отношениях с другим требованием REST - требованием о том, что ответ сервера должен содержать ссылки на другие ресурсы приложения, тем самым предлагая клиенту доступ к другим представлениям. Клиент web-сервиса - не человек, а программа, поэтому действия такого клиента строго детерминированы и вряд ли предполагают исследование других представлений через гиперссылки, полученные в ответе от сервера. Поэтому гиперссылки в ответах RESTful web-сервиса могут и вовсе не встречаться, а клиентская программа может конструировать нужные ей URL самостоятельно.

В заключительной части я рассмотрю маленькое RESTful приложение.

Комментариев нет:

Отправить комментарий