在 RESTful Web 服务中按 ID 和用户名查找

FInd by ID and Username in RESTful Web Services

我对单例资源的最佳偏好感到困惑。我想要具有用户 ID 的端点:

/users/{id}

但是我也想要username作为参数:

/users/{username}

我正在使用 Spring MVC(通过 Spring 引导),它说 2 个端点存在冲突。所以我决定首先为用户检索单例资源。但是为了客户端仍然使用用户名作为参数,我为用户的集合资源添加了用户名作为查询参数:

/users?username=<username>

在我的 repositoryservice 层中,return 是 Optional<User>,即它是 emptyone user 结果。但是在 controller 中,我将其包装在一个 list 中,要么为空,要么为一个,以使其与 /users 的 return as list.

一致。

这样的设计合理吗?还是有更好的设计?谢谢。

您应该尝试阻止客户端使用字符串操作组装 URI,因为为此,客户端必须对服务器内部结构有深入的了解。

然而,优雅的URI方案往往是合理资源结构的标志。当 URI 设计得很好时,对于想要使用 REST API 的开发人员来说,生活会更轻松;客户会喜欢容易记住的 URI。手动编辑 URI 的能力(例如,通过在“/”之后截断其中的一些 URI)是一个明显的优势。

如您所见:URI 标识资源。资源是“事物”,名词,不是动作或动词!如果你做过腐蚀资源设计,匹配的名字通常会自己出现。但是,我想在这里举一个例子,可能是因为它的结构或名称的问题的指示。

http://example.com/customers/create?name=XYZ

这里有点混乱: 动词“创建”在 URI 中没有位置,结构表明该操作由 URI 标识,而不是由 HTTP 动词标识。

您的 URI 应包含名词。对于按预期标识资源的 URI,您还可以轻松想象不同的 HTTP 方法如何工作:PUT updates a DELETE deletes, and a GET 获取信息。这是上面的 URI 看起来更好的示例:

POST http://example.com/customers/

透明且易于理解的 URI 设计的一个明显规则是将层次关系映射到 URI 的路径元素结构。

示例: http://example.com/organization/it/support/networks

因此,如果您是服务器的开发者,因此也是 URI 的设计者,您应该确保 http://example.com/organization/it/support 上的 GET 也是 returns 有用的结果。在您的客户中,您无论如何都不应该做出这样的假设! 这从根本上有助于分布式系统的稳定性,也称为 robustness principle 或 Postel 定律。

对于过滤器的 URI 设计,它们有不同的可能性。最明显的是使用查询参数。对于要为来自德国的所有客户过滤的 /customers 下的客户列表,示例 URI 可能如下所示:

http://example.com/customers?country=Germany 多个查询组件可以用“&”链接:

http://example.com/customers?country=Germany&year=2020

在这里,他们也通过这样的URI设计满足了用户的期望。如果省略查询字符串 (?country=Germany&year=2020),用户将被带到所有客户的未过滤列表中。

拥有稳定的URI很重要,酷的URI不会改变(Tim Berners-Lee)。

回答你的问题:

设计不应被高估;它应该是有用的,但并不完美(这是不可能实现的)。

基本上,您有两个选择:

  • 您有两个单独的端点,例如,/users/byId/{id}/users/byName/{username} 每个都映射到不同的控制器方法
class UserController {
    @GetMapping("/byId/{id}")
    Optional<User> findById(@PathVariable("id") String id){
       ...
    }

    @GetMapping("/byName/{username}")
    Optional<User> findByUsername(@PathVariable("username") String username){
       ...
    }
}
  • 或一个端点 /users/{id-or-name} 并进行联合查询,因此:findAllByIdOrName()

我倾向于说在单独的端点中执行它更干净、更高效。


编辑

RESTful 约定意味着您有 2 个端点:

    @GetMapping("/{id}")
    Optional<User> findById(@PathVariable("id") String id){
       ...
    }

    @GetMapping("/")
    List<User> findUsers(@RequestParam("username") String username){
       // if username is not empty, filter users
       // we could also filter with other user properties according to specs
       ...
    }

除此之外的任何其他内容都已经偏离了惯例。

不能对一个特定映射使用相同的端点名称。在这里,您对两个 @GetMapping 使用相同的端点名称。如果你想做这件事,你应该使用参数。

class UserController {
   @GetMapping("/users")
   Optional<User> findById(@RequestParam(required = false) String id, @RequestParam(required = false) String username){
      if (!username.equals("")) {
         // Response using a username
      }
      if (!id.equals("")) {
         // Response using an id
      }
   }
}

我遇到了同样的问题。这两个端点有冲突,所以事件 /user/{id} 不想工作,所以这就是为什么我给 findbyusername 一个不同的端点,例如:

@GetMapping("/user/username/{username}")