tutorial servicios services restful espaƱol entre ejemplo diferencia web-services rest

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.