如何设计RESTful搜索/过滤?

Eri*_*k B 426 api rest search filter

我目前正在设计和实现PHP中的RESTful API.但是,我没有成功实施我的初始设计.

GET /users # list of users
GET /user/1 # get user with id 1
POST /user # create new user
PUT /user/1 # modify user with id 1
DELETE /user/1 # delete user with id 1
Run Code Online (Sandbox Code Playgroud)

到目前为止标准相当,对吗?

我的问题是第一个问题GET /users.我正在考虑在请求正文中发送参数来过滤列表.这是因为我希望能够在不获取超长URL的情况下指定复杂的过滤器,例如:

GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4
Run Code Online (Sandbox Code Playgroud)

相反,我希望有类似的东西:

GET /users
# Request body:
{
    "parameter1": "value1",
    "parameter2": "value2",
    "parameter3": "value3",
    "parameter4": "value4"
}
Run Code Online (Sandbox Code Playgroud)

它更具可读性,为您提供设置复杂过滤器的绝佳机会.

无论如何,file_get_contents('php://input')没有返回请求的请求体GET.我也尝试过http_get_request_body(),但我正在使用的共享主机没有pecl_http.不确定它会有所帮助.

我发现了这个问题,并意识到GET可能不应该有一个请求体.这有点不确定,但他们建议不要这样做.

所以现在我不知道该怎么做.你如何设计RESTful搜索/过滤功能?

我想我可以使用POST,但这似乎不太RESTful.

小智 383

实现RESTful搜索的最佳方法是将搜索本身视为资源.然后您可以使用POST动词,因为您正在创建搜索.您不必在数据库中逐字创建以使用POST.

例如:

Accept: application/json
Content-Type: application/json
POST http://example.com/people/searches
{
  "terms": {
    "ssn": "123456789"
  },
  "order": { ... },
  ...
}
Run Code Online (Sandbox Code Playgroud)

您正在从用户的角度创建搜索.这个实现细节是无关紧要的.一些RESTful API甚至可能不需要持久性.这是一个实现细节.

  • 对搜索端点使用POST请求的一个重要限制是它无法加入书签.为搜索结果(特别是复杂查询)添加书签非常有用. (205认同)
  • 这绝对是最糟糕的答案.我无法相信它有这么多的赞成.这个答案解释了原因:http://programmers.stackexchange.com/questions/233164/how-do-searches-fit-into-a-restful-interface (73认同)
  • 使用POST进行搜索可能会破坏REST缓存约束.http://whatisrest.com/rest_constraints/cache_excerps (68认同)
  • 根据其性质,搜索是瞬态的:数据在具有相同参数的两次搜索之间演变,因此我认为GET请求不会完全映射到搜索模式.相反,搜索请求应该是POST(/资源/搜索),然后您可以保存该搜索并重定向到搜索结果,例如/ Resource/search/iyn3zrt.这样,GET请求成功并且有意义. (53认同)
  • 我不认为post是合适的搜索方法,普通GET请求的数据也可能随时间而变化. (28认同)
  • 也许只是转储所有动词,并使用POST来处理所有事情......但是说真的,如果我们要推断这个逻辑,那么应该使用POST而不是GET来检索所有动态页面,因为我们正在创建一个新的(并且可能唯一的)HTML/JSON /每个请求的任何文档.不敢相信这个答案有这么多的赞成. (15认同)
  • 这应该被OP接受为答案.这是对这个问题最正确的回答.@ jason-harrelson表示持久性并不需要支持POST的预期用途.对于搜索资源,使用POST的好处是可以将其用作未来的功能,例如已保存的搜索,或建议搜索API的用户. (9认同)
  • @Zlatko:想想'GET/users` - 你需要分页,对吧?分页也是一种搜索/过滤器,因为您传递了一些参数并获得了资源的子集.这是否值得`POST`? (7认同)
  • @OlegsJeremejevs Idempotent并不意味着"总是返回相同的结果".否则,我永远无法从任何也接受P​​OST或PUT的资源或集合中获取 (7认同)
  • 正是我在想什么.这就是为什么我认为POST在这里是错误的.搜索应该只是对"GET/resource"调用的过滤器. (6认同)
  • Facebook和Netflix用REST表达了他们的地狱,并提出了他们自己的查询规范.GraphQl和Falcor以及tney都是OSS. (4认同)
  • @jkerak 无论如何,您根本不应该拥有像 ssn 这样的敏感数据通过网络传输,尤其是对于搜索而言!使用代理键或不敏感的东西。 (3认同)
  • 非常有创意的答案:只需将搜索视为资源和-poof-您有理由使用POST而不是GET.在我看来,方便但错误. (3认同)
  • 怎么样GitHub做到这一点.我喜欢他们如何将它吐出资源但我想如果你正在进行像Google一样的自由文本搜索,你就不能把它真正地分解成网址中的具体资源.GitHub是这样的:/ search/[resourceName]?q = John ...其中q是关键字或者在搜索框中键入的文本,但是在这种情况下你要搜索特定的资源类型 (2认同)
  • @OlegsJeremejevs"可能只是转储所有动词,并使用POST来处理所有事情"我不确定你的意思.在这里,我们将发布到一个集合(搜索)以在该集合中创建一个新资源(搜索).这不是它通常用于什么? (2认同)
  • 在浏览了 Richard 发布的链接后,我认为 POST 不是正确的 RESTful 方式。 (2认同)
  • 我认为带参数的GET始终不是正确的选择。您可能正在通过敏感数据(例如卡号或SSN)进行搜索,将信息隐藏在POST正文中更为安全。隐藏发布数据中的所有内容是防止恶意攻击或阻止聪明用户执行不应执行的操作的好方法。 (2认同)
  • 使用GET进行搜索并不违反GET的幂等性质.关于REST的"幂等"点是调用该操作本身不会影响资源.用params调用GET 10次不会改变结果. (2认同)
  • 我发现有趣的是,有多少关于这个答案的评论完全错过了OP问题中的“RESTful API**”一词,使得有关书签或导航的任何观点都毫无意义。 (2认同)

jfc*_*edo 132

如果在GET请求中使用请求主体,则会违反REST原则,因为您的GET请求将无法缓存,因为缓存系统仅使用URL.

更糟糕的是,您的URL无法加入书签,因为该URL不包含将用户重定向到此页面所需的所有信息

使用URL或Query参数代替请求正文参数.

例如:

/myapp?var1=xxxx&var2=xxxx
/myapp;var1=xxxx/resource;var2=xxxx 
Run Code Online (Sandbox Code Playgroud)

事实上,HTTP RFC 7231说:

GET请求消息中的有效负载没有定义的语义; 在GET请求上发送有效负载主体可能会导致某些现有实现拒绝该请求.

有关更多信息,请查看此处

  • 从我的错误中学习 - 我使用接受的答案的建议(POST json)设计了一个api,但我正在转向url参数.书签能力可能比您想象的更重要.在我的情况下,需要将流量引导到某些搜索查询(广告系列).此外,使用历史API使用URL参数更有意义. (25认同)
  • 这取决于它的使用方式。如果您链接到基于这些参数加载页面的 URL,这是有道理的,但如果主页正在执行 AJAX 调用只是为了根据过滤器参数获取数据,则无论如何您都不能将其添加为书签,因为它是ajax 调用并没有任何意义。当然,您也可以为 URL 添加书签,当您访问该 URL 时,该 URL 会建立一个过滤器并将其发布到 ajax 调用中,这样就可以正常工作了。 (3认同)

Opa*_*pal 68

似乎资源过滤/搜索可以以RESTful方式实现.我们的想法是引入一个名为/filters/或的新端点/api/filters/.

使用此端点过滤器可以视为资源,因此通过POST方法创建.这种方式 - 当然 - 身体可用于携带所有参数以及可以创建复杂的搜索/过滤器结构.

创建此类过滤器后,有两种方法可以获得搜索/过滤结果.

  1. 将返回具有唯一ID的新资源以及201 Created状态代码.然后使用此ID GET可以使请求/api/users/如下:

    GET /api/users/?filterId=1234-abcd
    
    Run Code Online (Sandbox Code Playgroud)
  2. 新的过滤器通过创建后POST它将不会与回复201 Created,但在一次与303 SeeOther一起Location头指向/api/users/?filterId=1234-abcd.此重定向将通过底层库自动处理.

在这两种情况下,需要进行两次请求以获取过滤结果 - 这可能被视为缺点,尤其是对于移动应用程序.对于移动应用程序,我会使用单个POST调用/api/users/filter/.

如何保持创建的过滤器?

它们可以存储在DB中,以后再使用.它们也可以存储在一些临时存储器中,例如redis,并且有一些TTL,之后它们将过期并将被删除.

这个想法有什么好处?

过滤器,过滤结果可以缓存,甚至可以加入书签.

  • 此方法的唯一问题是,查询中是否有日期时间过滤器(或不断变化的值)。那么存储在数据库(或缓存)中的过滤器的数量是无数的。 (4认同)
  • 好吧,这应该是公认的答案。您没有违反REST原则,并且可以对资源进行长时间的复杂查询。很好,干净而且与书签兼容。唯一的附加缺点是需要存储用于创建的过滤器的键/值对以及已经提到的两个请求步骤。 (2认同)

Daf*_*aff 16

我认为您应该使用请求参数,但只要没有合适的HTTP标头来完成您想要的操作.在HTTP规范没有明确说,这GET不能有身体.然而,本文指出:

按照惯例,当使用GET方法时,标识资源所需的所有信息都在URI中编码.HTTP/1.1中没有关于安全交互(例如,检索)的约定,其中客户端在HTTP实体主体中而不是在URI的查询部分中向服务器提供数据.这意味着对于安全操作,URI可能很长.

  • ElasticSearch也可以与身体一起使用并且效果很好! (3认同)
  • 是的,但他们控制服务器的实现可能不是互联网上的问题。 (2认同)

the*_*ain 8

因为我正在使用laravel/php后端,所以我倾向于使用这样的东西:

/resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource
Run Code Online (Sandbox Code Playgroud)

PHP会自动将[]params转换为数组,所以在这个例子中,我最终会得到一个$filter包含过滤器数组/对象的变量,以及一个页面和我想要加载的任何相关资源.

如果您使用其他语言,这可能仍然是一个很好的约定,您可以创建一个解析器来转换[]为数组.

  • @Sky 这可以通过对`[` 和`]` 进行URI 编码来避免。使用这些字符的编码表示来对查询参数进行分组是众所周知的做法。它甚至用于 [JSON:API 规范](https://jsonapi.org/format/#fetching-sparse-fieldsets)。 (2认同)

lui*_*nal 7

如果您的初始API完全是RESTful(特别是当您处于alpha阶段时),请不要担心太多.让后端管道首先工作.您可以随时进行某种URL转换/重写以映射事物,迭代完善,直到您获得足够稳定的广泛测试("beta").

您可以定义URI,其参数按URI上的位置和约定进行编码,前缀为您知道始终映射到某个内容的路径.我不知道PHP,但我认为这样的工具存在(因为它存在于其他语言的Web框架中):

.IE.对商店#1的i = 1..4使用param [i] = value [i]执行"用户"类型的搜索(使用value1,value2,value3,...作为URI查询参数的简写):

1) GET /store1/search/user/value1,value2,value3,value4
Run Code Online (Sandbox Code Playgroud)

要么

2) GET /store1/search/user,value1,value2,value3,value4
Run Code Online (Sandbox Code Playgroud)

或者如下(尽管我不推荐它,稍后会详细介绍)

3) GET /search/store1,user,value1,value2,value3,value4
Run Code Online (Sandbox Code Playgroud)

使用选项1,您将所有带前缀的URI映射/store1/search/user到搜索处理程序(或PHP指定的任何一个)默认搜索store1下的资源(相当于/search?location=store1&type=user.

按照API的记录和强制执行的惯例,参数值1到4用逗号分隔并按顺序显示.

选项2将搜索类型(在本例中user)添加为位置参数#1.这两种选择都只是一种美容选择.

选项3也是可能的,但我不认为我会喜欢它.我认为在某些资源中搜索的能力应该在搜索本身之前的URI本身中呈现(好像在URI中清楚地表明搜索在资源中是特定的.)

相对于URI传递参数的优点是搜索是URI的一部分(因此将搜索视为资源,其内容可以 - 并且将随时间变化的资源.)缺点是参数顺序是强制性的.

一旦你做了这样的事情,你可以使用GET,它将是一个只读资源(因为你不能POST或PUT到它 - 它得到GET时更新).它也是一种资源,只有在被调用时才会存在.

也可以通过将结果缓存一段时间或使用DELETE导致缓存被删除来为其添加更多语义.但是,这可能与人们通常使用DELETE的方式背道而驰(因为人们通常使用缓存头来控制缓存.)

你如何去做这将是一个设计决定,但这将是我的方式.它并不完美,我相信会有这样的情况,这样做不是最好的事情(特别是对于非常复杂的搜索标准).

  • 我没有投票,但事实上问题始于:"我目前正在设计和实现一个RESTful API",你的答案始于"如果你的初始API完全是REST,那就不要担心太多"感觉对我不对 如果您正在设计API,那么您正在设计API.问题是询问如何最好地设计API,而不是关于是否应该设计API. (101认同)
  • API*是系统,首先在API上工作,而不是后端管道,第一个实现可能/应该只是一个模拟.HTTP有一个传递参数的机制,你建议它被重新发明,但更糟糕的是(有序参数而不是名称值对).因此投票率下降. (13认同)
  • @gardarh - 是的,这感觉不对,但有时它是务实的.主要目标是设计适用于手头业务环境的API.如果完全RESTFULL方法适合手头的业务,那么就去做吧.如果不是,那就不要去了.也就是说,设计一个满足您特定业务需求的API.尝试将RESTfull作为其主要要求,与询问"如何在X/Y问题中使用适配器模式"没有什么不同.除非他们解决实际的,有价值的问题,否则不要使用鞋拔范式. (12认同)
  • 哟,如果你(某人,无论是谁/什么)的东西都适合我的回答,那么至少会发表评论来表明你究竟不同意这对你有什么影响吗?我知道这是interweebz,但是......;) (7认同)