【问题标题】:RESTful MVC Pattern for Searches and Search Results用于搜索和搜索结果的 RESTful MVC 模式
【发布时间】:2015-07-02 09:32:48
【问题描述】:

所以,我确定之前一定有人问过这个问题,但我似乎找不到任何东西。问题是,当我为 Web 应用程序编写搜索功能时,我感觉从来都不是很合适。

我正在使用 Ruby on Rails,但我想这个问题适用于您使用 RESTful MVC 模式的任何情况。

假设您有一个想要搜索的资源(例如用户、待办事项等)。一旦应用程序增长,这将不再适用于简单的 LIKE 查询,并且您开始使用索引(例如 Solr、ElasticSearch、Lucene ......)。索引资源也往往是来自 Resource 及其关联对象(用户位置、ToDos 创建者……)的复合数据。

我们如何最好地表达这一点?

  • 它是对 /resources (Resource#index) 的 GET 吗?它是主要资源的选择性列表,但实际上它又是一个复杂的东西,如果搜索功能很广泛,它确实会导致模型的代码膨胀。
  • 它是到 /searches (Search#create) 的 POST 吗?我们正在创建搜索但不保存它。相反,它会转化为一组 SearchResults。
  • 那么,它是对 SearchResult (SearchResult#show) 的 GET 吗?但它没有身份证。我猜 SearchIndex 是该模型的数据库,但您不会真正创建 SearchResult,对吗?它更像是一个以 SearchResult#show 结尾的 Search#create,但我也觉得这很奇怪。

【问题讨论】:

  • 在对这个话题进行了更多思考之后,我意识到这真的与 MVC 没有任何关系。无论底层数据架构如何,我们主要讨论的是在 REST 中表示搜索的最佳方式。如下所示,GETing 资源并通过参数过滤它们似乎是解决此问题的常用方法。如果 MVC 是人们使用的唯一架构抽象层,那么是的,它们往往会变得臃肿。顺便说一句,在没有尝试将 MVC 引入图片的情况下,我的第一句话被证明是正确的:RESTful URL design for search

标签: rest search design-patterns model-view-controller


【解决方案1】:

通常不建议使用 POST 进行搜索操作,因为您会失去 GET 必须提供的所有优势 - 语义、幂等性、安全性(可缓存性)......

许多 RESTful 和类 REST 系统使用简单的 GET 查询,并将搜索参数作为 querypath 参数,以允许基于客户端和服务器的查询和结果缓存。从 HTTP 1.1 开始。除非正确指定缓存标头,否则缓存包含查询参数的 GET 请求不是问题。

但是预定义的查询有LIKE 查询的味道,你尽量避免。特别是 ElasticSearch 允许动态地向类型添加新字段。这可能会引入新的开销,以跟上添加新的预定义过滤器以支持对这些字段的查询。因此,从长远来看,根据需要动态添加查询可能是基本要求。不过,这并不难实现。

因此,包含动态添加的搜索过滤器的 GET /users/12345 查询的示例输出可能如下所示:

{
    "id": "12345",
    "firstName": "Max",
    "lastName": "Test",
    "_schema": {
        "href": "http://example.com/schema/user"
    }
    "_links": {
        "self": {
            "href": "/users/12345",
            "methods": ["get", "put", "delete"]
        },
        "curies": [{ 
            "name": "usr", 
            "href": "http://example.com/docs/rels/{rel}", 
            "templated": true
        }],
        "usr:employee": {
            "href": "/companies/112233",
            "title": "Sample Company",
            "type": "application/hal+json"
        }
    },
    "_embedded": {
        "usr:address": [
            {
                "_schema": {
                    "href": "http://example.com/schema/address"
                },
                "street" : "Sample Street",
                "zip": "...",
                "city": "...",
                "state": "...",
                "location": {
                    "longitude": "...",
                    "latitude": "..."
                }
                "_links": {
                    "self": {
                        "href": "/users/12345/address/1",
                        "_methods": ["get", "post", "put", "delete"],
                    }
                }
            }
        ],
        "usr:search": {
            "_schema": {
                "href": "http://example.com/schema/user_search"
            }
            "_links": {
                "self": {
                    "href": "/users/12345/search",
                    "methods: ["post", "delete"]
                }
            },
            "filters": [
                "_schema": {
                    "href": "http://example.com/schema/user_search_filter"
                },
                "_links": {
                    "self": {
                        "href": "/users/12345/search/filters",
                        "methods: ["get"]
                    },
                    "next": {
                        "href": "/users/12345/search/filters?page=2"
                        "methods: ["get"]
                    }
                },
                {
                    "byName": {
                        "query": {
                            "constant_score": {
                                "filter": {
                                    "term": {
                                        "name": {
                                            "href": "/users/12345#name"
                                        }
                                    }
                                }
                            }
                        }
                        "_links": {
                            "self": {
                                "href": "/users/12345/search/filter/byName",
                                "methods": ["get", "put", "delete"],
                                "_schema": {
                                    "href": "http://example.com/schema/search_byName"
                                }
                                "type": "application/hal+json"
                            }
                        }
                    }
                },
                {
                    "in20kmDistance" : {
                       "query": {
                           "filtered" : {
                               "query" : {
                                   "match_all" : {}
                               },
                               "filter" : {
                                   "geo_distance" : {
                                       "distance" : "20km",
                                           "Location" : {
                                               "lat" : {
                                                   "href": "/users/12345/address/location#lat"
                                               },
                                               "lon" : {
                                                   "href": "/users/12345/address/location#lon"
                                               }
                                           }
                                       }
                                   }
                               }
                           }
                        }
                        "_links": {
                            "self": {
                                "href": "/users/12345/search/filter/in20kmDistance,
                                "methods": ["get", "put", "delete"],
                                "_schema": {
                                    "href": "http://example.com/schema/search_in20kmDistance"
                                }
                                "type": "application/hal+json"
                            }
                        }
                    }
                },
                {
                    ...
                }
            ]
        }
    }
}

上面的示例代码包含一个用户表示,其中嵌入了地址和搜索过滤器,采用扩展的JSON HAL 格式。由于 RESTful 资源应尽可能一目了然,因此示例包含指向其位置和架构的链接,以便 postput 操作也知道服务器可能需要哪些字段。

search 资源充当过滤器的控制器,因为它只允许一次添加新过滤器或删除所有过滤器,而通过在 /users/{userId}/search/filters?page=pageNo 上调用 GET 来遍历过滤器页面。

一个实际的过滤器现在包含要执行的实际指令 - 在这种情况下,一个 ElasticSearch 查询用户名或当前地址 20 公里距离内的所有内容 - 以及一个指向执行的实际 URI 的链接询问。请注意,ElasticSearch 代码实际上包含指向包含实际查询应使用的数据的资源的链接。当然,也可以返回包含实际用户数据的有效 ElasticSearch 查询,甚至返回 JSON Pointer,而不是数据的 URI——这又是一些实现细节。

这种方法允许在运行时动态添加新查询或更新现有查询,同时在查询时保持GET 语义不变。此外,还可以使用缓存功能,这可能会显着提高性能 - 特别是在用户数据不经常更改的情况下。

然而,这种方法的缺点是,您必须返回更多关于用户查找的数据。您还可以考虑不返回嵌入式过滤器,并让客户端显式地轮询这些过滤器。此外,当前过滤器是通过某个名称添加的,该名称充当键。在实践中,这可能会导致命名冲突。因此,最终 UUID 会更好,但如果人类必须调用这些 URI,也会带走语义,因为 byName 对人类来说肯定比 de305d54-75b4-431b-adb2-eb6b9e546014 更具语义,但这更多的是实现细节。

【讨论】:

  • 好的,谢谢罗曼。特别是解释为什么我们可以排除POST。将搜索资源嵌套在主资源下方实际上是创建不言自明路线的好方法……尽管它仍然存在暗示它是有状态的问题。
猜你喜欢
  • 2011-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
  • 1970-01-01
  • 1970-01-01
  • 2011-08-27
相关资源
最近更新 更多