Diseño de una API REST por URI vs cadena de consulta

73

Digamos que tengo tres recursos que están relacionados así:

Grandparent (collection) -> Parent (collection) -> and Child (collection)

Lo anterior describe la relación entre estos recursos de la siguiente manera: cada abuelo puede mapearse con uno o varios padres. Cada padre puede asignar a uno o varios hijos. Quiero la capacidad de apoyar la búsqueda en el recurso secundario pero con los criterios de filtro:

Si mis clientes me pasan una referencia de identificación a un abuelo, solo quiero buscar contra los niños que son descendientes directos de ese abuelo.

Si mis clientes me pasan una referencia de identificación a un padre, solo quiero buscar contra los hijos que son descendientes directos de mi padre.

He pensado en algo así:

GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

y

GET /myservice/api/v1/parents/{parentID}/children?search={text}

para los requisitos anteriores, respectivamente.

Pero también podría hacer algo como esto:

GET /myservice/api/v1/children?search={text}&grandparentID={id}&parentID=${id}

En este diseño, podría permitir que mi cliente me pase uno u otro en la cadena de consulta: grandparentID o parentID, pero no ambos.

Mis preguntas son:

1) ¿Qué diseño de API es más RESTful y por qué? Semánticamente, significan y se comportan de la misma manera. El último recurso en el URI es "hijos", lo que implica efectivamente que el cliente está operando en el recurso hijos.

2) ¿Cuáles son los pros y los contras de cada uno en términos de comprensibilidad desde la perspectiva del cliente y mantenibilidad desde la perspectiva del diseñador?

3) ¿Para qué se utilizan realmente las cadenas de consulta, además de "filtrar" en su recurso? Si opta por el primer enfoque, el parámetro de filtro se incrusta en el URI como un parámetro de ruta en lugar de un parámetro de cadena de consulta.

¡Gracias!

HiChews123
fuente
3
El título de su pregunta debería ser extremadamente confuso para cualquiera que vea esto. Los segmentos válidos de un URI se definen como <scheme>: // <user>: <password> @ <host>: <port> / <path>; <params>? <query> / # <fragment> (aunque < contraseña> está en desuso) Una "cadena de consulta" es un componente válido de un URI, por lo que su "vs" en el título es una locura.
K. Alan Bates
Qué quiere decir I want to only search against children who are INdirect descendants of that grandparent.? Según su estructura, el abuelo no tiene hijos directos.
nulo
¿Cuál es la diferencia entre un niño y un padre? ¿Es un padre un padre si no tiene hijos? Huele a falla de diseño
Pinoniq
re: potential design flawy si tienes información sobre una persona pero no tienes información sobre sus padres, ¿califican como child? (por ejemplo, Adán y Eva) :)
Jesse Chisholm

Respuestas:

64

primero

Según RFC 3986 §3.4 (Identificadores uniformes de recursos § (Componentes de sintaxis) | Consulta

3.4 Consulta

El componente de consulta contiene datos no jerárquicos que, junto con los datos en el componente de ruta (Sección 3.3), sirven para identificar un recurso dentro del alcance del esquema y la autoridad de denominación del URI (si corresponde).

Los componentes de consulta son para recuperar datos no jerárquicos; ¡Hay pocas cosas más jerárquicas en la naturaleza que un árbol genealógico! Ergo , independientemente de si cree que es "REST-y" o no , para cumplir con los formatos, protocolos y marcos de trabajo y para desarrollar sistemas en Internet, no debe usar la cadena de consulta para identificar esta información.

REST no tiene nada que ver con esta definición.

Antes de abordar sus preguntas específicas, su parámetro de consulta de "búsqueda" está mal nombrado. Mejor sería tratar su segmento de consulta como un diccionario de pares clave-valor.

Su cadena de consulta podría definirse más apropiadamente como

?first_name={firstName}&last_name={lastName}&birth_date={birthDate} etc.

Para responder a sus preguntas específicas

1) ¿Qué diseño de API es más RESTful y por qué? Semánticamente, significan y se comportan de la misma manera. El último recurso en el URI es "hijos", lo que implica efectivamente que el cliente está operando en el recurso hijos.

No creo que esto sea tan claro como parece creer.

Ninguna de estas interfaces de recursos es RESTful. La condición previa principal para el estilo arquitectónico RESTful es que las transiciones de estado de la aplicación deben comunicarse desde el servidor como hipermedia. La gente ha trabajado sobre la estructura de los URI para hacerlos de alguna manera "URI RESTful", pero la literatura formal sobre REST en realidad tiene muy poco que decir al respecto. Mi opinión personal es que gran parte de la metainformación errónea sobre REST se publicó con la intención de romper viejos y malos hábitos. (Construir un sistema verdaderamente "RESTful" es en realidad bastante trabajo. La industria recurrió a "REST" y rellenó algunas preocupaciones ortogonales con calificaciones y restricciones sin sentido).

Lo que dice la literatura REST es que si va a utilizar HTTP como su protocolo de aplicación, debe cumplir con los requisitos formales de las especificaciones del protocolo y no puede "inventar http sobre la marcha y aún declarar que está utilizando http" ; Si va a utilizar URI para identificar sus recursos, debe cumplir con los requisitos formales de las especificaciones sobre URI / URL.

Su pregunta es abordada directamente por RFC3986 §3.4, que he vinculado anteriormente. La conclusión sobre este asunto es que, aunque un URI conforme sea insuficiente para considerar una API "RESTful", si desea que su sistema sea realmente "RESTful" y esté utilizando HTTP y URI, no podrá identificar datos jerárquicos a través de cadena de consulta porque:

3.4 Consulta

El componente de consulta contiene datos no jerárquicos.

...Es tan simple como eso.

2) ¿Cuáles son los pros y los contras de cada uno en términos de comprensibilidad desde la perspectiva del cliente y mantenibilidad desde la perspectiva del diseñador?

Los "pros" de los dos primeros es que están en el camino correcto . La "desventaja" del tercero es que parece estar completamente equivocado.

En lo que respecta a sus problemas de comprensibilidad y mantenibilidad, estos son definitivamente subjetivos y dependen del nivel de comprensión del desarrollador del cliente y de las habilidades de diseño del diseñador. La especificación de URI es la respuesta definitiva en cuanto a cómo se supone que deben formatearse los URI. Se supone que los datos jerárquicos se representan en la ruta y con los parámetros de la ruta. Se supone que los datos no jerárquicos se representan en la consulta. El fragmento es más complicado, porque su semántica depende específicamente del tipo de medios de la representación que se solicita. Por lo tanto, para abordar el componente de "comprensibilidad" de su pregunta, intentaré traducir exactamente lo que en realidad están diciendo sus dos primeros URI. Luego, intentaré representar lo que dices que estás tratando de lograr con URI válidos.

Traducción de sus URI literales a su significado semántico /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} Esto dice para los padres de los abuelos, encontrar a su hijo teniendo search={text} Lo que usted dijo con su URI solo es coherente si busca a los hermanos de un abuelo. Con tus "abuelos, padres, hijos" encontraste que un "abuelo" subía una generación a sus padres y luego volvía a la generación de "abuelos" mirando a los hijos de los padres.

/myservice/api/v1/parents/{parentID}/children?search={text} Esto dice que para el padre identificado por {parentID}, encuentre que su hijo tiene ?search={text}Esto está más cerca de corregir lo que desea y representa una relación padre-> hijo que probablemente se pueda usar para modelar toda su API. Para modelarlo de esta manera, el cliente tiene la responsabilidad de reconocer que si tienen un "grandparentId", existe una capa de indirección entre la ID que tienen y la parte del gráfico familiar que desean ver. Para encontrar un "niño" por "grandparentId", puede llamar a su /parents/{parentID}/childrenservicio y luego, para cada niño que se devuelva, busque su identificador de persona en sus niños.

Implementación de sus requisitos como URI Si desea modelar un identificador de recursos más extensible que pueda recorrer el árbol, puedo pensar en varias formas en que puede lograrlo.

1) El primero, al que ya he aludido. Representar el gráfico de "Personas" como una estructura compuesta. Cada persona tiene una referencia a la generación superior a través de su ruta de Padres y a una generación inferior a través de su ruta de Hijos.

/Persons/Joe/Parents/Mother/Parents sería una forma de agarrar a los abuelos maternos de Joe.

/Persons/Joe/Parents/Parents sería una forma de agarrar a todos los abuelos de Joe.

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} agarraría al abuelo de Joe con el identificador que tiene en la mano.

y todo esto tendría sentido (tenga en cuenta que podría haber una penalización de rendimiento aquí dependiendo de la tarea al forzar un dfs en el servidor debido a la falta de identificación de sucursal en el patrón "Padres / Padres / Padres"). También se beneficia de tener la capacidad de soportar cualquier número arbitrario de generaciones. Si, por alguna razón, desea buscar 8 generaciones, podría representar esto como

/Persons/Joe/Parents/Parents/Parents/Parents/Parents/Parents/Parents/Parents?id={Joe.NotableAncestor}

pero esto lleva a la segunda opción dominante para representar estos datos: a través de un parámetro de ruta.


2) Use parámetros de ruta para "consultar la jerarquía". Podría desarrollar la siguiente estructura para ayudar a aliviar la carga de los consumidores y aún así tener una API que tenga sentido.

Para mirar hacia atrás 147 generaciones, representar este identificador de recursos con parámetros de ruta le permite hacer

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor}

Para localizar a Joe de su bisabuelo, puede mirar hacia abajo en el gráfico un número conocido de generaciones para Joe's Id. /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

Lo más importante con estos enfoques es que sin más información en el identificador y la solicitud, debe esperar que el primer URI esté recuperando una Persona 147 generaciones de Joe con el identificador de Joe.NotableAncestor. Deberías esperar que el segundo recupere a Joe. Suponga que lo que realmente desea es que su cliente llamante pueda recuperar todo el conjunto de nodos y sus relaciones entre la Persona raíz y el contexto final de su URI. Puede hacerlo con el mismo URI (con algo de decoración adicional) y establecer un Aceptar text/vnd.graphvizen su solicitud, que es el tipo de medio registrado de IANA para la .dotrepresentación gráfica. Con eso, cambie el URI a

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor.Id}#directed

con un encabezado de solicitud HTTP Accept: text/vnd.graphviz y puede hacer que los clientes comuniquen con bastante claridad que desean el gráfico dirigido de la jerarquía generacional entre Joe y 147 generaciones anteriores, donde esa 147a generación ancestral contiene una persona identificada como el "Ancestro notable" de Joe.

No estoy seguro de si text / vnd.graphviz tiene alguna semántica predefinida para su fragmento; no pude encontrar ninguna en la búsqueda de instrucciones. Si ese tipo de medio realmente tiene información de fragmento predefinida, entonces se debe seguir su semántica para crear un URI conforme. Pero, si esa semántica no está predefinida, la especificación de URI establece que la semántica del identificador de fragmento no está restringida y, en su lugar, el servidor la define, lo que hace que este uso sea válido.


3) ¿Para qué se utilizan realmente las cadenas de consulta, además de "filtrar" en su recurso? Si opta por el primer enfoque, el parámetro de filtro se incrusta en el URI como un parámetro de ruta en lugar de un parámetro de cadena de consulta.

Creo que ya lo he derrotado completamente, pero las cadenas de consulta no son para "filtrar" recursos. Son para identificar su recurso a partir de datos no jerárquicos. Si ha perforado abajo de su jerarquía con su camino yendo /person/{id}/children/y que está deseando para identificar un determinado niño o un determinado conjunto de los niños, se utilizaría algún atributo que se aplica al conjunto que está identificando e incluirla dentro de la consulta.

K. Alan Bates
fuente
1
El RFC solo se ocupa de la jerarquía en la medida en que define una sintaxis y un algoritmo para resolver referencias de URI relativas. ¿Podría elaborar o citar algunas fuentes que expliquen por qué los ejemplos en la publicación original no son conformes?
user2313838
2
¿No es un árbol genealógico realmente un gráfico, no un árbol, y no es jerárquico? considerando múltiples padres, divorcio y nuevo matrimonio, etc.
Myster
1
@RobertoAloi Me parece contradictorio comunicar su propia interfaz "No se encontraron elementos" a través de un conjunto vacío cuando HTTP ya tiene una definición para eso. El principio básico es que le está pidiendo al servidor que devuelva "cosa (s)" y si no hay "cosa (s)" que devolver, el servidor lo comunica con "404 - No encontrado" ¿Qué tiene de intuitivo eso?
K. Alan Bates
1
Siempre creí 404 para indicar que no se encontró la URL raíz del recurso, es decir, la colección en su conjunto. Entonces, si consultaste / books? Author = Jim y no había libros de jim, recibirías un conjunto vacío []. Pero si usted consultó / artículos? Author = Jim pero el recurso de artículos ni siquiera existía como una colección 404 ayudaría a indicar que no sirve de nada buscar ningún artículo.
adjenks
1
@adjenks No lo aprendiste de las especificaciones formales. Estructuralmente, se puede pensar que los componentes de una url contienen una "raíz" si eso le ayuda a razonar sobre los propósitos de las partes constituyentes, pero en última instancia, la cadena de consulta no es un filtro de visualización contra un recurso identificado a través de la ruta. La cadena de consulta es un ciudadano de primera clase de una url. Cuando no encuentra recursos en el servidor que coincidan con su url (incluida la cadena de consulta) 404 es el medio definido dentro de http para comunicar esto. El modelo de interacción que plantea introduce una distinción sin diferencia.
K. Alan Bates
14

Aquí es donde te equivocas:

Si mis clientes me pasan una referencia de identificación

En un sistema REST, el cliente nunca debe ser molestado con ID. Los únicos identificadores de recursos que el cliente debe conocer deben ser los URI. Este es el principio de "interfaz uniforme".

Piense en cómo los clientes interactuarían con su sistema. Digamos que el usuario comienza a navegar a través de una lista de abuelos, eligió a uno de los hijos de los abuelos, que lo lleva a /grandparent/123. Si el cliente debe poder buscar los hijos de /grandparent/123, entonces, de acuerdo con "HATEOAS", cualquier cosa que se devuelva cuando realice una consulta /grandparent/123debería devolver una URL a la interfaz de búsqueda. Esta URL ya debe tener los datos necesarios para filtrar por el abuelo actual incrustado en ella.

Si el enlace se parece /grandparent/123?search={term}o /parent?grandparent=123&search={term}o /parent?grandparentTerm=someterm&someothergplocator=blah&search={term}son intrascendentes de acuerdo a descansar. Observe cómo todas esas URL tienen el mismo número de parámetros, que es {term}, a pesar de que utilizan diferentes criterios. Puede cambiar entre cualquiera de esas URL o puede mezclarlas dependiendo de los abuelos específicos y el cliente no se rompería, porque la relación lógica entre los recursos es la misma, aunque la implementación subyacente puede diferir significativamente.

Si, en cambio, hubiera creado el servicio de manera tal que requiera /grandparent/{grandparentID}?search={term}cuando vaya en una dirección pero /children?parent={parentID}&search={term}a} cuando vaya en otra dirección, eso es demasiado acoplamiento porque el cliente tendría que saber interpolar diferentes cosas en diferentes relaciones que son conceptualmente similares.

Si realmente va con /grandparent/123?search={term}o /parent?grandparent=123&search={term}es cuestión de gustos y cualquier implementación que sea más fácil para usted en este momento. Lo importante es no requerir que el cliente se modifique si cambia su estrategia de URL o si usa diferentes estrategias en diferentes relaciones padres-hijos.

Lie Ryan
fuente
10

No estoy seguro de por qué la gente piensa que poner los valores de ID en la URL significa que de alguna manera es una API REST, REST se trata de manejar verbos, pasar recursos.

Entonces, si desea PONER a un nuevo usuario, tendría que enviar una buena cantidad de datos y una solicitud POST http es ideal, por lo que aunque podría enviar la clave (por ejemplo, ID de usuario), enviará los datos del usuario (por ejemplo, nombre, dirección) como datos POST.

Ahora es un idioma común poner el identificador de recursos en el URI, pero esto es más convencional que cualquier forma de canónico "no es REST si no está en el URI". Recuerde que la tesis original de REST realmente no menciona http en absoluto, es un modismo para manejar datos en un cliente-servidor, no algo que sea una extensión de http (aunque, obviamente, http es nuestra forma principal de implementar REST) .

Por ejemplo, Fielding utiliza la solicitud de un trabajo académico como ejemplo. Desea recuperar el recurso "Documento del Dr. John sobre el consumo de cervezas", pero también puede desear la versión inicial o la última versión, por lo que el identificador del recurso puede no ser algo que se pueda mencionar fácilmente como una identificación única colocado en la URI. REST permite esto y una intención declarada detrás de esto es:

REST se basa en cambio en que el autor elija un identificador de recurso que mejor se adapte a la naturaleza del concepto que se está identificando

Por lo tanto, no hay nada que le impida usar un URI estático para recuperar a sus padres, pasando un término de búsqueda en la cadena de consulta para identificar al usuario exacto que está buscando. En este caso, el 'recurso' que está identificando es el conjunto de abuelos (y, por lo tanto, el URI contiene 'abuelos' como parte del URI. REST incluye el concepto de 'datos de control' diseñado para determinar qué representación de su se debe recuperar el recurso, el ejemplo dado en estos es el control de la memoria caché, pero también el control de la versión, por lo que mi solicitud del excelente trabajo del Dr. John se puede refinar pasando la versión como datos de control, no como parte del URI.

Creo que un ejemplo de interfaz REST que generalmente no se menciona es SMTP . Al construir un mensaje de correo, envía verbos (FROM, TO, etc.) con los datos de recursos para cada parte del mensaje de correo. Esto es RESTful a pesar de que no usa los verbos http, usa su propio conjunto.

Entonces ... si bien necesita tener alguna identificación de recursos en su URI, no tiene que ser su referencia de identificación. Esto se puede enviar felizmente como datos de control, en la cadena de consulta o incluso en datos POST. Lo que realmente está identificando en su API REST es que está detrás de un niño, que ya tiene en su URI.

Entonces, en mi opinión, al leer la definición de REST, está solicitando un recurso secundario, pasando datos de control (en forma de cadena de consulta) para determinar cuál desea que se devuelva. Como resultado, no puede realizar una solicitud de URI para un abuelo o padre. Desea que se devuelvan los hijos, por lo que el término padre / abuelo o id definitivamente no debe estar en dicho URI. Los niños deberían ser.

gbjbaanb
fuente
En realidad, URI como concepto es central para REST. Sin embargo, lo que no es tan importante es la sintaxis de URI. Una interfaz REST adecuada debe identificar los recursos con una sintaxis común. En el principio más básico de REST, no es necesariamente malo identificar recursos complejos con un objeto JSON en lugar de URI RFC 3986, aunque si lo hace, debe usar JSON RI para toda su API.
Lie Ryan
4

Mucha gente ya ha hablado sobre lo que significa REST, etc., etc. Pero ninguno parece abordar el problema real: su diseño.

¿Por qué el abuelo es diferente de un padre? ambos tienen hijos que posiblemente puedan tener hijos que puedan ...

Finalmente, todos son 'humanos'. Probablemente también tenga eso en su código. Entonces usa eso:

GET /<api-endpoint>/humans/<ID>

Devolverá información útil sobre el humano. (Nombre y cosas)

GET /<api-endpoint>/humans/<ID>/children

obviamente devolverá una gran variedad de niños. Si no existen hijos, una matriz vacía es probablemente el camino a seguir.

Para facilitarlo, por ejemplo, puede agregar una bandera 'hasChildren'. o 'hasGrandChildren'.

Piensa más inteligente, no más difícil

Pinoniq
fuente
1

Lo siguiente es más RESTfull porque cada grandparentID obtiene su propia URL. De esta manera, el recurso se identifica de una manera única.

GET /myservice/api/v1/grandparents/{grandparentID}
GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

La búsqueda de parámetros de consulta es una buena manera de ejecutar una búsqueda desde el contexto de ese recurso.

Cuando una familia se está volviendo muy grande, puede usar inicio / límite como opciones de consulta, por ejemplo:

GET /myservice/api/v1/grandparents/{grandparentID}/children?start=1&limit=50

Como desarrollador, es bueno tener diferentes recursos con una URL / URI única. Creo que debería usar el parámetro de consulta solo cuando también podrían omitirse.

Tal vez esta sea una buena lectura http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling y, de lo contrario, la tesis doctoral original de Roy T Fielding https://www.ics.uci.edu /~fielding/pubs/dissertation/fielding_dissertation.pdf que explica los conceptos muy bien y completo.

l.renkema
fuente
1

Iré con

/myservice/api/v1/people/{personID}/descendants/2?search={text}

En este caso, cada prefijo es un recurso válido:

  • /myservice/api/v1/people: todas las personas
  • /myservice/api/v1/people/{personID}: una persona con información completa (incluidos antepasados, hermanos, descendientes)
  • /myservice/api/v1/people/{personID}/descendants: descendientes de una persona
  • /myservice/api/v1/people/{personID}/descendants/2: nietos de una persona

Puede que no sea la mejor respuesta, pero al menos tiene sentido para mí.

laike9m
fuente
-1

muchos antes de mí ya han señalado que el formato de URL es intrascendente en el contexto del servicio RESTful ... mis dos centavos serían ... un aspecto importante de REST como se propuso en la redacción original fue el concepto de "Recursos". . Cuando diseñe su URL, un aspecto que debe tener en cuenta es que un 'Recurso' no es una fila en una sola tabla (aunque podría serlo) ... más bien es una representación ... eso también es consistente ... . y se puede utilizar para realizar cambios en el estado de esa representación (y efectivamente en el medio subyacente de almacenamiento) ... y un recurso dado solo puede tener sentido en su contexto comercial ... por ejemplo;

En su ejemplo / myservice / api / v1 / grandparents / {grandparentID} / parents / children? Search = {text}

Podría ser un recurso más significativo si acorta esto / myservice / api / v1 / siblingsOfGrandParents? Search = ..

en este caso puedes declarar 'siblingsOfGrandParents' como recurso ... esto simplifica la URL

Como otros señalaron, existe una noción errónea generalizada de que debe ajustarse a cada tipo de relación jerárquica entre objetos de dominio en forma más explícita en una URL ... no hay una regla estricta y rápida para tener una asignación 1 a 1 entre objetos de dominio y recursos y representar todas sus relaciones ... la pregunta debería ser más bien creo ... hay una necesidad de exponer dicha relación a través de URL ... específicamente segmentos de ruta ... tal vez no.

Senthu Sivasambu
fuente
cuando pasa su valioso tiempo degradando una respuesta, sería útil si puede hablar y dar sus razones.
Senthu Sivasambu
No estoy completamente seguro de por qué votar en base a leer su respuesta. Estoy de acuerdo contigo. Algunas cosas inmediatas que me llaman la atención es que usted proporcionó un ejemplo ligeramente tangente al mencionar "hermanos" cuando el OP preguntaba sobre las relaciones jerárquicas. Quizás también se pueda mejorar su estilo de escritura. Usas mucho "...", tendemos a no usar "..." mucho en la escritura formal, profesional o instructiva. También hay un poco de redundancia (por ejemplo, menciona por ejemplo, luego dice en su ejemplo). Quizás su respuesta podría ser más concisa y abordar más directamente la pregunta de los OP.
KennyCason