通过URI与查询字符串设计REST API


73

假设我有三个相关的资源,如下所示:

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

上面这样描述了这些资源之间的关系:每个祖父母可以映射到一个或几个父母。每个父母可以映射到一个或几个孩子。我希望能够支持针对子资源的搜索,但具有过滤条件:

如果我的客户给我传递了一个祖父母的ID参考,那么我只想搜索是该祖父母的直接后代的孩子。

如果我的客户向我传递了对父母的ID引用,我只想搜索父母直接后代的孩子。

我想到过这样的事情:

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

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

分别满足上述要求。

但是我也可以这样做:

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

在这种设计中,我可以允许我的客户端向我传递查询字符串中的一个或另一个:grandparentID或parentID,但不能两者都传递。

我的问题是:

1)哪种API设计更RESTful,为什么?在语义上,它们的含义和行为方式相同。URI中的最后一个资源是“子代”,有效地暗示客户端正在子代资源上运行。

2)从客户的角度看可理解性以及从设计者的角度看可维护性方面,每种方法的优缺点是什么。

3)除了对资源进行“过滤”外,查询字符串的真正用途是什么?如果采用第一种方法,则将filter参数作为路径参数而不是查询字符串参数嵌入到URI本身中。

谢谢!


3
您的问题的标题对浏览此文件的任何人都应该非常困惑。URI的有效段定义为<scheme>:// <user>:<password> @ <host>:<port> / <path>; <params>?<query> /#<fragment>(尽管<不建议使用密码>)“查询字符串”是URI的有效组成部分,因此标题中的“ vs”简直是疯了。
K. Alan Bates 2015年

你是说I want to only search against children who are INdirect descendants of that grandparent.吗 根据您的结构,祖父母或外祖父母没有直子。
空值

孩子和父母之间的区别是什么?如果父母没有孩子,父母是父母吗?设计错误的气味
Pinoniq 2015年

回复:potential design flaw如果您有关于某人的信息,但没有有关其父母的信息,那么他们是否有资格child?(例如,亚当和夏娃):)
杰西·奇斯霍尔姆

Answers:


64

第一

根据RFC 3986§3.4(统一资源标识符§(语法组件)|查询

3.4查询

查询组件包含非分层数据,以及路径组件中的数据(第3.3节),用于标识URI方案和命名权限(如果有)范围内的资源。

查询组件用于检索非分层数据;在自然界中,没有什么比家谱更有层次感了!因此-不管您是否认为它是“ REST-y”-为了符合Internet上的系统的格式,协议和框架并用于开发Internet上的系统,不得使用查询字符串来标识此信息。

REST与该定义无关。

在解决您的特定问题之前,您对“搜索”的查询参数的命名不正确。最好将您的查询段视为键值对字典。

您的查询字符串可以更恰当地定义为

?first_name={firstName}&last_name={lastName}&birth_date={birthDate} 等等

回答您的特定问题

1)哪种API设计更RESTful,为什么?在语义上,它们的含义和行为方式相同。URI中的最后一个资源是“子代”,有效地暗示客户端正在子代资源上运行。

我认为这并不像您认为的那么明确。

这些资源接口都不是RESTful的。RESTful体系结构样式的主要前提是必须从服务器以超媒体的形式传递应用程序状态转换。人们已经在URI的结构上进行了努力,以使其以某种方式成为“ RESTful URI”,但是有关REST的正式文献实际上对此没有什么可说的。我个人的观点是,有关REST的许多元错误信息是为了打破旧的不良习惯而发布的。(构建一个真正的“ RESTful”系统实际上是一项相当大的工作。该行业陷入了“ REST”的困境,并以毫无意义的条件和限制回填了一些正交的问题。)

REST文献的意思是,如果要使用HTTP作为应用程序协议,则必须遵守该协议规范的形式要求,并且不能“随手编写HTTP并仍然声明您正在使用http”。 ; 如果要使用URI标识资源,则必须遵守有关URI / URL规范的形式要求。

您的问题已由我上面链接的RFC3986§3.4直接解决。关于此问题的底线是,即使符合标准的URI不足以认为API是“ RESTful”,但如果您希望系统实际上 “ RESTful”并且使用的是HTTP和URI,那么您将无法通过查询字符串,因为:

3.4查询

查询组件包含非分层数据

...就这么简单。

2)从客户的角度看可理解性以及从设计者的角度看可维护性方面,每种方法的优缺点是什么。

前两个的“优点”是他们走在正确的道路上。第三点的“缺点”似乎完全是错误的。

就您的可理解性和可维护性而言,这些绝对是主观的,并且取决于客户开发人员的理解水平和设计人员的设计思想。URI规范是有关应该如何格式化URI的明确答案。层次数据应该在路径上并使用路径参数表示。非分层数据应该在查询中表示。该片段更加复杂,因为其语义特别取决于所请求表示形式的媒体类型。因此,为了解决您问题的“可理解性”部分,我将尝试准确翻译您的前两个URI实际所说的内容。然后,我将尝试代表您说的您要使用有效URI完成的内容。

将逐字URI转换为其语义的含义 /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} 这对于祖父母的父母来说,发现他们的孩子拥有search={text} URI所说的内容只有在搜索祖父母的兄弟姐妹时才是连贯的。与您的“祖父母,父母,子女”一起,您会发现“祖父母”在父母的世代中长大,然后再通过查看父母的子女回到“祖父母”世代。

/myservice/api/v1/parents/{parentID}/children?search={text} 这就是说,对于由{parentID}标识的父级,找到其子级具有?search={text}更接近于您想要的子级的子级,并表示可能用于建模整个API的父级>子级关系。要以这种方式进行建模,将负担加在客户端上,以使他们认识到,如果他们具有“ grandparentId”,则他们拥有的ID与希望看到的族图的一部分之间会有一个间接层。要通过“ grandparentId”查找“孩子”,您可以调用/parents/{parentID}/children服务,然后为返回的每个孩子,在其孩子中搜索您的人标识符。

将需求实现为URI 如果要对可以遍历树的更具扩展性的资源标识符进行建模,我可以想到几种实现方法。

1)第一个,我已经提到过。将“人物”图表示为复合结构。每个人都通过其“父母”路径提及其上方的一代,并通过其“子女”路径对其下方的一代进行引用。

/Persons/Joe/Parents/Mother/Parents 将是抓住乔的外祖父母的一种方式。

/Persons/Joe/Parents/Parents 将是抓住乔所有祖父母的一种方法。

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} 会抓住拥有您的标识符的乔的祖父母的身份。

并且所有这些都是有道理的(请注意,由于“父母/父母/父母/父母”模式中缺少分支标识,因此在服务器上强制使用dfs可能会因性能而导致性能下降。)您还可以受益于拥有支持任意数量的世代的能力。如果出于某种原因,您希望查找8代,则可以将其表示为

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

但这导致通过路径参数来表示此数据的第二个主要选择。


2)使用路径参数“查询层次结构”您可以开发以下结构来帮助减轻使用者的负担,并且仍然具有有意义的API。

要回顾147代,用路径参数表示此资源标识符,您可以

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

要从他的曾祖父母那里找到乔,您可以在图表上向下看乔的ID的已知代数。 /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

这些方法的主要注意事项是,在标识符和请求中没有更多信息时,您应该期望第一个URI正在使用Joe.NotableAncestor标识符从Joe检索一个147代的Person。您应该期望第二个检索到Joe。假设您真正想要的是呼叫客户端能够检索整个节点集及其在根Person和URI最终上下文之间的关系。您可以使用相同的URI(带有一些其他修饰)并text/vnd.graphviz在您的请求中设置“接受” 来完成此操作,这是IANA注册的.dot图形表示形式的媒体类型。这样,将URI更改为

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

使用HTTP请求标头 Accept: text/vnd.graphviz ,您可以让客户很清楚地传达信息,即他们希望Joe和147世代之间的世代层次结构的有向图,在此之前的第147世代包含一个被标识为Joe的“著名祖先”的人。

我不确定text / vnd.graphviz的片段是否具有任何预定义的语义;在搜索指令时找不到任何语义。如果该媒体类型确实确实具有预定义的片段信息,则应遵循其语义来创建一致的URI。但是,如果未预先定义这些语义,则URI规范会声明片段标识符的语义不受限制,而是由服务器定义,从而使此用法有效。


3)除了对资源进行“过滤”外,查询字符串的真正用途是什么?如果采用第一种方法,则将filter参数作为路径参数而不是查询字符串参数嵌入到URI本身中。

我相信我已经彻底击败了这一点,但是查询字符串不是用于“过滤”资源的。它们用于从非分层数据中识别您的资源。如果您已准备向下钻取层次结构与您的路径 /person/{id}/children/,并且您希望确定一个特定的孩子或特定组的孩子,你可以使用适用于您的识别一组某属性,包括它的查询中。


1
RFC仅涉及层次结构,因为它定义了用于解析相对URI引用的语法和算法。您能否详细说明或引用一些资料来解释原始帖子中的示例为何不一致?
user2313838

2
族谱不是图,不是树,也根本不是层次图。考虑多名父母,离婚和再婚等
Myster

1
@RobertoAloi当HTTP已经有一个定义时,通过空集传达您自己的“未找到项目”接口对我来说似乎违反直觉。基本原则是,您要服务器返回“事物”,并且如果没有“事物”要返回,则服务器将其与“ 404-Not Found”进行通信,这有什么反常理?
K. Alan Bates

1
我一直相信404表示未找到资源的根URL,即整个集合。因此,如果您查询/ books?author = Jim并且没有jim的书,那么您会收到一个空集[]。但是,如果您查询/ articles?author = Jim,但文章资源甚至不存在,因为集合404会帮助您指出寻找任何文章根本没有用。
adjenks '18

1
@adjenks您没有从正式规范中学到这一点。从结构上讲,如果可以帮助您推断组成部分的用途,则可以将url的组成部分视为包含“根”,但最终查询字符串不是针对通过路径标识的资源的显示过滤器。查询字符串是网址的一等公民。当您在服务器上找不到与您的URL(包括查询字符串)匹配的资源时,404就是在http中定义的用于传达此信息的手段。您构成的交互模型引入了差异而没有差异。
K. Alan Bates

14

这是您弄错了的地方:

如果我的客户通过我的ID参考

在REST系统中,客户端永远不会被ID困扰。客户端应该知道的唯一资源标识符应该是URI。这就是“统一接口”的原理。

考虑客户端如何与您的系统交互。假设用户开始浏览祖父母列表,他选择了一个祖父母的孩子,将他带到/grandparent/123。如果客户端应该能够搜索的子级/grandparent/123,则根据“ HATEOAS”,在执行查询时/grandparent/123返回的任何内容都应将URL返回到搜索界面。此URL应该已经具有需要用来嵌入当前祖父母项过滤的任何数据。

无论链接看起来像/grandparent/123?search={term}/parent?grandparent=123&search={term}/parent?grandparentTerm=someterm&someothergplocator=blah&search={term}根据其他都是无关紧要的。请注意{term},尽管所有这些URL 使用不同的条件,但它们具有相同数量的参数,即。您可以在这些URL中的任何一个之间切换,也可以根据特定的祖父母来混合使用它们,并且客户端不会中断,因为即使底层实现可能有很大的不同,资源之间的逻辑关系也是相同的。

如果您创建的服务相反,/grandparent/{grandparentID}?search={term}当您/children?parent={parentID}&search={term}沿一种方式需要它,而沿另一种方式却需要a}时,这将导致过多的耦合,因为客户必须知道要在概念上相似的不同关系上插值不同的事物。

无论您是真正选择/grandparent/123?search={term}还是选择/parent?grandparent=123&search={term}都取决于品味,无论哪种实现方式现在对您来说都更容易。重要的是,如果您更改URL策略或对不同的父子关系使用不同的策略,则无需修改客户端。


10

我不确定为什么人们会认为将ID值放入URL意味着REST API,REST是关于处理动词,传递资源的。

因此,如果您想放置新用户,则必须发送大量数据,而POST http请求是理想的选择,因此尽管您可以发送密钥(例如,用户ID),但仍将发送用户数据(例如名称,地址)作为POST数据。

现在,将资源标识符放在URI中是一种常见的习惯用法,但这比任何形式的规范化规范(“如果不在URI中,则不是REST”)更具惯例。请记住,REST 的原始论点并没有真正提到http,它是在客户端服务器中处理数据的一种习语,而不是http的扩展(尽管,显然,http是实现REST的主要形式) 。

例如,菲尔丁以学术论文的要求为例。您想要检索“喝啤酒的约翰博士的论文”资源,但是您可能还想要初始版本或最新版本,因此资源标识符可能不是一个可以轻易引用为单个ID的东西。放在URI中。REST允许这样做,其背后的明确意图是:

REST而是依靠作者选择最适合所识别概念性质的资源标识符

因此,没有什么可以阻止您使用静态URI来检索您的父母,而是在查询字符串中传入搜索字词来标识您所要寻找的确切用户。在这种情况下,您标识的“资源”是一组祖父母(因此URI包含“祖父母”作为URI的一部分。REST包含“控制数据”的概念,该概念旨在确定您的哪种表示形式)资源将被检索-这些示例中给出的示例是缓存控制,还有版本控制-因此,我可以通过将版本作为控制数据而不是URI的一部分来细化对John博士出色论文的要求。

我认为SMTP接口通常不被提及。构造邮件消息时,您发送动词(FROM,TO等)以及邮件消息各部分的资源数据。即使它不使用http动词,它也是RESTful的,它使用自己的集合。

所以...虽然您确实需要在URI中有一些资源标识,但它不一定是您的id引用。可以愉快地将其作为控制数据,查询字符串甚至是POST数据发送。您在REST API中真正识别出的是您正在生一个孩子,而您已经在URI中了。

因此,在我看来,阅读REST的定义时,您是在请求子资源,并传入控制数据(以querystring的形式)以确定要返回的资源。结果,您无法为祖父母或父母提出URI请求。您希望孩子返回,因此术语“父母/祖父母”或“ id”绝对不应使用此类URI。孩子应该。


实际上,URI作为概念是REST的核心。URI语法并不重要。正确的REST接口必须使用通用语法标识资源。在REST原则的基础上,使用JSON对象而不是RFC 3986 URI标识复杂的资源并不一定很糟糕,尽管如果这样做,则应对整个API使用JSON RI。
Lie Ryan

4

很多人已经在谈论REST的含义等了,但是似乎没有一个解决真正的问题的:您的设计。

为什么祖父母与父亲不同?他们俩都有孩子,可能会有孩子可以...

最终,它们都是“人类”。您的代码中也可能包含该代码。因此使用:

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

将返回一些有关人类的有用信息。(名字和东西)

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

显然会返回一系列孩子。如果不存在任何子代,则可能要使用空数组。

为了简单起见,您可以添加一个标志“ hasChildren”。或“ hasGrandChildren”。

更聪明地思考不难


1

以下是更完整的RESTfull,因为每个grandparentID都有自己的URL。这样,资源就以独特的方式被识别。

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

查询参数搜索是从该资源的上下文执行搜索的好方法。

当一个家庭越来越大时,您可以使用开始/限制作为查询选项,例如:

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

作为开发人员,拥有唯一URL / URI的不同资源是一件好事。我认为您仅应在查询参数也可能被忽略时才使用查询参数。

也许这是一本不错的书,http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling和Roy T Fielding的原始博士学位论文https://www.ics.uci.edu /~fielding/pubs/dissertation/fielding_dissertation.pdf很好地解释了这些概念。


1

我去

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

在这种情况下,每个前缀都是有效资源:

  • /myservice/api/v1/people: 所有人
  • /myservice/api/v1/people/{personID}:一个人,具有完整的信息(包括祖先,兄弟姐妹,后代)
  • /myservice/api/v1/people/{personID}/descendants:一个人的后代
  • /myservice/api/v1/people/{personID}/descendants/2:一个人的孙子

可能不是最佳答案,但至少对我来说有意义。


-1

在我之前的许多人已经指出,URL格式在RESTful服务的上下文中是无关紧要的。我的两分钱将是...最初撰写中提倡的REST的一个重要方面是“资源”的概念。 。当您设计URL时,您可能需要牢记的一个方面是,“资源”不是单个表中的一行(尽管可以)。而是一种表示形式。可以用来更改该表示形式的状态(并有效地更改存储的底层介质)。给定资源仅在您的业务环境中有意义。

在您的示例中/ myservice / api / v1 / grandparents / {grandparentID} / parents / children?search = {text}

如果您缩短/ myservice / api / v1 / siblingsOfGrandParents?search =。,可能会成为更有意义的资源。

在这种情况下,您可以将“ siblingsOfGrandParents”声明为资源。这简化了URL

正如其他人指出的那样,普遍存在一种误导性的观念,即您需要以URL中更明确的形式适应域对象之间的每种类型的层次关系。在域对象与资源并表示它们之间的所有关系...我宁愿相信的问题是...是否需要通过URL公开这种关系...特别是路径段...也许没有。


当您将宝贵的时间花在降级答案上时,如果您可以说出来并说出自己的理由,将会很有帮助。
Senthu Sivasambu

我不完全确定为什么要根据阅读的答案而投票否决。我同意你的看法。我突然想到的一些事情是,您在OP询问层次结构关系时通过提及“兄弟姐妹”提供了一个切线的示例。也许您的写作风格也可以得到改善。您使用很多“ ...”,我们在正式,专业或指导性写作中往往不使用“ ...”。还有一点冗余(例如,您提到,然后在示例中说)。也许您的答案可能更简洁,并且可以更直接地解决OP的问题。
KennyCason
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.