Práctica recomendada para actualizaciones parciales en un servicio RESTful

Estoy escribiendo un servicio RESTful para un sistema de gestión de clientes y estoy tratando de encontrar la mejor práctica para actualizar registros parcialmente. Por ejemplo, quiero que la persona que llama pueda leer el registro completo con una solicitud GET. Pero para actualizarlo solo se permiten ciertas operaciones en el registro, como cambiar el estado de ENABLED a DISABLED. (Tengo escenarios más complejos que este)

No quiero que la persona que llama envíe el registro completo solo con el campo actualizado por razones de seguridad (también parece exagerado).

¿Hay una forma recomendada de construir los URI? Al leer los libros REST, las llamadas al estilo RPC parecen estar mal vistas.

Si la siguiente llamada devuelve el registro completo del cliente con el ID 123

GET /customer/123  {lots of attributes} ENABLED {even more attributes}  

¿cómo debo actualizar el estado?

 POST /customer/123/status DISABLED POST /customer/123/changeStatus DISABLED ... 

Actualización : Para boost la pregunta. ¿Cómo se incorporan las ‘llamadas de lógica de negocios’ en una API REST? ¿Hay una forma acordada de hacer esto? No todos los métodos son CRUD por naturaleza. Algunos son más complejos, como ‘ sendEmailToCustomer (123) ‘, ‘ mergeCustomers (123, 456) ‘, ‘ countCustomers ()

 POST /customer/123?cmd=sendEmail POST /cmd/sendEmail?customerId=123 GET /customer/count 

Gracias Frank

Básicamente tienes dos opciones:

  1. Use PATCH (pero tenga en cuenta que debe definir su propio tipo de medio que especifique lo que sucederá exactamente)

  2. Use POST para un recurso secundario y devuelva 303. Consulte Otro con el encabezado de Ubicación apuntando al recurso principal. La intención del 303 es decirle al cliente: “He realizado su POST y el efecto fue que se actualizó algún otro recurso. Consulte el encabezado de ubicación para el recurso que era”. POST / 303 está diseñado para adiciones iterativas a recursos para construir el estado de algún recurso principal y es perfecto para actualizaciones parciales.

Debe usar POST para actualizaciones parciales.

Para actualizar los campos para el cliente 123, haga una POST a / cliente / 123.

Si desea actualizar solo el estado, también puede PONER en / customer / 123 / status.

En general, las solicitudes GET no deben tener ningún efecto secundario, y PUT es para escribir / reemplazar todo el recurso.

Esto se sigue directamente de HTTP, como se ve aquí: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods

Debe usar PATCH para actualizaciones parciales, ya sea usando documentos json-patch (vea http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 o http://www.mnot.net/ blog / 2012/09/05 / patch ) o el marco de parches XML (ver http://tools.ietf.org/html/rfc5261 ). En mi opinión, sin embargo, json-patch es la mejor opción para su clase de datos comerciales.

PATCH con documentos de parche JSON / XML tiene una semántica directa muy estrecha para actualizaciones parciales. Si comienza a usar POST, con copias modificadas del documento original, para actualizaciones parciales, pronto se encontrará con problemas en los que desea valores perdidos (o, mejor dicho, valores nulos) para representar “ignorar esta propiedad” o “establecer esta propiedad en el valor vacío “- y eso lleva a un agujero de conejo de soluciones pirateadas que al final dará como resultado su propio tipo de formato de parche.

Puede encontrar una respuesta más detallada aquí: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .

Me estoy encontrando con un problema similar. PUT en un sub-recurso parece funcionar cuando quiere actualizar solo un campo. Sin embargo, a veces desea actualizar un montón de cosas: piense en un formulario web que represente el recurso con la opción de cambiar algunas entradas. El envío del formulario del usuario no debe dar como resultado un PUT múltiple.

Aquí hay dos soluciones que puedo pensar:

  1. hacer un PUT con todo el recurso. En el lado del servidor, defina la semántica de que un PUT con todo el recurso ignora todos los valores que no han cambiado.

  2. hacer un PUT con un recurso parcial. En el lado del servidor, defina la semántica de esto como una combinación.

2 es solo una optimización de ancho de banda de 1. A veces, 1 es la única opción si el recurso define que algunos campos son obligatorios (piense en búfers de prototipos).

El problema con ambos enfoques es cómo limpiar un campo. Tendrá que definir un valor nulo especial (especialmente para los búfers proto ya que los valores nulos no están definidos para los búfers de protocolo) que causará la eliminación del campo.

¿Comentarios?

Cosas para agregar a su pregunta aumentada. Creo que a menudo puedes perfectamente diseñar acciones comerciales más complicadas. Pero debe regalar el estilo de pensar método / procedimiento y pensar más en recursos y verbos.

envíos de correo

POST /customers/123/mails payload: {from: x@x.com, subject: "foo", to: y@y.com}
POST /customers/123/mails payload: {from: x@x.com, subject: "foo", to: y@y.com} 

La implementación de este recurso + POST enviaría el correo. si es necesario, puede ofrecer algo como / customer / 123 / outbox y luego ofrecer enlaces de recursos a / customer / mails / {mailId}.

cuenta del cliente

Puede manejarlo como un recurso de búsqueda (incluidos los metadatos de búsqueda con paginación e información numérica, que le da el recuento de clientes).

GET /customers response payload: {numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}
GET /customers response payload: {numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....} 

Use PUT para actualizar el recurso incompleto / parcial.

Puede aceptar jObject como parámetro y analizar su valor para actualizar el recurso.

A continuación se muestra la función que puede utilizar como referencia:

 public IHttpActionResult Put(int id, JObject partialObject) { Dictionary dictionaryObject = new Dictionary(); foreach (JProperty property in json.Properties()) { dictionaryObject.Add(property.Name.ToString(), property.Value.ToString()); } int id = Convert.ToInt32(dictionaryObject["id"]); DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]); Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]); //Call function to update resource update(id, startTime, isGroup); return Ok(appointmentModelList); } 

Para modificar el estado, creo que un enfoque RESTful es usar un sub-recurso lógico que describa el estado de los recursos. Esta IMO es bastante útil y limpia cuando tienes un conjunto reducido de estados. Hace que su API sea más expresiva sin forzar las operaciones existentes para los recursos de sus clientes.

Ejemplo:

 POST /customer/active < -- Providing entity in the body a new customer { ... // attributes here except status } 

El servicio POST debe devolver al cliente recién creado con la identificación:

 { id:123, ... // the other fields here } 

El GET para el recurso creado usaría la ubicación del recurso:

 GET /customer/123/active 

Un GET / cliente / 123 / inactive debería devolver 404

Para la operación PUT, sin proporcionar una entidad Json solo actualizará el estado

 PUT /customer/123/inactive < -- Deactivating an existing customer 

Proporcionar una entidad le permitirá actualizar los contenidos del cliente y actualizar el estado al mismo tiempo.

 PUT /customer/123/inactive { ... // entity fields here except id and status } 

Está creando un sub-recurso conceptual para su recurso de cliente. También es consistente con la definición de Roy Fielding de un recurso: "... Un recurso es un mapeo conceptual para un conjunto de entidades, no la entidad que corresponde al mapeo en un punto particular en el tiempo ..." En este caso el el mapeo conceptual está activo-cliente a cliente con estado = ACTIVO.

Operación de lectura:

 GET /customer/123/active GET /customer/123/inactive 

Si realiza esas llamadas una justo después de que la otra devuelva el estado 404, la salida correcta puede no incluir el estado ya que está implícito. Por supuesto, todavía puede usar GET / customer / 123? Status = ACTIVE | INACTIVE para consultar los recursos del cliente directamente.

La operación DELETE es interesante ya que la semántica puede ser confusa. Pero tiene la opción de no publicar esa operación para este recurso conceptual o usarla de acuerdo con su lógica comercial.

 DELETE /customer/123/active 

Eso puede llevar a su cliente a un estado DELETED / DISABLED o al estado opuesto (ACTIVE / INACTIVE).

Consulte http://www.odata.org/

Define el método MERGE, por lo que en tu caso sería algo como esto:

 MERGE /customer/123  DISABLED  

Solo se actualiza la propiedad de status y se conservan los otros valores.

En cuanto a tu actualización.

El concepto de CRUD creo que ha causado cierta confusión con respecto al diseño de API. CRUD es un concepto general de bajo nivel para que las operaciones básicas funcionen sobre datos, y los verbos HTTP son solo métodos de solicitud ( creados hace 21 años ) que pueden o no asignarse a una operación CRUD. De hecho, intente encontrar la presencia del acrónimo CRUD en la especificación HTTP 1.0 / 1.1.

Una guía muy bien explicada que aplica una convención pragmática se puede encontrar en la documentación API de la plataforma en la nube de Google . Describe los conceptos detrás de la creación de una API basada en recursos, que enfatiza una gran cantidad de recursos sobre las operaciones e incluye los casos de uso que está describiendo. Aunque es solo un diseño de convención para su producto, creo que tiene mucho sentido.

El concepto base aquí (y que produce mucha confusión) es el mapeo entre “métodos” y verbos HTTP. Una cosa es definir qué “operaciones” (métodos) hará su API sobre qué tipos de recursos (por ejemplo, obtener una lista de clientes o enviar un correo electrónico) y otro son los verbos HTTP. Debe haber una definición de ambos, los métodos y los verbos que planea usar y un mapeo entre ellos .

También dice que, cuando una operación no se corresponde exactamente con un método estándar ( List , Get , Create , Update , Delete en este caso), se pueden usar “Métodos personalizados”, como BatchGet , que recupera varios objetos en función de varios objetos. entrada de identificación, o SendEmail .

No importa. En términos de REST, no puedes hacer un GET, porque no es almacenable en caché, pero no importa si usas POST o PATCH o PUT o lo que sea, y no importa cómo se ve la URL. Si está haciendo REST, lo que importa es que cuando obtiene una representación de su recurso del servidor, esa representación le puede dar al cliente opciones de transición de estado.

Si su respuesta GET tuvo transiciones de estado, el cliente solo necesita saber cómo leerlas, y el servidor puede cambiarlas si es necesario. Aquí se realiza una actualización utilizando POST, pero si se cambió a PATCH, o si la URL cambia, el cliente todavía sabe cómo hacer una actualización:

 { "customer" : { }, "operations": [ "update" : { "method": "POST", "href": "https://server/customer/123/" }] } 

Puede llegar a enumerar los parámetros necesarios / opcionales para que el cliente se los devuelva. Depende de la aplicación.

En cuanto a las operaciones comerciales, podría tratarse de un recurso diferente vinculado a los recursos del cliente. Si desea enviar un correo electrónico al cliente, tal vez ese servicio es su propio recurso al que PUEDE realizar una transferencia, por lo que puede incluir la siguiente operación en el recurso del cliente:

 "email": { "method": "POST", "href": "http://server/emailservice/send?customer=1234" } 

Algunos buenos videos y ejemplos de la architecture REST del presentador son estos. Stormpath solo usa GET / POST / DELETE, lo cual está bien, ya que REST no tiene nada que ver con las operaciones que usa o cómo deberían verse las URL (excepto que los GET deben ser almacenados en caché):

https://www.youtube.com/watch?v=pspy1H6A3FM ,
https://www.youtube.com/watch?v=5WXYw4J4QOU ,
http://docs.stormpath.com/rest/quickstart/