问题 REST和复杂的搜索查询


我正在寻找一种在REST API中建模搜索查询的强大方法。

在我的api中,您可以使用查询参数在资源的URI中指定搜索条件。

例如:

/cars?search=color,blue;AND;doors,4 --> Returns a list of blue cars with 4 doors

/cars?search=color,blue;OR;doors,4 --> Returns a list of cars that are blue or have 4 doors

在服务器端,搜索字符串映射到所需的基础技术。根据其余资源,这可以是SQL查询,Hibernate Criteria api,另一个Web服务调用,......

这2个例子很简单,但我还需要更复杂的搜索功能,如子字符串搜索,日期之前/之后搜索,NOT,...

这是我认为的常见问题。是否有我可以使用的库(或模式):

  • 将指定为字符串的搜索查询映射到通用Criteria模型。 搜索格式不必与上面列出的相同。
  • 允许我这样做 将Criteria模型映射到任何技术 我需要用。
  • 为Hibernate / JPA / SQL提供映射支持,但这是一个奖励;)

亲切的问候,

格伦


5381
2017-10-28 15:28


起源

听起来很安全。您是否真的希望第三方图书馆负责互联网上随机用户可以向您请求的复杂查询?自己解析查询是您验证查询是否是您要执行的查询的机会。 - Henning Makholm
@HenningMakholm:解析在验证之前进行。您需要解析的结果来确定解析结果是否是您要执行的查询。而且你真的不想把你的开发资金花在像解析/ lexing这样基本的东西上,并建立一个你可以分析的查询结构,并且(在你决定它是你要回答的东西之后)传递给你的查询处理模块/技术/图书馆。 - Marjan Venema
@HenningMakholm:我同意Marjan Venema。安全性是一个单独的问题,可以在将查询映射到基础技术时处理。例如。如果使用得当,Hibernate Criteria api对于sql注入是安全的。对于SQL,您可以使用 OWASP ESAPI等 - GlennV
@GlennV:我觉得你想要将“将查询映射到底层技术”的过程外包给一个通用库。如果您有一个库将您的URL转换为SQL查询,那么您必须解析输出SQL以确定该查询是否是您希望允许Internet上的随机用户执行的查询。例如,如果用户可以指定一个非常复杂的“合法”查询,那么“安全的SQL注入”就不是一切,例如,它会吞噬您的服务器资源。 - Henning Makholm
@HenningMakholm现在你谈论的是性能。同样,这是一个单独的问题,这不是我的问题所在。如果存在将搜索uri查询参数映射到某些底层技术的库,那么我会考虑使用它。当然,我会考虑安全性和性能。要求安全且高性能的api并不一定意味着你必须从头开始编写所有内容。 - GlennV


答案:


每当我遇到这些问题时,我会问自己“如果我正在创建传统网页,我将如何向用户展示”?简单的答案是我不会在一个页面中提供这些选项。界面太复杂了;但我能做的是提供一个界面,允许用户在许多页面上构建越来越复杂的查询,这是我认为你应该在这种情况下应该采用的解决方案。

HATEOAS约束 指定我们必须在响应中包含超媒体控件(链接和表单)。所以,假设我们有一个分页的汽车收藏品 /cars 有了搜索选项,所以当你得到 /cars 它会返回类似的内容(顺便说一下,我在这里使用自定义媒体类型,但表单和链接应该非常明显。如果不是,请告诉我):

<cars href="/cars">
    <car href="/cars/alpha">...</car>
    <car href="/cars/beta">...</car>
    <car href="/cars/gamma">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?page=2"/>
    <search-color href="/cars" method="GET">
        <color type="string" cardinality="required"/>
        <color-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color-match>
        <color-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color-logic>
    </search>
    <search-doors href="/cars" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

所以只要说我们搜索白色汽车,我们就会GET /cars?color=white 我们可能会得到类似的东西:

<cars href="/cars?color=white">
    <car href="/cars/beta">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?color=white&page=2"/>
    <search-color href="/cars?color=white" method="GET">
        <color2 type="string" cardinality="required"/>
        <color2-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color2-match>
        <color2-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color2-logic>
    </search>
    <search-doors href="/cars?color=white" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

然后,这个结果让我们改进我们的查询。所以只是说我们想要白色车而不是“灰白色”车,我们可以GET'/ cars?color = white&color2 = off-white&color2-logic = not',这可能会返回

<cars href="/cars?color=white&color2=off-white&color2-logic=not">
    <car href="/cars/beta">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?color=white&color2=off-white&color2-logic=not&page=2"/>
    <search-color href="/cars?color=white&color2=off-white&color2-logic=not" method="GET">
        <color3 type="string" cardinality="required"/>
        <color3-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color3-match>
        <color3-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color3-logic>
    </search>
    <search-doors href="/cars?color=white&color2=off-white&color2-logic=not" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

然后我们可以进一步细化我们的查询,但重点是在整个过程中的每一步,超媒体控件告诉我们什么是可能的。

现在,如果我们考虑汽车的搜索选项,颜色,门,品牌和模型都不是无限制的,所以我们可以通过提供枚举来使选项更加明确。例如

<cars href="/cars">
    ...
    <search-doors href="/cars" method="GET">
        <doors type="enumeration" cardinality="required">
            <option name="2"/>
            <option name="3"/>
            <option name="4"/>
            <option name="5"/>
        </doors>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

然而,我们唯一的白色车可能是2门和4门,在这种情况下GETing /cars?color=white 可能会给我们

<cars href="/cars?color=white">
    ...
    <search-doors href="/cars?color=white" method="GET">
        <doors type="enumeration" cardinality="required">
            <option name="2"/>
            <option name="4"/>
        </doors>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

同样,当我们改进颜色时,我们可能会发现它们只有几个选项,在这种情况下我们可以从提供字符串搜索切换到提供枚举搜索。例如,GETing /cars?color=white 可能会给我们

<cars href="/cars?color=white">
    ...
    <search-color href="/cars?color=white" method="GET">
        <color2 type="enumeration" cardinality="required">
            <option name="white"/>
            <option name="off-white"/>
            <option name="blue with white racing stripes"/>
        </color2>
        <color2-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color2-logic>
    </search>
    ...
</cars>

您可以对其他搜索类别执行相同操作。例如,最初您不想枚举所有品牌,因此您将提供某种文本搜索。一旦集合被细化,并且只有几个模型可供选择,那么提供枚举是有意义的。同样的逻辑适用于其他集合。例如,你不想列举世界上所有的城市,但是一旦你将这个区域精炼到10个左右的城市,那么枚举它们会非常有帮助。

有没有一个图书馆会为你做这个?没有我知道的。我见过的大多数人甚至不支持超媒体控制(即 你必须自己添加链接和表单)。你有可以使用的模式吗?是的,我相信以上是解决此类问题的有效模式。


4
2017-10-29 11:26



有几个人评论说,没有库可以支持这种开箱即用的功能。我认为这是当前JAX-RS实现仍然缺乏的一部分。有一些尝试将OData与REST一起使用,例如,Redlet具有OData扩展,并且还有 odata4j 这似乎有泽西岛的支持,但初看起来,它看起来并不成熟。对于我的项目,我(很高兴)使用Jersey,并将继续推出我自己的解决方案。你提出的方法@TomHoward是实用的,它符合我的目标,谢谢;) - GlennV


嗯......这是一个棘手的领域。没有针对您的问题量身定制的框架。甚至@ p0wl提到的ODATA对字段映射到域模型的方式也有一定的限制。一点之后变得奇怪。

解决此问题的最简单方法是将查询作为String接受,然后使用AST或您想要的任何内容对域特定语言进行验证。这使您可以在接受查询时保持灵活性,同时仍然可以验证查询的正确性。您失去的是能够通过URL以更有意义的方式描述查询。

看一眼 第8章 Restful食谱食谱。它突出了我提出的解决方案之一,并建议采用其他方法。根据客户所需的灵活性与查询的表现力选择一个。你可以在某个地方找到平衡点。


4
2017-10-28 15:53





@GlennV

我不确定是否存在与此直接相关的模式/规则,如果这是一个常见问题(我的意思是查询字符串中的逻辑运算符),但在设计类似这样的事情时我会问自己以下问题:

  • 客户如何构建此类查询?

问题是URI(作为一个整体,包括查询字符串部分)必须完全由服务提供者控制,并且客户端不能直接耦合到特定于您的问题域的URI构造逻辑。

客户端使用URI以及构造它们的方式有多种:

  • 客户端可以跟随链接,这些链接通过“链接:<...>”标题或实体主体(如原子或HTML链接或类似内容)包含在HTTP标头中
  • 客户端可以按媒体类型或其他规范定义的规则构造URI。

引导URI构建过程的几种众所周知的方法:

根据上述信息,您可以选择现有方法或设计自己的方法,但要记住简单的规则,服务必须协调客户如何构建查询(即定义您自己的规范,自定义媒体类型或扩展现有)。

另一件事是你如何将URI映射到底层实现,并且在REST方面有两个非常重要的事情:

  • 连接器  - 其中包括HTTP + URI规范,实际上只有客户端的连接器问题。
  • 零件  - 这是潜在的实施,除了你,没有人关心。这部分需要完全隐藏在客户端之外,你可以在这里选择任何绑定方法,没有人能告诉你确切的方法,它完全取决于特定的要求。

2
2017-10-28 20:05





你搜索像ODATA这样的图书馆(链接)?

这将解决您的查询解析问题,您只需将查询转发到您选择的数据库。


1
2017-10-28 15:42





如前所述,仍然没有通用的解决方案来做到这一点。可能,最好的选择(至少发现最好的香港专业教育学院)是Spring DATA REST(http://static.springsource.org/spring-data/rest/docs/1.1.0.M1/reference/htmlsingle/)。使用CrudRepository,您可以获得findOne,findAll等方法。如果扩展它,您可以添加自己的通用映射。 在示例中,我们所做的是将其扩展为具有以下通用查询选项: customer?country = notArgentina,以获得不属于阿根廷的客户。或者,例如:customer?country = like()让所有客户都拥有像“Island”这样的国家(以SQL方式)。希望能帮助到你。


1
2018-03-25 00:15