Шаблоны для обработки пакетных операций в веб-службах 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. Он отлично работает, но задайтесь вопросом, что другие думают об этом подходе.