Шаблоны для обработки пакетных операций в веб-сервисах REST?


170

Какие проверенные шаблоны проектирования существуют для пакетных операций с ресурсами в веб-службе в стиле REST?

Я пытаюсь найти баланс между идеалами и реальностью с точки зрения производительности и стабильности. Сейчас у нас есть API, где все операции извлекаются либо из ресурса списка (то есть: GET / user), либо из одного экземпляра (PUT / user / 1, DELETE / user / 22 и т. Д.).

В некоторых случаях вы хотите обновить одно поле целого набора объектов. Кажется очень расточительным отправлять полное представление для каждого объекта туда и обратно, чтобы обновить одно поле.

В API стиля RPC у вас может быть метод:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc. 

Какой здесь REST-эквивалент? Или это нормально идти на компромисс сейчас и потом. Разве это разрушает дизайн, добавляя несколько определенных операций, где он действительно улучшает производительность и т. Д.? Клиент во всех случаях прямо сейчас является веб-браузером (приложение javascript на стороне клиента).

Ответы:


77

Простой шаблон RESTful для пакетов - это использование ресурса коллекции. Например, удалить сразу несколько сообщений.

DELETE /mail?&id=0&id=1&id=2

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

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

По сути, вы обновляете список писем, помеченных как прочитанные.

Вы также можете использовать это для присвоения нескольких предметов одной категории.

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

Очевидно, что гораздо сложнее делать частичные обновления в стиле iTunes (например, artist + albumTitle, но не trackTitle). Аналогия с ведром начинает рушиться.

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

В долгосрочной перспективе гораздо проще обновить отдельный частичный ресурс или атрибуты ресурса. Просто используйте подресурс.

POST /mail/0/markAsRead
POSTDATA: true

Кроме того, вы можете использовать параметризованные ресурсы. Это менее распространено в шаблонах REST, но разрешено в спецификациях URI и HTTP. Точка с запятой разделяет горизонтально связанные параметры внутри ресурса.

Обновите несколько атрибутов, несколько ресурсов:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Обновите несколько ресурсов, только один атрибут:

POST /mail/0;1;2/markAsRead
POSTDATA: true

Обновите несколько атрибутов, только один ресурс:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

RESTful творчество в изобилии.


1
Кто-то может утверждать, что ваше удаление должно быть постом, так как он не уничтожает этот ресурс.
Крис Никола

6
Это не обязательно. POST - это метод фабричного шаблона, он менее явный и очевидный, чем PUT / DELETE / GET. Единственное ожидание состоит в том, что сервер сам решит, что делать в результате POST. POST - это именно то, что было всегда, я отправляю данные формы, и сервер делает что-то (как мы надеемся) и дает мне некоторое представление о результате. Мы не обязаны создавать ресурсы с помощью POST, мы просто часто выбираем это. Я легко могу создать ресурс с помощью PUT, я просто должен определить URL ресурса как отправителя (не всегда идеальный).
Крис Никола

1
@nishant, в этом случае вам, вероятно, не нужно ссылаться на несколько ресурсов в URI, а просто передать кортежи со ссылками / значениями в теле запроса. например, POST / mail / markAsRead, BODY: i_0_id = 0 & i_0_value = true & i_1_id = 1 & i_1_value = false & i_2_id = 2 & i_2_value = true
Алекс

3
точка с запятой зарезервирована для этой цели.
Алекс

1
Удивило, что никто не указал, что обновление нескольких атрибутов на одном ресурсе приятно покрыто PATCH- нет необходимости в творчестве в этом случае.
LB2

25

Совсем нет - я думаю, что эквивалент REST (или, по крайней мере, одно решение) почти точно таков - специализированный интерфейс, разработанный для выполнения операций, требуемых клиентом.

Мне вспоминается шаблон, упомянутый в книге Крейна и Паскарелло « Аякс в действии» (кстати, отличная книга - очень рекомендуется), в которой они иллюстрируют реализацию объекта типа CommandQueue , работа которого заключается в том, чтобы ставить запросы в пакеты и затем периодически отправляйте их на сервер.

Объект, если я правильно помню, по существу просто содержал массив «команд» - например, для расширения вашего примера, каждая из которых - запись, содержащая команду «markAsRead», «messageId» и, возможно, ссылку на обратный вызов / обработчик function - и затем по некоторому расписанию или по какому-либо действию пользователя объект команды будет сериализован и отправлен на сервер, а клиент будет обрабатывать последующую постобработку.

У меня нет подробностей, но похоже, что подобная очередь команд будет одним из способов решения вашей проблемы; это значительно уменьшило бы общение и абстрагировало бы интерфейс на стороне сервера таким образом, который вы могли бы найти более гибким в будущем.


Обновление : ага! Я нашел отрывок из этой самой книги в Интернете с примерами кода (хотя я все же предлагаю подобрать настоящую книгу!). Посмотрите здесь , начиная с раздела 5.5.3:

Это легко закодировать, но может привести к большому количеству очень маленьких битов трафика на сервер, что неэффективно и может привести к путанице. Если мы хотим контролировать наш трафик, мы можем записывать эти обновления и ставить их в очередь локально, а затем отправлять их на сервер партиями на досуге. Простая очередь обновлений, реализованная в JavaScript, показана в листинге 5.13. [...]

Очередь поддерживает два массива. queued числовой индексированный массив, к которому добавляются новые обновления. sent является ассоциативным массивом, содержащим те обновления, которые были отправлены на сервер, но ожидают ответа.

Вот две соответствующие функции - одна отвечает за добавление команд в очередь ( addCommand), а другая отвечает за сериализацию и затем отправку их на сервер ( fireRequest):

CommandQueue.prototype.addCommand = function(command)
{ 
    if (this.isCommand(command))
    {
        this.queue.append(command,true);
    }
}

CommandQueue.prototype.fireRequest = function()
{
    if (this.queued.length == 0)
    { 
        return; 
    }

    var data="data=";

    for (var i = 0; i < this.queued.length; i++)
    { 
        var cmd = this.queued[i]; 
        if (this.isCommand(cmd))
        {
            data += cmd.toRequestString(); 
            this.sent[cmd.id] = cmd;

            // ... and then send the contents of data in a POST request
        }
    }
}

Это должно заставить вас идти. Удачи!


Спасибо. Это очень похоже на мои идеи о том, как я буду двигаться вперед, если мы сохраним пакетные операции на клиенте. Проблема заключается в времени прохождения операции для большого количества объектов.
Марк Ренуф

Хм, хорошо - я думал, что вы хотите выполнить операцию с большим количеством объектов (на сервере) посредством упрощенного запроса. Я неправильно понял?
Кристиан Нунсиато,

Да, но я не вижу, как этот пример кода будет выполнять операцию более эффективно. Он объединяет запросы, но по-прежнему отправляет их на сервер по одному. Я неправильно понимаю?
Марк Ренуф

На самом деле он объединяет их и затем отправляет все сразу: цикл for в fireRequest () по существу собирает все ожидающие команды, сериализует их в виде строки (с .toRequestString (), например, «method = markAsRead & messageIds = 1,2,3» , 4 "), присваивает эту строку" data "и отправляет данные серверу.
Кристиан Нунциато

20

Хотя я думаю, что @Alex идет по правильному пути, концептуально я думаю, что это должно быть наоборот того, что предлагается.

URL-адрес в действительности «ресурсы, на которые мы нацелены», следовательно:

    [GET] mail/1

значит получить запись из почты с идентификатором 1 и

    [PATCH] mail/1 data: mail[markAsRead]=true

означает исправление почтовой записи с идентификатором 1. Строка запроса является «фильтром», фильтрующим данные, возвращаемые из URL.

    [GET] mail?markAsRead=true

Поэтому здесь мы запрашиваем все письма, уже помеченные как прочитанные. Таким образом, [ПАТЧИРОВАТЬ] по этому пути можно было бы сказать «исправить записи, уже помеченные как истинные» ... что мы не пытаемся достичь

Таким образом, пакетный метод, следуя этому мнению, должен быть:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

Конечно, я не говорю, что это действительно REST (который не позволяет манипулировать пакетной записью), скорее, он следует логике, уже существующей и используемой REST.


Интересный ответ! Для вашего последнего примера, не будет ли это более соответствовать [GET]формату [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](или даже просто data: {"ids": [1,2,3]})? Еще одно преимущество этого альтернативного подхода заключается в том, что вы не столкнетесь с ошибками «414 Request URI too long», если обновляете сотни / тысячи ресурсов в коллекции.
Риного

@rinogo - на самом деле нет. Это то, о чем я говорил. Строка запроса - это фильтр для записей, с которыми мы хотим работать (например, [GET] mail / 1 получает почтовую запись с идентификатором 1, тогда как [GET] mail? MarkasRead = true возвращает почту, для которой markAsRead уже имеет значение true). Нет смысла устанавливать патч на этот же URL-адрес (т. Е. «Исправлять записи, где markAsRead = true»), когда на самом деле мы хотим выполнить патч для определенных записей с идентификаторами 1,2,3, REGARDLESS текущего состояния поля markAsRead. Отсюда и метод, который я описал. Согласитесь, есть проблема с обновлением многих записей. Я бы построил менее тесно связанную конечную точку.
fezfox

11

Ваш язык, «Это кажется очень расточительным ...», для меня указывает на попытку преждевременной оптимизации. Если не будет показано, что отправка всего представления объектов является основным ударом по производительности (мы говорим неприемлемо для пользователей с> 150 мс), то нет смысла пытаться создать новое нестандартное поведение API. Помните, что чем проще API, тем проще его использовать.

Для удаления отправьте следующее, так как серверу не нужно ничего знать о состоянии объекта, прежде чем произойдет удаление.

DELETE /emails
POSTDATA: [{id:1},{id:2}]

Следующая мысль заключается в том, что если приложение сталкивается с проблемами производительности, связанными с массовым обновлением объектов, то следует рассмотреть возможность разбиения каждого объекта на несколько объектов. Таким образом, полезная нагрузка JSON является частью размера.

Например, при отправке ответа для обновления статусов «чтение» и «архивирование» двух отдельных писем вам нужно будет отправить следующее:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"someone@bratwurst.com",
              from:"someguy@frommyville.com",
              subject:"Try this recipe!",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
              read:true,
              archived:true,
              importance:2,
              labels:["Someone","Mustard"]
            },
            {
              id:2,
              to:"someone@bratwurst.com",
              from:"someguy@frommyville.com",
              subject:"Try this recipe (With Fix)",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
              read:true,
              archived:false,
              importance:1,
              labels:["Someone","Mustard"]
            }
            ]

Я бы разделил изменяемые компоненты электронной почты (чтение, архивирование, важность, метки) на отдельный объект, так как остальные (к теме, тексту) никогда не будут обновлены.

PUT /email-statuses
POSTDATA: [
            {id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
            {id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
          ]

Другой подход - использовать использование PATCH. Чтобы явно указать, какие свойства вы намереваетесь обновить и что все остальные должны игнорироваться.

PATCH /emails
POSTDATA: [
            {
              id:1,
              read:true,
              archived:true
            },
            {
              id:2,
              read:true,
              archived:false
            }
          ]

Люди утверждают, что PATCH должен быть реализован путем предоставления массива изменений, содержащих: действие (CRUD), путь (URL) и изменение значения. Это может считаться стандартной реализацией, но если вы посмотрите на весь REST API, он не интуитивно понятен. Кроме того, в приведенной выше реализации GitHub реализовал PATCH .

Подводя итог, можно придерживаться принципов RESTful с пакетными действиями и при этом иметь приемлемую производительность.


Я согласен, что PATCH имеет наибольшее значение, проблема в том, что если у вас есть другой код перехода состояния, который должен выполняться при изменении этих свойств, его становится труднее реализовать в виде простого PATCH. Я не думаю, что REST действительно приспособлен к каким-либо изменениям состояния, учитывая, что он должен быть без сохранения состояния, его не волнует, с чего он переходит, а только то, каково его текущее состояние.
BeniRose

Привет BeniRose, спасибо за добавление комментария, я часто задаюсь вопросом, видят ли люди некоторые из этих постов. Я рад, что люди это делают. Ресурсы, относящиеся к природе «REST без состояния», определяют его как проблему с сервером, которому не нужно поддерживать состояние между запросами. Таким образом, мне не ясно, какую проблему вы описывали, можете ли вы привести пример?
Джастин Хьюи

8

API Google Drive имеет действительно интересную систему для решения этой проблемы ( см. Здесь ).

В основном они группируют разные запросы в одном Content-Type: multipart/mixedзапросе, каждый отдельный завершенный запрос разделяется определенным разделителем. Заголовки и параметры запроса пакетного запроса наследуются отдельным запросам (т. Е. Authorization: Bearer some_token), Если они не переопределены в отдельном запросе.


Пример : (взято из их документов )

Запрос:

POST https://www.googleapis.com/batch

Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963

--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"example@appsrocks.com",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
  "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

Отклик:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT

--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "12218244892818058021i"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "04109509152946699072k"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk--

1

Я был бы соблазн в операции, подобной той, что в вашем примере, написать анализатор диапазона.

Нетрудно создать парсер, который может читать «messageIds = 1-3,7-9,11,12-15». Это, безусловно, повысит эффективность общих операций, охватывающих все сообщения, и будет более масштабируемым.


Хорошее наблюдение и хорошая оптимизация, но вопрос был в том, может ли этот стиль запроса быть «совместимым» с концепцией REST.
Марк Ренуф

Привет, да, я понимаю. Оптимизация делает концепцию более полезной, и я не хотел упускать из виду мой совет только потому, что он немного отклонился от темы.

1

Отличный пост. Я искал решение в течение нескольких дней. Я пришел к решению использовать передачу строки запроса с кучей идентификаторов, разделенных запятыми, например:

DELETE /my/uri/to/delete?id=1,2,3,4,5

... затем передать это WHERE INв мой SQL. Это прекрасно работает, но интересно, что другие думают об этом подходе.


1
Мне это не очень нравится, потому что он как бы вводит новый тип, строку, которую вы используете в качестве списка, где в. Я бы предпочел вместо этого разобрать его для конкретного языкового типа, а затем я могу использовать тот же метод в одинаково в нескольких разных частях системы.
softarn

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

2
Зависит от желаемого поведения, DELETE /books/delete?id=1,2,3когда книга № 3 не существует - они WHERE INбудут молча игнорировать записи, тогда как я обычно ожидаю DELETE /books/delete?id=3404, если 3 не существует.
февраля

3
Другой проблемой, с которой вы можете столкнуться при использовании этого решения, является ограничение на количество символов в строке URL. Если кто-то решит массово удалить 5000 записей, браузер может отклонить URL-адрес или HTTP-сервер (например, Apache) может отклонить его. Общее правило (которое, как мы надеемся, меняется с улучшением серверов и программного обеспечения) должно было соответствовать максимальному размеру 2 КБ. Где с телом POST вы можете пойти до 10 МБ. stackoverflow.com/questions/2364840/…
justin.hughey

0

С моей точки зрения, я думаю, что Facebook имеет лучшую реализацию.

Один HTTP-запрос выполняется с параметром пакета, а другой - для токена.

В пакет отправляется JSON. который содержит коллекцию «запросов». Каждый запрос имеет свойство метода (get / post / put / delete / etc ...) и свойствоlative_url (URI конечной точки), кроме того, методы post и put позволяют использовать свойство «body», в котором обновляются поля. посланы .

больше информации на: Facebook пакетного API

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.