Шаблоны для обработки пакетных операций в веб-сервисах REST? - PullRequest
163 голосов
/ 04 февраля 2009

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

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

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

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

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

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

Ответы [ 8 ]

73 голосов
/ 27 октября 2009

Простой шаблон 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

ОТДЫХОВОГО творчества предостаточно.

24 голосов
/ 04 февраля 2009

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

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

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

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


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

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

19 голосов
/ 13 марта 2014

Хотя я думаю, что @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.

11 голосов
/ 23 октября 2014

Ваш язык "Это кажется очень расточительным ...", для меня указывает на попытку преждевременной оптимизации. Если нельзя доказать, что отправка всего представления объектов является серьезным ударом по производительности (мы говорим неприемлемо для пользователей, как> 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 с пакетными действиями и при этом иметь приемлемую производительность.

7 голосов
/ 21 июня 2016

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 голос
/ 03 июля 2014

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

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

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

1 голос
/ 04 февраля 2009

В такой операции, как в вашем примере, я хотел бы написать парсер диапазона.

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

0 голосов
/ 17 мая 2018

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

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

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

Более подробная информация по адресу: API пакета Facebook

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...