Projetando uma API REST por URI vs string de consulta


73

Digamos que eu tenho três recursos que estão relacionados assim:

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

A descrição acima mostra a relação entre esses recursos da seguinte maneira: Cada avô pode mapear para um ou vários pais. Cada pai pode mapear para um ou vários filhos. Quero a capacidade de oferecer suporte à pesquisa no recurso filho, mas com os critérios de filtro:

Se meus clientes me fornecerem uma referência de identificação para um avô, quero pesquisar apenas crianças descendentes diretas desse avô.

Se meus clientes me passarem uma referência de identificação para os pais, desejo pesquisar apenas os filhos descendentes diretos dos meus pais.

Eu pensei em algo assim:

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

e

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

para os requisitos acima, respectivamente.

Mas eu também poderia fazer algo assim:

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

Nesse design, eu podia permitir que meu cliente me passasse um ou outro na sequência de consultas: grandparentID ou parentID, mas não os dois.

Minhas perguntas são:

1) Qual design de API é mais RESTful e por quê? Semântica, eles significam e se comportam da mesma maneira. O último recurso no URI é "filhos", implicando efetivamente que o cliente está operando no recurso filho.

2) Quais são os prós e os contras de cada um em termos de compreensão da perspectiva de um cliente e manutenção da perspectiva do designer.

3) Para que as strings de consulta são realmente usadas, além de "filtrar" seu recurso? Se você seguir a primeira abordagem, o parâmetro filter será incorporado no próprio URI como um parâmetro de caminho em vez de um parâmetro de sequência de caracteres de consulta.

Obrigado!


3
O título da sua pergunta deve ser extremamente confuso para quem vê isso. Os segmentos válidos de um URI são definidos como <scheme>: // <user>: <password> @ <host>: <port> / <caminho>; <params>? <query> / # <fragment> (embora < password> está obsoleta) Uma "string de consulta" é um componente válido de um URI, portanto seu "vs" no título é uma loucura.
K. Alan Bates

Você quer dizer I want to only search against children who are INdirect descendants of that grandparent.? De acordo com sua estrutura, o avô não tem filhos diretos.
null

Qual é a diferença entre um filho e um pai? Um pai é pai ou mãe se ele não tem filhos? Cheiros de uma falha de design
Pinoniq

re: potential design flawe se você tiver informações sobre uma pessoa, mas nenhuma informação sobre os pais, elas se qualificam como child? (por exemplo, Adão e Eva) :)
Jesse Chisholm

Respostas:


64

Primeiro

Conforme RFC 3986 §3.4 (identificadores uniformes de recursos § (componentes de sintaxe)) |

3.4 Consulta

O componente de consulta contém dados não hierárquicos que, juntamente com os dados no componente de caminho (Seção 3.3), servem para identificar um recurso dentro do escopo do esquema e da autoridade de nomenclatura do URI (se houver).

Os componentes de consulta são para recuperação de dados não hierárquicos; existem poucas coisas de natureza mais hierárquica do que uma árvore genealógica! Ergo - independentemente de você achar que é "REST-y" ou não - para estar em conformidade com os formatos, protocolos e estruturas de e para o desenvolvimento de sistemas na Internet, você não deve usar a string de consulta para identificar essas informações.

O REST não tem nada a ver com essa definição.

Antes de abordar suas perguntas específicas, seu parâmetro de consulta de "pesquisa" não recebe um nome adequado. Melhor seria tratar seu segmento de consulta como um dicionário de pares de valores-chave.

Sua string de consulta pode ser definida de maneira mais apropriada como

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

Para responder suas perguntas específicas

1) Qual design de API é mais RESTful e por quê? Semântica, eles significam e se comportam da mesma maneira. O último recurso no URI é "filhos", implicando efetivamente que o cliente está operando no recurso filho.

Eu não acho que isso seja tão claro quanto você parece acreditar.

Nenhuma dessas interfaces de recursos é RESTful. A principal pré-condição para o estilo de arquitetura RESTful é que as transições do Estado do Aplicativo devem ser comunicadas do servidor como hipermídia. As pessoas trabalharam na estrutura dos URIs para torná-los de alguma forma "URIs RESTful", mas a literatura formal sobre o REST realmente tem muito pouco a dizer sobre isso. Minha opinião pessoal é que grande parte da meta-desinformação sobre o REST foi publicada com a intenção de quebrar velhos e maus hábitos. (Construir um sistema verdadeiramente "RESTful" é, na verdade, bastante trabalhoso. A indústria procurou o "REST" e preencheu algumas preocupações ortogonais com qualificações e restrições sem sentido.)

O que a literatura do REST diz é que, se você usará o HTTP como protocolo de aplicativo, deverá seguir os requisitos formais das especificações do protocolo e não poderá "inventar o http à medida que avança e ainda declarar que está usando o http" ; Se você usar URIs para identificar seus recursos, deverá seguir os requisitos formais das especificações relacionadas a URI / URLs.

Sua pergunta é direcionada diretamente pelo RFC3986 §3.4, que vinculei acima. A linha de fundo sobre este assunto é que mesmo que um URI conforme é insuficiente para considerar uma API "RESTful", se você quiser que seu sistema realmente ser "RESTful" e você estiver usando HTTP e URIs, então você não pode identificar dados hierárquicos através do string de consulta porque:

3.4 Consulta

O componente de consulta contém dados não hierárquicos

...É simples assim.

2) Quais são os prós e os contras de cada um em termos de compreensão da perspectiva de um cliente e manutenção da perspectiva do designer.

Os "profissionais" dos dois primeiros é que eles estão no caminho certo . Os "contras" do terceiro é que parece estar completamente errado.

No que diz respeito à sua compreensibilidade e capacidade de manutenção, essas são definitivamente subjetivas e dependem do nível de compreensão do desenvolvedor do cliente e dos aspectos de design do designer. A especificação de URI é a resposta definitiva sobre como os URIs devem ser formatados. Dados hierárquicos devem ser representados no caminho e com seus parâmetros. Dados não hierárquicos devem ser representados na consulta. O fragmento é mais complicado, porque sua semântica depende especificamente do tipo de mídia da representação que está sendo solicitada. Portanto, para abordar o componente de "compreensibilidade" da sua pergunta, tentarei traduzir exatamente o que seus dois primeiros URIs estão realmente dizendo. Em seguida, tentarei representar o que você diz que está tentando realizar com URIs válidos.

Tradução dos seus URIs verbatim ao seu significado semântico /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} Isto diz para os pais dos avós, que o filho deles tenha search={text} O que você disse com o seu URI é coerente apenas se procurar pelos irmãos de um avô. Com seus "avós, pais, filhos", você encontrou um "avô" subiu uma geração com os pais e depois voltou à geração "avós" olhando para os filhos dos pais.

/myservice/api/v1/parents/{parentID}/children?search={text} Isso indica que, para o pai identificado por {parentID}, encontre o filho dele. ?search={text}Isso está mais próximo de corrigir o que você deseja e representa um relacionamento pai - filho que provavelmente pode ser usado para modelar toda a sua API. Para modelá-lo dessa maneira, o ônus é imputado ao cliente ao reconhecer que, se ele tiver um "grandparentId", existe uma camada de indireção entre o ID que possui e a parte do gráfico de família que deseja ver. Para encontrar um "filho" por "grandparentId", você pode ligar para o seu /parents/{parentID}/childrenserviço e, em seguida, procurar um filho retornado, procurar o identificador pessoal de seus filhos.

Implementação de seus requisitos como URIs Se você deseja modelar um identificador de recurso mais extensível que possa percorrer a árvore, posso pensar em várias maneiras de conseguir isso.

1) O primeiro, eu já aludi. Represente o gráfico de "Pessoas" como uma estrutura composta. Cada pessoa tem uma referência à geração acima dela através do caminho dos Pais e a uma geração abaixo dela através do caminho dos Filhos.

/Persons/Joe/Parents/Mother/Parents seria uma maneira de pegar os avós maternos de Joe.

/Persons/Joe/Parents/Parents seria uma maneira de pegar todos os avós de Joe.

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} pegaria o avô de Joe com o identificador que você tem em mãos.

e tudo isso faria sentido (observe que pode haver uma penalidade de desempenho aqui, dependendo da tarefa, forçando um dfs no servidor devido à falta de identificação de ramificação no padrão "Pais / Pais / Pais".) Você também se beneficia de ter a capacidade de suportar qualquer número arbitrário de gerações. Se, por algum motivo, você desejar procurar 8 gerações, poderá representar isso como

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

mas isso leva à segunda opção dominante para representar esses dados: através de um parâmetro path.


2) Use parâmetros de caminho para "consultar a hierarquia". Você pode desenvolver a seguinte estrutura para ajudar a aliviar a carga sobre os consumidores e ainda ter uma API que faça sentido.

Olhando para trás 147 gerações, representar esse identificador de recurso com parâmetros de caminho permite que você faça

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

Para localizar Joe de seu bisavô, você pode procurar no gráfico um número conhecido de gerações pela identificação de Joe. /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

O ponto principal a ser observado nessas abordagens é que, sem mais informações no identificador e na solicitação, você deve esperar que o primeiro URI recupere uma Pessoa 147 gerações acima de Joe com o identificador de Joe.NotableAncestor. Você deve esperar que o segundo recupere Joe. Suponha que o que você realmente deseja é que o cliente que está chamando possa recuperar todo o conjunto de nós e seus relacionamentos entre a Pessoa raiz e o contexto final do seu URI. Você pode fazer isso com o mesmo URI (com alguma decoração adicional) e definindo um Accept of text/vnd.graphvizem sua solicitação, que é o tipo de mídia registrado pela IANA para a .dotrepresentação gráfica. Com isso, altere o URI para

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

com um cabeçalho de solicitação HTTP Accept: text/vnd.graphviz e é possível que os clientes comuniquem claramente que desejam o gráfico direcionado da hierarquia geracional entre Joe e 147 gerações anteriores, quando essa 147ª geração ancestral contiver uma pessoa identificada como "Antepassado Notável" de Joe.

Não tenho certeza se text / vnd.graphviz tem alguma semântica predefinida para seu fragmento; não encontrei nenhuma em busca de instruções. Se esse tipo de mídia realmente tiver informações de fragmentos predefinidas, sua semântica deve ser seguida para criar um URI em conformidade. Porém, se essas semânticas não forem predefinidas, a especificação do URI indicará que a semântica do identificador de fragmentos é irrestrita e, em vez disso, é definida pelo servidor, tornando esse uso válido.


3) Para que as strings de consulta são realmente usadas, além de "filtrar" seu recurso? Se você seguir a primeira abordagem, o parâmetro filter será incorporado no próprio URI como um parâmetro de caminho em vez de um parâmetro de sequência de caracteres de consulta.

Acredito que já superei isso completamente, mas as cadeias de consulta não servem para "filtrar" recursos. Eles são para identificar seu recurso a partir de dados não hierárquicos. Se você detalhou sua hierarquia com seu caminho /person/{id}/children/e deseja identificar um filho específico ou um conjunto específico de filhos, você usaria algum atributo que se aplica ao conjunto que está identificando e o incluiria na consulta.


11
O RFC está preocupado apenas com a hierarquia, na medida em que define uma sintaxe e um algoritmo para resolver as referências relativas de URI. Você poderia elaborar ou citar algumas fontes que explicam por que os exemplos na postagem original não estão em conformidade?
user2313838

2
Uma árvore genealógica não é realmente um gráfico, não é uma árvore e não é hierárquica? considerando múltiplos pais, divórcio e re-casamento etc.
Myster

11
@RobertoAloi Parece-me pouco intuitivo comunicar sua própria interface "Nenhum item encontrado" através de um conjunto vazio quando o HTTP já tem uma definição para isso. O princípio básico é que você está pedindo ao servidor para retornar "coisa (s)" e se não houver "coisa (s)" para retornar, o servidor comunica isso com "404 - Não encontrado". O que é contra-intuitivo?
K. Alan Bates

11
Eu sempre acreditei que 404 indicava que o URL raiz do recurso não foi encontrado, ou seja, a coleção como um todo. Então, se você consultasse / books? Author = Jim e não houvesse livros de jim, você receberia um conjunto vazio []. Mas se você consultou / articles? Author = Jim, mas o recurso articles nem sequer existia como uma coleção 404, ajudaria a indicar que não adianta procurar nenhum artigo.
adjenks

11
@adjenks Você não aprendeu isso com especificações formais. Estruturalmente, pode-se considerar que os constituintes de uma URL contêm uma "raiz" se isso o ajudar a raciocinar sobre os propósitos das partes constituintes, mas, em última análise, a cadeia de consulta não é um filtro de exibição para um recurso identificado pelo caminho. A string de consulta é um cidadão de primeira classe de um URL. Quando você não encontra recursos no servidor que correspondam ao seu URL (incluindo a string de consulta), 404 é o meio definido no http para comunicar isso. O modelo de interação que você apresenta introduz uma distinção sem diferença.
K. Alan Bates

14

É aqui que você entende errado:

Se meus clientes me passarem uma referência de identificação

Em sistemas REST, o cliente nunca deve ser incomodado com IDs. Os únicos identificadores de recursos que o cliente deve conhecer devem ser URIs. Este é o princípio da "interface uniforme".

Pense em como os clientes interagem com seu sistema. Digamos que o usuário comece a navegar por uma lista de avós, ele escolheu um dos filhos dos avós, que o leva a ele /grandparent/123. Se o cliente conseguir pesquisar os filhos de /grandparent/123, de acordo com "HATEOAS", o que for retornado ao fazer uma consulta /grandparent/123deve retornar um URL para a interface de pesquisa. Esse URL já deve ter os dados necessários para filtrar pelo avô atual incorporado nele.

Se o link se parece com /grandparent/123?search={term}ou /parent?grandparent=123&search={term}ou /parent?grandparentTerm=someterm&someothergplocator=blah&search={term}são irrelevantes acordo com REST. Observe como todos esses URLs têm o mesmo número de parâmetros, ou seja {term}, mesmo que eles usem critérios diferentes. Você pode alternar entre qualquer um desses URLs ou misturá-los, dependendo dos avós específicos e o cliente não quebraria, porque o relacionamento lógico entre os recursos é o mesmo, embora a implementação subjacente possa diferir significativamente.

Se, em vez disso, você tivesse criado o serviço de tal forma que ele requer /grandparent/{grandparentID}?search={term}quando você segue um caminho, mas /children?parent={parentID}&search={term}a} quando segue outro caminho, isso é muito acoplamento, porque o cliente precisaria saber para interpolar coisas diferentes em diferentes relações que são conceitualmente semelhantes.

Se você realmente acompanha /grandparent/123?search={term}ou /parent?grandparent=123&search={term}é uma questão de gosto e qual a implementação mais fácil para você agora. O importante é não exigir que o cliente seja modificado se você alterar sua estratégia de URL ou se usar estratégias diferentes em diferentes relações entre pais e filhos.


10

Não sei por que as pessoas pensam que colocar os valores de ID na URL significa que, de alguma forma, é uma API REST, REST trata de lidar com verbos, passando recursos.

Portanto, se você quiser COLOCAR um novo usuário, terá que enviar uma boa quantidade de dados e uma solicitação HTTP POST é ideal; portanto, embora você possa enviar a chave (por exemplo, ID do usuário), você enviará os dados do usuário (por exemplo, nome, endereço) como dados do POST.

Agora, é um idioma comum colocar o identificador de recurso no URI, mas isso é mais convencional do que qualquer forma de canônico "não é REST se não estiver no URI". Lembre-se de que a tese original do REST realmente não menciona http, é um idioma para manipular dados em um cliente-servidor, não algo que seja uma extensão do http (embora, obviamente, http seja nossa forma principal de implementar o REST) .

Por exemplo, Fielding usa a solicitação de um artigo acadêmico como exemplo. Você deseja recuperar o recurso "Artigo do Dr. John sobre o consumo de cerveja", mas também pode querer a versão inicial ou a versão mais recente, para que o identificador de recurso não seja algo que seja facilmente referenciado como um único ID que possa ser colocado no URI. O REST permite isso e uma intenção declarada por trás disso é:

O REST depende, em vez disso, do autor escolher um identificador de recurso que melhor se ajusta à natureza do conceito que está sendo identificado

Portanto, não há nada que o impeça de usar um URI estático para recuperar seus pais, passando um termo de pesquisa na string de consulta para identificar o usuário exato que você procura. Nesse caso, o 'recurso' que você está identificando é o conjunto de avós (portanto, o URI contém 'avós' como parte do URI. O REST inclui o conceito de 'dados de controle' projetado para determinar qual representação de seu O recurso deve ser recuperado - o exemplo dado a estes é o controle de cache, mas também o controle de versão - para que minha solicitação do excelente artigo do Dr. John possa ser refinada passando a versão como dados de controle, não como parte do URI.

Eu acho que um exemplo de interface REST que geralmente não é mencionado é SMTP . Ao construir uma mensagem de email, você envia verbos (FROM, TO etc) com os dados do recurso para cada parte da mensagem. Isso é RESTful, mesmo que não use os verbos http, ele usa seu próprio conjunto.

Então ... embora você precise ter alguma identificação de recurso em seu URI, ele não precisa ser sua referência de ID. Felizmente, isso pode ser enviado como dados de controle, na string de consulta ou mesmo nos dados do POST. O que você realmente está identificando na API REST é que está atrás de um filho, que você já possui em seu URI.

Então, na minha opinião, ao ler a definição de REST, você está solicitando um recurso filho, passando dados de controle (na forma de string de consulta) para determinar qual você deseja retornar. Como resultado, você não pode fazer uma solicitação de URI para um avô ou pai. Você deseja que os filhos sejam devolvidos, para que o termo pai / avô ou id definitivamente não deva estar em um URI. As crianças devem ser.


Na verdade, o URI como conceito é central para o REST. O que não é tão importante, porém, é a sintaxe do URI. Uma interface REST adequada deve identificar recursos com uma sintaxe comum. No princípio mais básico do REST, não é necessariamente ruim identificar recursos complexos com um objeto JSON em vez do URI RFC 3986, embora, se você fizer isso, use o JSON RI para toda a API.
Lie Ryan

4

Muitas pessoas já falaram sobre o que significa REST, etc. etc. Mas nenhuma parece abordar o problema real: seu design.

Por que os avós são diferentes de um pai? ambos têm filhos que podem ter filhos que podem ...

Eventualmente, eles são todos 'humanos'. Você provavelmente também tem isso no seu código. Então use isso:

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

Retornará algumas informações úteis sobre o ser humano. (Nome e outras coisas)

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

obviamente retornará uma matriz de filhos. Se não houver filhos, provavelmente uma matriz vazia é o caminho a percorrer.

Para facilitar, você pode, por exemplo, adicionar uma bandeira 'hasChildren'. ou 'hasGrandChildren'.

Pense mais inteligente, não mais difícil


1

O seguinte é mais RESTfull porque cada grandparentID obtém seu próprio URL. Dessa forma, o recurso é identificado de uma maneira única.

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

A pesquisa de parâmetros de consulta é uma boa maneira de executar uma pesquisa a partir do contexto desse recurso.

Quando uma família está ficando muito grande, você pode usar iniciar / limitar como opções de consulta, por exemplo:

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

Como desenvolvedor, é bom ter recursos diferentes com um URL / URI exclusivo. Eu acho que você deve usar o parâmetro de consulta apenas quando eles também podem ser deixados de fora.

Talvez seja uma boa leitura http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling e, caso contrário, a tese original de doutorado de Roy T Fielding https://www.ics.uci.edu /~fielding/pubs/dissertation/fielding_dissertation.pdf que explica os conceitos muito bem e completos.


1

Eu vou com

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

Nesse caso, todo prefixo é um recurso válido:

  • /myservice/api/v1/people: todas as pessoas
  • /myservice/api/v1/people/{personID}: uma pessoa com informações completas (incluindo ancestrais, irmãos, descendentes)
  • /myservice/api/v1/people/{personID}/descendants: descendentes de uma pessoa
  • /myservice/api/v1/people/{personID}/descendants/2: netos de uma pessoa

Pode não ser a melhor resposta, mas pelo menos faz sentido para mim.


-1

muitos antes de mim já apontaram que o formato da URL é irrelevante no contexto do serviço RESTful ... meus dois centavos seriam ... um aspecto importante do REST, conforme preconizado na redação original, era o conceito de "Recursos". .quando você cria seu URL, um aspecto que você deve ter em mente é que um 'Recurso' não é uma linha em uma única tabela (embora possa ser). .e pode ser usado para fazer alterações no estado dessa representação (e efetivamente no subjacente meio de armazenamento) .. e um determinado recurso pode ser significativo apenas no seu contexto de negócios .. por exemplo;

No seu exemplo / myservice / api / v1 / grandparents / {grandparentID} / pais / filhos? Search = {text}

Poderia ser um recurso mais significativo se você reduzir este / myservice / api / v1 / siblingsOfGrandParents? Search = ..

nesse caso, você pode declarar 'siblingsOfGrandParents' como recurso .. isso simplifica a URL

Como outros apontaram, existe uma noção equivocada generalizada de que você precisa se encaixar em todos os tipos de relacionamento hierárquico entre objetos de domínios de forma mais explícita em um URL. recursos e representam todos os seus relacionamentos ... a questão deveria ser, creio eu ... existe a necessidade de expor esse relacionamento via URLs ... especificamente segmentos de caminho ... talvez não.


quando você gasta seu precioso tempo desclassificando uma resposta, seria útil se você pudesse se manifestar e apresentar seus motivos.
Senthu Sivasambu

Não sei ao certo por que a votação foi reduzida com base na leitura da sua resposta. Eu concordo com você. Algumas coisas imediatas que me interessam é que você forneceu um exemplo ligeiramente tangente ao mencionar "irmãos" quando o OP estava perguntando sobre relacionamentos hierárquicos. Talvez também o seu estilo de escrita possa ser melhorado. Você usa muito "...", nós tendemos a não usar muito "..." na escrita formal, profissional ou instrucional. Também há um pouco de redundância (por exemplo, você menciona, por exemplo, e depois diz no seu exemplo). Talvez sua resposta possa ser mais concisa e abordar mais diretamente a questão dos POs.
KennyCason
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.