API CRUD: ¿Cómo especifica qué campos actualizar?

9

Digamos que tiene algún tipo de estructura de datos, que persiste en algún tipo de base de datos. Por simplicidad, llamemos a esta estructura de datos Person. Ahora tiene la tarea de diseñar una API CRUD, que permite a otras aplicaciones crear, leer, actualizar y eliminar correos electrónicos Person. Para simplificar, supongamos que se accede a esta API a través de algún tipo de servicio web.

Para las partes C, R y D de CRUD, el diseño es simple. Usaré la notación funcional similar a C #: la implementación podría ser SOAP, REST / JSON u otra cosa:

class Person {
    string Name;
    DateTime? DateOfBirth;
    ...
}

Identifier CreatePerson(Person);
Person GetPerson(Identifier);
void DeletePerson(Identifier);

¿Qué hay de la actualización? Lo natural sería

void UpdatePerson(Identifier, Person);

pero, ¿cómo especificaría qué campos Personactualizar?


Soluciones que podría aportar:

  • Siempre puede solicitar que se pase una persona completa , es decir, el cliente haría algo como esto para actualizar la fecha de nacimiento:

    p = GetPerson(id);
    p.DateOfBirth = ...;
    UpdatePerson(id, p);
    

    Sin embargo, eso requeriría algún tipo de consistencia transaccional o bloqueo entre Get y Update; de lo contrario, podría sobrescribir algún otro cambio realizado en paralelo por algún otro cliente. Esto haría que la API sea mucho más complicada. Además, es propenso a errores, ya que el siguiente pseudocódigo (asumiendo un idioma de cliente con soporte JSON)

    UpdatePerson(id, { "DateOfBirth": "2015-01-01" });
    

    - que parece correcto - no solo cambiaría DateOfBirth sino que también restablecería todos los demás campos a nulo.

  • Podrías ignorar todos los campos que están null. Sin embargo, ¿cómo haría una diferencia entre no cambiar DateOfBirth y cambiarlo deliberadamente a nulo ?

  • Cambia la firma a void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Cambia la firma a void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Use alguna característica del protocolo de transmisión: por ejemplo, podría ignorar todos los campos que no están contenidos en la representación JSON de la Persona. Sin embargo, eso generalmente requiere analizar el JSON usted mismo y no poder usar las funciones integradas de su biblioteca (por ejemplo, WCF).

Ninguna de las soluciones me parece realmente elegante. Seguramente, este es un problema común, entonces, ¿cuál es la mejor solución utilizada por todos?

Heinzi
fuente
¿Por qué el identificador no es parte de la persona? Para Personinstancias recién creadas que aún no son persistentes, y en el caso de que el identificador se decida como parte del mecanismo de persistencia, simplemente déjelo en nulo. En cuanto a la respuesta, JPA usa un número de versión; si lee la versión 23, cuando actualiza el elemento si la versión en DB es 24, la escritura falla.
SJuan76
Permitir y comunicar ambos PUTy PATCHmétodos. Cuando se usa PATCH, solo se deben reemplazar las teclas de envío, se reemplaza PUTtodo el objeto.
Lode

Respuestas:

8

Si no tiene un seguimiento de los cambios como requisito en este objeto (por ejemplo, "El usuario John cambió el nombre y la fecha de nacimiento"), lo más simple sería anular todo el objeto en DB con uno que reciba del consumidor. Este enfoque implicaría que se envíen un poco más de datos por cable, pero está evitando leer antes de la actualización.

Si tiene un requisito de seguimiento de actividad. Su mundo es mucho más complicado y deberá diseñar cómo almacenar información sobre las acciones CRUD y cómo interceptarlas. Ese es el mundo en el que no quieres sumergirte si no tienes ese requisito.

Según los valores primordiales de transacciones separadas, sugeriría investigar sobre el bloqueo optimista y pesimista . Mitigan este escenario común:

  1. El objeto es leído por el usuario1
  2. El objeto es leído por el usuario2
  3. Objeto escrito por usuario1
  4. Objeto escrito por usuario2 y cambios sobrescritos por usuario1

Cada usuario tiene una transacción diferente, por lo tanto, SQL estándar con esto. Lo más común es el bloqueo optimista (también mencionado por @ SJuan76 en comentarios sobre versiones). Su versión de su registro en DB y durante la escritura primero eche un vistazo a DB si las versiones coinciden. Si las versiones no coinciden, sabe que alguien actualizó el objeto mientras tanto, y debe responder con un mensaje de error al consumidor sobre esta situación. Sí, debe mostrar esta situación al usuario.

Tenga en cuenta que debe leer el registro real de la base de datos antes de escribirlo (para una comparación optimista de la versión de bloqueo), por lo que implementar la lógica delta (escribir solo valores modificados) puede no requerir una consulta de lectura adicional antes de escribir.

Poner en lógica delta depende en gran medida del contrato con el consumidor, pero tenga en cuenta que lo más fácil para el consumidor es construir una carga útil completa en lugar de delta también.

luboskrnac
fuente
2

Tenemos una API PHP en el trabajo. Para actualizaciones si un campo no se envía en el objeto JSON, se establece en NULL. Luego pasa todo al procedimiento almacenado. El procedimiento almacenado intenta actualizar cada campo con field = IFNULL (input, field). Entonces, si solo 1 campo está en el objeto JSON, solo ese campo se actualiza. Para vaciar explícitamente un campo establecido, debemos tener field = '', la base de datos luego actualiza el campo con una cadena vacía o el valor predeterminado de esa columna.

Jared Bernacchi
fuente
3
¿Cómo establece deliberadamente un campo en nulo que aún no es nulo?
Robert Harvey
Todos los campos están configurados como NO NULL, por lo que los campos CHAR obtienen '' y todos los campos enteros obtienen 0.
Jared Bernacchi
1

Especifique la lista de campos actualizados en Cadena de consulta.

PUT /resource/:id?fields=name,address,dob Body { //resource body }

Implemente fusionar datos almacenados con el modelo del cuerpo de la solicitud:

private ResourceModel MergeResourceModel(ResourceModel original, ResourceModel updated, List<string> fields)
{
    var comparer = new FieldComparer();

    foreach (
            var item in
            typeof (ResourceModel).GetProperties()
                    .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof (JsonIgnoreAttribute))))
    {
        if (fields.Contains(item.Name, comparer))
        {
            var property = typeof (ResourceModel).GetProperty(item.Name);
            property.SetValue(original, property.GetValue(updated));
        }
    }

    return original;
}
adisembiring
fuente