Mejores prácticas para actualizaciones parciales en un servicio RESTful

208

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 esto)

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

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

Si la siguiente llamada devuelve el registro completo del cliente para el cliente con la identificación 123

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

¿Cómo debo actualizar el estado?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Actualización : para aumentar 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 CRUDOS 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 
magiconair
fuente
3
Para responder a su pregunta sobre "llamadas de lógica de negocios" aquí hay una publicación sobre el POSTpropio Roy Fielding: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post donde la idea básica es: si no hay Es un método (como GETo PUT) ideal para su uso operativo POST.
rojoca
Esto es más o menos lo que terminé haciendo. Realice llamadas REST para recuperar y actualizar recursos conocidos utilizando GET, PUT, DELETE. POST para agregar nuevos recursos y POST con alguna URL descriptiva para llamadas de lógica de negocios.
magiconair
Independientemente de lo que decida, si esa operación no es parte de la respuesta GET, no tiene un servicio RESTful. No estoy viendo eso aquí
MStodd

Respuestas:

69

Básicamente tienes dos opciones:

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

  2. Úselo POSTpara un recurso secundario y devuelva 303 Vea Otro con el encabezado Ubicación que apunta 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 saber qué recurso fue". POST / 303 está destinado a adiciones iterativas a recursos para construir el estado de algún recurso principal y es perfecto para actualizaciones parciales.

Jan Algermissen
fuente
OK, el POST / 303 tiene sentido para mí. PATCH y MERGE No pude encontrar en la lista de verbos HTTP válidos, por lo que requeriría más pruebas. ¿Cómo construiría un URI si quiero que el sistema envíe un correo electrónico al cliente 123? Algo así como una llamada al método RPC puro que no cambia el estado del objeto en absoluto. ¿Cuál es la manera RESTful de hacer esto?
magiconair
No entiendo la pregunta del URI del correo electrónico. ¿Desea implementar una puerta de enlace en la que pueda PUBLICAR para que envíe un correo electrónico o está buscando mailto: [email protected]?
Jan Algermissen
15
Ni REST ni HTTP tienen nada que ver con CRUD aparte de algunas personas que equiparan los métodos HTTP con CRUD. REST se trata de manipular el estado de los recursos mediante la transferencia de representaciones. Sea lo que sea que quiera lograr, transfiera una representación a un recurso con la semántica adecuada. Tenga cuidado con los términos 'llamadas a métodos puros' o 'lógica de negocios' ya que implican muy fácilmente 'HTTP es para el transporte'. Si necesita enviar un correo electrónico, POST a un recurso de puerta de enlace, si necesita fusionarse en cuentas, crear uno nuevo y POST representaciones de los otros dos, etc.
Jan Algermissen
9
Vea también cómo lo hace Google: googlecode.blogspot.com/2010/03/…
Marius el
44
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{"op": "prueba", "ruta": "/ a / b / c", "valor" : "foo"}, {"op": "eliminar", "ruta": "/ a / b / c"}, {"op": "agregar", "ruta": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "replace", "path": "/ a / b / c", "value": 42}, {"op": "move", "from": "/ a / b / c", "path": "/ a / b / d"}, {"op": "copy", "from": "/ a / b / d "," ruta ":" / a / b / e "}]
intotecho
48

Debe usar POST para actualizaciones parciales.

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

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

En general, las solicitudes GET no deberían 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

wsorenson
fuente
1
@John Saunders POST no necesariamente tiene que crear un nuevo recurso al que se pueda acceder desde un URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson
10
@wsorensen: Sé que no tiene por qué dar como resultado una nueva URL, pero todavía pensé que una POST /customer/123debería crear lo obvio que está lógicamente en el cliente 123. ¿Tal vez un pedido? PUT to /customer/123/statusparece tener más sentido, suponiendo que POST haya /customerscreado implícitamente un status(y suponiendo que sea REST legítimo).
John Saunders
1
@ John Saunders: prácticamente hablando, si queremos actualizar un campo en un recurso ubicado en un URI dado, POST tiene más sentido que PUT, y al no tener una ACTUALIZACIÓN, creo que a menudo se usa en los servicios REST. POST a / clients puede crear un nuevo cliente, y un PUT to / customer / 123 / status puede alinearse mejor con la palabra de la especificación, pero en cuanto a las mejores prácticas, no creo que haya ninguna razón para no POST a / customer / 123 para actualizar un campo: es conciso, tiene sentido y no va estrictamente en contra de nada en la especificación.
wsorenson
8
¿No deberían las solicitudes POST no ser idempotentes? ¿Seguramente actualizar una entrada es idempotente y, por lo tanto, debería ser un PUT?
Martin Andersson
1
Las solicitudes de @MartinAndersson POSTno necesitan ser no idempotentes. Y como se mencionó, PUTdebe reemplazar un recurso completo.
Halle Knast
10

Debe usar PATCH para actualizaciones parciales, ya sea usando documentos json-patch (consulte 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 (consulte http://tools.ietf.org/html/rfc5261 ). Sin embargo, en mi opinión, json-patch es la mejor opción para su tipo de datos comerciales.

PATCH con documentos de parche JSON / XML tiene una semántica muy estricta 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 que falten valores (o, más bien, valores nulos) para representar "ignorar esta propiedad" o "establecer esta propiedad en 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 .

Jørn Wildt
fuente
Tenga en cuenta que, mientras tanto, se han finalizado los RFC para json-patch y xml-patch .
botchniaque
8

Me encuentro con un problema similar. PUT en un recurso secundario parece funcionar cuando desea actualizar un solo 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 por parte del usuario no debe dar lugar a múltiples PUT.

Aquí hay dos soluciones en las 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 fusió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 campos obligatorios (piense en los buffers proto).

El problema con estos dos enfoques es cómo limpiar un campo. Tendrá que definir un valor nulo especial (especialmente para los buffers de proto ya que los valores nulos no están definidos para los buffers de proto) que provocarán la eliminación del campo.

Comentarios?

usuario360657
fuente
2
Esto sería más útil si se publica como una pregunta separada.
intotecho
6

Para modificar el estado, creo que un enfoque RESTful es usar un subsubcurso 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 el recurso de su cliente.

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

A GET / customer / 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 el contenido del cliente y actualizar el estado al mismo tiempo.

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

Está creando un recurso secundario conceptual para el recurso de su cliente. También es consistente con la definición de Roy Fielding de un recurso: "... Un recurso es un mapeo conceptual a 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 es activo de cliente a cliente con estado = ACTIVO.

Operación de lectura:

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

Si realiza esas llamadas una después de que la otra debe devolver el estado 404, la salida exitosa puede no incluir el estado, ya que está implícito. Por supuesto, aún puede usar GET / customer / 123? Status = ACTIVE | INACTIVE para consultar el recurso 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 empresarial.

DELETE /customer/123/active

Ese puede llevar a su cliente a un estado ELIMINADO / DESACTIVADO o al estado opuesto (ACTIVO / INACTIVO).

raspacorp
fuente
¿Cómo se llega al sub recurso?
MStodd
Refactoré la respuesta tratando de hacerlo más claro
raspacorp
5

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

envíos de correo


POST /customers/123/mails

payload:
{from: [email protected], subject: "foo", to: [email protected]}

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

recuento de clientes

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


GET /customers

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

manuel aldana
fuente
Me gusta la forma de agrupación lógica de campos en el recurso POST.
gertas
3

Use PUT para actualizar recursos incompletos / parciales.

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

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

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    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);
}
Puneet Pathak
fuente
2

En cuanto a su 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 se realicen en 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.

Puede encontrar una guía muy bien explicada que aplica una convención pragmática en la documentación de la 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 uno 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 otra 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, Deleteen este caso), se puede utilizar "métodos personalizados", al igual BatchGet, que recupera varios objetos en función de varios de entrada de objeto de identificación, o SendEmail.

atoledo
fuente
2

RFC 7396 : parche de fusión JSON (publicado cuatro años después de la publicación de la pregunta) describe las mejores prácticas para un PARCHE en términos de formato y reglas de procesamiento.

En pocas palabras, envía un PATCH HTTP a un recurso de destino con el tipo de medio MIME application / merge-patch + json y un cuerpo que representa solo las partes que desea cambiar / agregar / eliminar y luego sigue las siguientes reglas de procesamiento.

reglas :

  • Si el parche de fusión proporcionado contiene miembros que no aparecen dentro del objetivo, esos miembros se agregan.

  • Si el objetivo contiene el miembro, el valor se reemplaza.

  • Los valores nulos en el parche de fusión tienen un significado especial para indicar la eliminación de los valores existentes en el destino.

Ejemplos de casos de prueba que ilustran las reglas anteriores (como se ve en el apéndice de ese RFC):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}
Voicu
fuente
1

Echa un vistazo a http://www.odata.org/

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

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Solo statusse actualiza la propiedad y se conservan los demás valores.

Max Toro
fuente
¿Es MERGEun verbo HTTP válido?
John Saunders
3
Mire a PATCH, que pronto será HTTP estándar y hace lo mismo.
Jan Algermissen
@ John Saunders Sí, es un método de extensión.
Max Toro
FYI MERGE se ha eliminado de OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Ver docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k
0

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

Si su respuesta GET tenía 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 mediante POST, pero si se cambió a PATCH, o si la URL cambia, el cliente aún sabe cómo realizar una actualización:

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

Podría ir tan lejos como para enumerar los parámetros obligatorios / opcionales para que el cliente le devuelva. Depende de la aplicación.

En cuanto a las operaciones comerciales, ese podría ser un recurso diferente vinculado al recurso del cliente. Si desea enviar un correo electrónico al cliente, tal vez ese servicio es un recurso propio en el que puede PUBLICAR, 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 arquitectura 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 deben verse las URL (excepto que las GET deben ser almacenables en caché):

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

MStodd
fuente