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

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

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

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

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

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

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

Ответ 1

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

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

Это немного сложнее для пакетного обновления частичных ресурсов или атрибутов ресурсов. То есть обновите каждый атрибут markAsRead. В принципе, вместо того, чтобы рассматривать атрибут как часть каждого ресурса, вы рассматриваете его как ведро, в которое нужно поместить ресурсы. Один пример уже опубликован. Я немного поправился.

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 изобилует.

Ответ 2

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

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

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

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


Обновление: Ага! Я нашел отрывок из этой самой книги в Интернете, в комплекте с образцами кода (хотя я все же предлагаю собрать реальную книгу!). Посмотрите здесь, начиная с раздела 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
        }
    }
}

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

Ответ 3

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

URL-адрес - это "ресурсы, на которые мы нацеливаемся":

    [GET] mail/1

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

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

означает патч почтовой записи с идентификатором 1. Querystring является "фильтром", фильтруя данные, возвращаемые с URL-адреса.

    [GET] mail?markAsRead=true

Итак, здесь мы запрашиваем всю почту, уже отмеченную как прочитанную. Таким образом, [PATCH] на этот путь будет сказано: "Запланируйте записи уже, отмеченные как истинные"... это не то, что мы пытаемся достичь.

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

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

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

Ответ 4

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

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

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

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

В качестве примера при отправке ответа на обновление "прочитанных" и "архивных" статусов двух отдельных электронных писем вам необходимо отправить следующее:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"[email protected]",
              from:"[email protected]",
              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:"[email protected]",
              from:"[email protected]",
              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 с пакетными действиями и по-прежнему иметь приемлемую производительность.

Ответ 5

API-интерфейс google имеет действительно интересную систему для решения этой проблемы (см. здесь).

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


Пример: (взято из docs)

Запрос:

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":"[email protected]",
  "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--

Ответ 6

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

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

Ответ 7

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

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

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