web services - services - Patrones para manejar operaciones por lotes en servicios web REST?
web service rest c# (7)
Buena publicación. He estado buscando una solución por unos días. Se me ocurrió una solución de usar pasar una cadena de consulta con un conjunto de ID separados por comas, como:
DELETE /my/uri/to/delete?id=1,2,3,4,5
... luego pasando eso a una cláusula WHERE IN
en mi SQL. Funciona muy bien, pero se preguntan qué piensan los demás de este enfoque.
¿Qué patrones de diseño probados existen para las operaciones por lotes en recursos dentro de un servicio web de estilo REST?
Estoy tratando de encontrar un equilibrio entre los ideales y la realidad en términos de rendimiento y estabilidad. Ahora tenemos una API donde todas las operaciones se recuperan de un recurso de la lista (es decir: GET / usuario) o en una sola instancia (PUT / user / 1, DELETE / user / 22, etc.).
Hay algunos casos en los que desea actualizar un solo campo de un conjunto completo de objetos. Parece un desperdicio enviar toda la representación de cada objeto de ida y vuelta para actualizar el campo.
En una API de estilo RPC, podrías tener un método:
/mail.do?method=markAsRead&messageIds=1,2,3,4... etc.
¿Cuál es el equivalente REST aquí? ¿O está bien comprometerse de vez en cuando? ¿Arruina el diseño para agregar algunas operaciones específicas donde realmente mejora el rendimiento, etc.? El cliente en todos los casos en este momento es un navegador web (aplicación javascript en el lado del cliente).
En absoluto, creo que el equivalente de REST es (o al menos una solución es) casi exactamente eso: una interfaz especializada diseñada para acomodar una operación requerida por el cliente.
Me recuerda un patrón mencionado en el libro de Crane y Pascarello Ajax in Action (un excelente libro, por cierto, muy recomendado) en el que ilustran la implementación de un tipo de objeto CommandQueue cuyo trabajo consiste en poner en cola las solicitudes en lotes y luego, publíquelos en el servidor periódicamente.
El objeto, si mal no recuerdo, esencialmente solo contenía una serie de "comandos", por ejemplo, para extender su ejemplo, cada uno un registro que contiene un comando "markAsRead", un "messageId" y quizás una referencia a un callback / manejador función - y luego de acuerdo con alguna programación, o en alguna acción del usuario, el objeto de comando sería serializado y publicado en el servidor, y el cliente manejaría el posterior procesamiento posterior.
No tengo los detalles a mano, pero parece que una cola de comandos de este tipo sería una forma de manejar su problema; Reduciría sustancialmente la cantidad de conversaciones en general, y abstraería la interfaz del lado del servidor de una manera que podría ser más flexible en el futuro.
Actualización : ¡Ajá! He encontrado un recorte de ese mismo libro en línea, completo con muestras de código (¡aunque todavía sugiero que recoja el libro real!). Eche un vistazo aquí , comenzando con la sección 5.5.3:
Esto es fácil de codificar, pero puede generar muchos bits de tráfico muy pequeños en el servidor, lo que es ineficiente y potencialmente confuso. Si queremos controlar nuestro tráfico, podemos capturar estas actualizaciones y ponerlas en cola localmente y luego enviarlas al servidor en lotes cuando lo deseemos. En la lista 5.13 se muestra una cola de actualización simple implementada en JavaScript. [...]
La cola mantiene dos arrays.
queued
es una matriz indexada numéricamente, a la que se añaden nuevas actualizaciones.sent
es una matriz asociativa, que contiene las actualizaciones que se han enviado al servidor pero que están esperando una respuesta.
Aquí hay dos funciones pertinentes: una responsable de agregar comandos a la cola ( addCommand
) y otra responsable de serializar y luego enviarlos al servidor ( 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
}
}
}
Eso debería hacerte avanzar. ¡Buena suerte!
La API de Google Drive tiene un sistema realmente interesante para resolver este problema ( ver aquí ).
Lo que hacen es básicamente agrupar las diferentes solicitudes en una solicitud Content-Type: multipart/mixed
, con cada solicitud completa individual separada por algún delimitador definido. Los encabezados y el parámetro de consulta de la solicitud por lotes se heredan para las solicitudes individuales (es decir, Authorization: Bearer some_token
) a menos que se anulen en la solicitud individual.
Ejemplo : (tomado de sus documentos )
Solicitud:
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--
Respuesta:
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--
Me sentiría tentado en una operación como la de tu ejemplo de escribir un analizador de rango.
No es demasiado molesto hacer un analizador que pueda leer "messageIds = 1-3,7-9,11,12-15". Ciertamente aumentaría la eficiencia para las operaciones generales que cubren todos los mensajes y es más escalable.
Si bien creo que @Alex está en el camino correcto, conceptualmente creo que debería ser lo contrario de lo que se sugiere.
La URL es en efecto "los recursos a los que apuntamos", por lo tanto:
[GET] mail/1
significa obtener el registro del correo con id 1 y
[PATCH] mail/1 data: mail[markAsRead]=true
significa parchear el registro de correo con id 1. La cadena de consulta es un "filtro", filtrando los datos devueltos desde la URL.
[GET] mail?markAsRead=true
Entonces aquí estamos solicitando todo el correo ya marcado como leído. Entonces, [PATCH] a este camino sería decir "parchear los registros ya marcados como verdaderos" ... que no es lo que estamos tratando de lograr.
Entonces, un método por lotes, siguiendo este pensamiento debería ser:
[PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true
por supuesto, no estoy diciendo que esto sea cierto REST (que no permite la manipulación de registros por lotes), sino que sigue la lógica ya existente y en uso por REST.
Su lenguaje, " Parece muy derrochador ...", para mí indica un intento de optimización prematura. A menos que se pueda demostrar que el envío de toda la representación de objetos es un gran golpe de rendimiento (estamos hablando de inaceptable para los usuarios como> 150ms), entonces no tiene sentido intentar crear un nuevo comportamiento de API no estándar. Recuerde, cuanto más simple es la API, más fácil es de usar.
Para eliminar envíe lo siguiente ya que el servidor no necesita saber nada sobre el estado del objeto antes de que se produzca la eliminación.
DELETE /emails
POSTDATA: [{id:1},{id:2}]
El siguiente pensamiento es que si una aplicación tiene problemas de rendimiento con respecto a la actualización masiva de objetos, se debe considerar dividir cada objeto en varios objetos. De esta forma, la carga útil de JSON es una fracción del tamaño.
Como ejemplo, al enviar una respuesta para actualizar los estados de "lectura" y "archivado" de dos correos electrónicos por separado, deberá enviar lo siguiente:
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"]
}
]
Yo dividiría los componentes mutables del correo electrónico (leer, archivar, importancia, etiquetas) en un objeto separado ya que los otros (a, desde, tema, texto) nunca se actualizarían.
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"]}
]
Otro enfoque a seguir es aprovechar el uso de un PARCHE. Para indicar explícitamente qué propiedades tiene la intención de actualizar y que todas las demás deben ignorarse.
PATCH /emails
POSTDATA: [
{
id:1,
read:true,
archived:true
},
{
id:2,
read:true,
archived:false
}
]
Las personas afirman que PATCH debe implementarse proporcionando una variedad de cambios que contengan: acción (CRUD), ruta (URL) y cambio de valor. Esto se puede considerar como una implementación estándar, pero si observa la totalidad de una API REST no es una intuición única. Además, la implementación anterior es cómo GitHub ha implementado PATCH .
En resumen, es posible adherirse a los principios RESTful con acciones por lotes y aun así tener un rendimiento aceptable.
Un patrón RESTful simple para lotes es hacer uso de un recurso de colección. Por ejemplo, para eliminar varios mensajes a la vez.
DELETE /mail?&id=0&id=1&id=2
Es un poco más complicado actualizar por lotes recursos parciales, o atributos de recursos. Es decir, actualice cada atributo marcado como Lectura. Básicamente, en lugar de tratar el atributo como parte de cada recurso, lo tratas como un cubo en el que poner recursos. Un ejemplo ya fue publicado. Lo ajusté un poco.
POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]
Básicamente, está actualizando la lista de correos marcados como leídos.
También puede usar esto para asignar varios elementos a la misma categoría.
POST /mail?category=junk
POSTDATA: ids=[0,1,2]
Obviamente, es mucho más complicado hacer actualizaciones parciales de lotes estilo iTunes (por ejemplo, artista + título de álbum pero no trackTitle). La analogía del cubo comienza a descomponerse.
POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]
A largo plazo, es mucho más fácil actualizar un solo recurso parcial o atributos de recursos. Solo haga uso de un subrecurso.
POST /mail/0/markAsRead
POSTDATA: true
Alternativamente, puede usar recursos parametrizados. Esto es menos común en los patrones REST, pero está permitido en las especificaciones URI y HTTP. Un punto y coma divide los parámetros relacionados horizontalmente dentro de un recurso.
Actualizar varios atributos, varios recursos:
POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk
Actualice varios recursos, solo un atributo:
POST /mail/0;1;2/markAsRead
POSTDATA: true
Actualice varios atributos, solo un recurso:
POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk
La creatividad RESTful abunda.