2009-02-04 17 views
146

¿Qué patrones de diseño probados existen para las operaciones por lotes en recursos dentro de un servicio web de estilo REST?Patrones para manejar operaciones por lotes en servicios web REST?

Estoy tratando de lograr 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 estilo RPC, usted podría tener un método:

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

¿Cuál es el equivalente a descansar 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).

Respuesta

22

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 adaptarse a una operación requerida por el cliente.

Me recuerda a un patrón mencionado en la grúa y de Pascarello libro Ajax in Action (un libro excelente, por cierto - muy recomendable) en el que se ilustran la implementación de un CommandQueue tipo de objeto, cuyo trabajo es hacer cola solicitudes en lotes y luego publicarlos en el servidor periódicamente.

El objeto, si no recuerdo mal, 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 una función de devolución de llamada/manejador - y luego, de acuerdo con alguna programación, o en alguna acción del usuario, el objeto de comando se serializaría y publicaría 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: Aha! 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!). Have a look here, comenzando con la sección 5.5.3:

Esto es fácil de código, pero puede dar lugar a una gran cantidad de muy pequeños trozos de tráfico a el servidor, que es ineficiente y potencialmente confusa. Si queremos controlar nuestro tráfico, podemos capturar estas actualizaciones y ponerlas en cola localmente y luego enviarlas al servidor en lotes en nuestro tiempo libre. En la lista 5.13 se muestra una cola de actualización simple implementada en JavaScript . [...]

La cola mantiene dos matrices.queued es una matriz indexada numéricamente, al cuyas nuevas actualizaciones se anexan. sent es una matriz asociativa que contiene las actualizaciones que se han enviado al servidor pero que esperan una respuesta .

Aquí hay dos funciones pertinentes - responsable de añadir comandos a la cola (addCommand), y responsable de la serialización 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 para que te vayas. ¡Buena suerte!

+0

Gracias. Eso es muy similar a mis ideas sobre cómo avanzaría si mantuviéramos las operaciones por lotes en el cliente. El problema es el tiempo de ida y vuelta para realizar una operación en una gran cantidad de objetos. –

+0

Hm, está bien - Pensé que quería realizar la operación en una gran cantidad de objetos (en el servidor) mediante una solicitud de poco peso. ¿Lo entendí mal? –

+0

Sí, pero no veo cómo esa muestra de código podría realizar la operación de manera más eficiente. Encabeza las solicitudes, pero todavía las envía al servidor de a una por vez. ¿Estoy malinterpretando? –

1

Me gustaría una operación como la de su ejemplo para escribir un analizador de rango.

No es muy 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.

+0

Buena observación y una buena optimización, pero la pregunta era si este estilo de solicitud podría ser alguna vez "compatible" con el concepto REST. –

+0

Hola, sí lo entiendo. La optimización hace que el concepto sea más RESTful y no quería dejar de lado mi consejo solo porque estaba vagando un poco lejos del tema. –

71

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ás 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] 

Es obvio que es mucho más complicado de hacer actualizaciones parciales de lotes del estilo de iTunes (por ejemplo, artista + albumTitle 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 utilizar 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 

Actualizar varios recursos, sólo uno de los atributos:

POST /mail/0;1;2/markAsRead 
POSTDATA: true 

Actualizar varios atributos, sólo uno de los recursos:

POST /mail/0/markAsRead;category 
POSTDATA: markAsRead=true,category=junk 

El REST la creatividad abunda

+1

Uno podría argumentar que su eliminación debería ser una publicación, ya que en realidad no está destruyendo ese recurso. –

+0

métodos idempotentes. obtener es leer la publicación es para crear/agregar. Poner es actualizar/reemplazar. Eliminar es eliminar/destruir. – Alex

+0

Estoy confundido ¿Estás diciendo que POST es idempotente? No es. Además, su asignación de verbos HTTP a CRUD no es 100% verdadera, es común que no se requiera. –

19

Aunque 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 que nos dirigimos", por lo tanto:

[GET] mail/1 

los medios conseguir el registro de correo electrónico con id 1 y

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

medios parchear el registro electrónico con id 1 . La cadena de consulta es un "filtro" que filtra los datos devueltos desde la URL.

[GET] mail?markAsRead=true 

Así que aquí estamos solicitando todos los correos ya marcados como leídos. Entonces, para [PATCH] a este camino sería decir "parchear los registros ya marcados como verdaderos" ... que no es lo que estamos tratando de lograr.

lo tanto, un método de lotes, siguiendo este pensamiento debe ser:

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

por supuesto, no estoy diciendo que esto es verdadero reposo (que duerma permiso de manipulación de registro del lote), sino que sigue la lógica ya existente y en uso por REST.

+0

¡Respuesta interesante! Para su último ejemplo, ¿no sería más consistente con el formato '[GET]' hacer '[PATCH] mail? MarkAsRead = true data: [{" id ": 1}, {" id ": 2}, {"id": 3}] '(o incluso solo' data: {"ids": [1,2,3]} ')? Otro beneficio de este enfoque alternativo es que no se encontrará con los errores de "414 Request URI too long" si está actualizando cientos/miles de recursos en la colección. – rinogo

+0

@rinogo - en realidad no. Este es el punto que estaba haciendo. La cadena de consulta es un filtro para los registros sobre los que queremos actuar (p. Ej., [GET] mail/1 obtiene el registro con una identificación de 1, mientras que [GET] mail? MarkasRead = true devuelve el correo donde markAsRead ya es verdadero). No tiene sentido parchear en la misma URL (es decir, "parchear los registros donde markAsRead = true") cuando de hecho queremos parchar registros particulares con los identificadores 1,2,3, INDEPENDIENTEMENTE del estado actual del campo markAsRead. De ahí el método que describí. Acepte que hay un problema con la actualización de muchos registros. Construiría un punto final menos estrechamente acoplado. – fezfox

1

Great post. He estado buscando una solución por unos días. Se me ocurrió una solución de utilizar que pasa una cadena de consulta con los ID manojo separadas por comas, como:

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

... entonces pasarlos a una cláusula WHERE IN en mi SQL. Funciona muy bien, pero se preguntan qué piensan los demás de este enfoque.

+1

Realmente no me gusta porque introduce un tipo nuevo, la cadena que usa como una lista en donde. Preferiría analizarlo en un tipo específico de idioma y luego puedo usar el mismo método en de la misma manera en múltiples partes diferentes del sistema. – softarn

+4

Un recordatorio para ser cauteloso con los ataques de inyección de SQL y siempre limpiar sus datos y usar los parámetros de vinculación cuando se toma este enfoque. –

+2

Depende del comportamiento deseado de 'DELETE/books/delete? Id = 1,2,3' cuando el libro # 3 no existe - el' WHERE IN' ignorará silenciosamente los registros, mientras que normalmente esperaría 'DELETE/books/delete? id = 3' a 404 si 3 no existe. – chbrown

11

Su lenguaje, "Es 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 se está ejecutando en problemas de rendimiento con respecto a la actualización masiva de objetos y luego en consideración rompiendo cada objeto en múltiples objetos se debe dar. De esta forma, la carga útil de JSON es una fracción del tamaño.

A modo de ejemplo al enviar una respuesta a actualizar el "leer" y "archivados" Estados de dos correos electrónicos separados que tendrían que enviar el 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"] 
      } 
      ] 

sin dividir los componentes mutables de la correo electrónico (leer, archivar, importancia, etiquetas) en un objeto separado ya que los otros (a, de, sujeto, 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 PATCH. 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 
      } 
      ] 

personas declaran que parche debe ser implementada proporcionando una serie de cambios que contienen: acción (CRUD), trayectoria (URL), y el 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 has implemented PATCH.

Para resumir, es posible adherirse a los principios RESTful con acciones por lotes y aún así tener un rendimiento aceptable.

+0

Estoy de acuerdo en que PATCH tiene más sentido, el problema es que si tiene otro código de transición de estado que necesita ejecutarse cuando esas propiedades cambian, se vuelve más difícil de implementar como un simple PATCH. No creo que REST realmente se adapte a cualquier tipo de transición de estado, dado que se supone que es apátrida, no importa de qué está pasando, solo cuál es su estado actual. – BeniRose

+0

Hola BeniRose, gracias por agregar un comentario, a menudo me pregunto si la gente ve algunas de estas publicaciones. Me alegra ver que las personas lo hacen. Los recursos relacionados con la naturaleza "sin estado" de REST lo definen como una preocupación porque el servidor no tiene que mantener el estado en todas las solicitudes. Como tal, no me queda claro qué tema describió, ¿puede explicarlo con un ejemplo? –

5

La API de Google Drive tiene un sistema realmente interesante para resolver este problema (see here).

Lo que hacen es básicamente agrupar 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 su docs) 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-- 
Cuestiones relacionadas