API REST: creación o actualización masiva en una sola solicitud [cerrado]

94

Supongamos que hay dos recursos Bindery Doccon relación de asociación, lo que significa que Docy se Bindermantienen por sí mismos. Docpodría o no pertenecer a Bindery Binderpodría estar vacío.

Si quiero diseñar una API REST que permite a un usuario enviar una colección de Docs, en una única solicitud , como la siguiente:

{
  "docs": [
    {"doc_number": 1, "binder": 1}, 
    {"doc_number": 5, "binder": 8},
    {"doc_number": 6, "binder": 3}
  ]
}

Y para cada documento en el docs,

  • Si docexiste, asígnelo aBinder
  • Si docno existe, créelo y luego asígnelo

Estoy realmente confundido sobre cómo se debe implementar esto:

  • ¿Qué método HTTP utilizar?
  • ¿Qué código de respuesta se debe devolver?
  • ¿Esto está calificado para DESCANSO?
  • ¿Cómo se vería la URI? /binders/docs?
  • Manejo de solicitudes masivas, ¿qué pasa si algunos elementos generan un error pero el otro pasa? ¿Qué código de respuesta se debe devolver? ¿Debería ser atómica la operación a granel?
Sam R.
fuente

Respuestas:

59

Creo que podría usar un método POST o PATCH para manejar esto, ya que generalmente diseñan para esto.

  • El uso de un POSTmétodo generalmente se usa para agregar un elemento cuando se usa en un recurso de lista, pero también puede admitir varias acciones para este método. Vea esta respuesta: Cómo actualizar una colección de recursos REST . También puede admitir diferentes formatos de representación para la entrada (si corresponden a una matriz o un solo elemento).

    En el caso, no es necesario definir su formato para describir la actualización.

  • El uso de un PATCHmétodo también es adecuado, ya que las solicitudes correspondientes corresponden a una actualización parcial. Según RFC5789 ( http://tools.ietf.org/html/rfc5789 ):

    Varias aplicaciones que extienden el Protocolo de transferencia de hipertexto (HTTP) requieren una función para realizar modificaciones parciales de recursos. El método HTTP PUT existente solo permite la sustitución completa de un documento. Esta propuesta agrega un nuevo método HTTP, PATCH, para modificar un recurso HTTP existente.

    En el caso, debe definir su formato para describir la actualización parcial.

Creo que en este caso, POSTy PATCHson bastante similares ya que realmente no es necesario describir la operación a realizar para cada elemento. Yo diría que depende del formato de la representación a enviar.

El caso de PUTes un poco menos claro. De hecho, cuando utilice un método PUT, debe proporcionar la lista completa. De hecho, la representación proporcionada en la solicitud sustituirá a la del recurso de lista.

Puede tener dos opciones con respecto a las rutas de recursos.

  • Usando la ruta de recursos para la lista de documentos

En este caso, debe proporcionar explícitamente el enlace de los documentos con una carpeta en la representación que proporcione en la solicitud.

Aquí hay una ruta de muestra para esto /docs.

El contenido de tal enfoque podría ser por método POST:

[
    { "doc_number": 1, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 2, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 3, "binder": 5, (other fields in the case of creation) },
    (...)
]
  • Usando la ruta de recursos secundarios del elemento de enlace

Además, también podría considerar aprovechar las subrutas para describir el vínculo entre documentos y carpetas. Las sugerencias sobre la asociación entre un documento y una carpeta no tienen que especificarse ahora en el contenido de la solicitud.

Aquí hay una ruta de muestra para esto /binder/{binderId}/docs. En este caso, enviar una lista de documentos con un método POSTo PATCHadjuntará documentos a la carpeta con un identificador binderIddespués de haber creado el documento si no existe.

El contenido de tal enfoque podría ser por método POST:

[
    { "doc_number": 1, (other fields in the case of creation) },
    { "doc_number": 2, (other fields in the case of creation) },
    { "doc_number": 3, (other fields in the case of creation) },
    (...)
]

En cuanto a la respuesta, depende de usted definir el nivel de respuesta y los errores a devolver. Veo dos niveles: el nivel de estado (nivel global) y el nivel de carga útil (nivel más delgado). También depende de usted definir si todas las inserciones / actualizaciones correspondientes a su solicitud deben ser atómicas o no.

  • Atómico

En este caso, puede aprovechar el estado HTTP. Si todo va bien, obtienes un estatus 200. Si no es así, otro estado como 400si los datos proporcionados no son correctos (por ejemplo, la identificación de la carpeta no es válida) o algo más.

  • No atómico

En este caso, 200se devolverá un estado y depende de la representación de la respuesta describir lo que se hizo y dónde eventualmente ocurren los errores. ElasticSearch tiene un punto final en su API REST para actualización masiva. Esto podría darle algunas ideas a este nivel: http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/bulk.html .

  • Asincrónico

También puede implementar un procesamiento asincrónico para manejar los datos proporcionados. En este caso, las devoluciones de estado HTTP serán 202. El cliente necesita extraer un recurso adicional para ver qué sucede.

Antes de terminar, también me gustaría notar que la especificación de OData aborda el problema con respecto a las relaciones entre entidades con la función denominada enlaces de navegación . Quizás podrías echarle un vistazo a esto ;-)

El siguiente enlace también puede ayudarlo: https://templth.wordpress.com/2014/12/15/designing-a-web-api/ .

Espero que te ayude, Thierry

Thierry Templier
fuente
Tengo un seguimiento de la pregunta. Opté por rutas planas sin un recurso secundario anidado. Para obtener todos los documentos que llamo GET /docsy recuperar todos los documentos dentro de una carpeta particular GET /docs?binder_id=x. Para eliminar un subconjunto de los recursos, ¿llamaría DELETE /docs?binder_id=xo debería llamar DELETE /docscon un {"binder_id": x}en el cuerpo de la solicitud? ¿Lo usaría alguna vez PATCH /docs?binder_id=xpara una actualización por lotes, o simplemente PATCH /docsy pasaría pares?
Andy Fusniak
35

Probablemente necesitará usar POST o PATCH, porque es poco probable que una sola solicitud que actualice y cree múltiples recursos sea idempotente.

Hacerlo PATCH /docses definitivamente una opción válida. Puede encontrar complicado el uso de los formatos de parche estándar para su escenario particular. No estoy seguro de esto.

Puede usar 200. También puede usar 207 - Multi Status

Esto se puede hacer de forma RESTful. La clave, en mi opinión, es tener algún recurso que esté diseñado para aceptar un conjunto de documentos para actualizar / crear.

Si usa el método PATCH, creo que su operación debería ser atómica. es decir, no usaría el código de estado 207 y luego reportaría éxitos y fallas en el cuerpo de respuesta. Si utiliza la operación POST, el enfoque 207 es viable. Tendrá que diseñar su propio cuerpo de respuesta para comunicar qué operaciones tuvieron éxito y cuáles fallaron. No tengo conocimiento de uno estandarizado.

Darrel Miller
fuente
Muchas gracias. Por This can be done in a RESTful wayqué se refiere la actualización y crear debe hacer por separado?
Sam R.
1
@norbertpy Realizar algún tipo de operación de escritura en un recurso puede hacer que se actualicen y creen otros recursos a partir de una única solicitud. REST no tiene ningún problema con eso. Mi elección de frase fue porque algunos marcos implementan operaciones masivas serializando solicitudes HTTP en documentos de varias partes y luego enviando las solicitudes HTTP serializadas como un lote. Creo que ese enfoque viola la restricción REST de identificación de recursos.
Darrel Miller
19

PUT ING

PUT /binders/{id}/docs Crear o actualizar y relacionar un solo documento con una carpeta

p.ej:

PUT /binders/1/docs HTTP/1.1
{
  "docNumber" : 1
}

PARCHE ING

PATCH /docs Crear documentos si no existen y relacionarlos con carpetas

p.ej:

PATCH /docs HTTP/1.1
[
    { "op" : "add", "path" : "/binder/1/docs", "value" : { "doc_number" : 1 } },
    { "op" : "add", "path" : "/binder/8/docs", "value" : { "doc_number" : 8 } },
    { "op" : "add", "path" : "/binder/3/docs", "value" : { "doc_number" : 6 } }
] 

Incluiré información adicional más adelante, pero mientras tanto, si lo desea, eche un vistazo a RFC 5789 , RFC 6902 y William Durand's Please. Entrada de blog Don't Patch Like an Idiot .

Mauricio morales
fuente
2
En algún momento, el cliente necesita una operación masiva y no quiere importarle si el recurso está ahí o no. Como dije en la pregunta, el cliente quiere enviar un montón de docsy asociarlos con binders. El cliente quiere crear carpetas si no existen y hacer la asociación si existe. En UNA SOLA solicitud a granel.
Sam R.
12

En un proyecto en el que trabajé, resolvimos este problema implementando algo que llamamos solicitudes 'Batch'. Definimos una ruta /batchdonde aceptamos json en el siguiente formato:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 5,
         binder: 8
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      }
   },
]

La respuesta tiene el código de estado 207 (Multi-Status) y se ve así:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
      status: 200
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         error: {
            msg: 'A document with doc_number 5 already exists'
            ...
         }
      },
      status: 409
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      },
      status: 200
   },
]

También puede agregar soporte para encabezados en esta estructura. Implementamos algo que resultó útil, que eran las variables para usar entre solicitudes en un lote, lo que significa que podemos usar la respuesta de una solicitud como entrada a otra.

Facebook y Google tienen implementaciones similares:
https://developers.google.com/gmail/api/guides/batch
https://developers.facebook.com/docs/graph-api/making-multiple-requests

Cuando desee crear o actualizar un recurso con la misma llamada, usaría POST o PUT según el caso. Si el documento ya existe, ¿desea que todo el documento sea:

  1. ¿Reemplazado por el documento que envía (es decir, las propiedades que faltan en la solicitud se eliminarán y se sobrescribirán las existentes)?
  2. ¿Se fusionó con el documento que envía (es decir, las propiedades que faltan en la solicitud no se eliminarán y las propiedades ya existentes se sobrescribirán)?

En caso de que desee el comportamiento de la alternativa 1, debe usar un POST y en caso de que desee el comportamiento de la alternativa 2, debe usar PUT.

http://restcookbook.com/HTTP%20Methods/put-vs-post/

Como la gente ya sugirió, también podría optar por PATCH, pero prefiero mantener la API simple y no usar verbos adicionales si no son necesarios.

David Berg
fuente
5
Me gusta esta respuesta para la Prueba de concepto, así como los enlaces de Google y Facebook. Pero no estoy de acuerdo con la parte final sobre POST o PUT. En los 2 casos mencionados en esta respuesta, el primero debe ser PUT y el segundo debe ser PATCH.
RayLuo
@RayLuo, ¿puedes explicar por qué necesitamos PATCH además de POST y PUT?
David Berg
2
Porque para eso se inventó el PATCH. Puede leer esta definición y ver cómo PUT y PATCH coinciden con sus 2 viñetas.
RayLuo
@DavidBerg, parece que Google ha preferido otro enfoque para procesar solicitudes por lotes, es decir, separar el encabezado y el cuerpo de cada sub solicitud de la parte correspondiente de una solicitud principal, con un límite como --batch_xxxx. ¿Existen algunas diferencias cruciales entre las soluciones de Google y Facebook? Además, sobre "utilizar la respuesta de una solicitud como entrada para otra", suena muy interesante, ¿te importaría compartir más detalles? o qué tipo de escenario debería utilizarse?
Yang