如何为子资源设计 rest 端点 URL

How to design rest endpoint URLs for sub resources

假设我们有两个实体。 User 实体可以有很多要约,而 Offer 必须有一个 User

class User {
    ...
    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private Set<Offer> offers;
}

class Offer {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_user")
    private User user;
}

此时有两个控制器。 UserControllerOrderControllerUserController 映射在 /api/v1/users/ 下,OrderController 映射在 /api/v1/orders/ 下。

我同意它应该在同一个 'UserController' 中,这是有道理的,因为优惠属于用户,所以有一个像这样的端点:

@GetMapping("{user}/offers")
    public Set<OfferDTO> getOffers(@PathVariable("user") String user) {
    return offerService.getOffers(user);
}

您可以定义特殊的 DTO 以从报价中获取 meta-data,例如,如果您想将它们显示在列表中,并且您可以将它们作为列表显示给您的用户。

您可以设置一个类似的端点来更新报价,它可以是一个 POST 端点,以及一个用于删除的 DELETE 端点。您可能想考虑如果用户在您删除报价时正在查看报价会发生什么,例如在后台线程中执行异步任务以删除报价并更新 UI 以通知用户报价已删除。

Spring 有一些非常好的安全注释(检查 this and this),您可以为管理员端点编写自己的注释:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('" + ROLE_ADMIN + "')")
public @interface IsAdmin {}

然后像这样注释你的方法:

@DeleteMapping("/{user}/{offer}/delete")
  @IsReceiverAdmin
  public void delete(@PathVariable("user") String user, @PathVariable("offer") String offer){
    return offerService.delete(user, offer);
  }

当然,服务层的实现非常重要,但它可以像调用您的存储库并在那里执行操作一样简单:)

我在创建端点时使用的一般经验法则是:

  • URL需要简洁易懂
  • 它们应该尽可能短,但仍能提供信息。
  • 尝试以允许您在合理数量内重复使用它的方式构建它
  • 考虑用户体验(无论是从浏览器还是移动应用等调用)

我不确定是否有明确的书面规则应该如何构建 URL。 在您的具体情况下,只有当这是您公开和使用优惠的唯一地方时,我才会使用 /users/{username}/offers/{offerId},因为您是按功能分隔代码的。
如果您有关于优惠的任何进一步逻辑 and/or 有具有此类逻辑的 bean,我将为优惠创建一个单独的控制器,该控制器将位于 /offers 下。 关于你的最后一个问题。这在很大程度上取决于您要实现的目标。如果您需要能够 update/delete/create 提供,那么拥有这样的功能是有意义的。即使它仅由管理员使用。您可以限制对端点的访问。如何做到这一点取决于您授权用户的方式以及您拥有的关于他们的信息。大多数人使用角色。
如果您决定拥有完整的 CRUD 功能,我建议您使用带有请求方法组合的单一路径。

我个人会创建以下内容:

@RestController
@RequestMapping(value = "/users")
class UserController {
@GetMapping("{userId}/offers")
    public Set<Offer> getAllOffers(@PathVariable("userId") String userId){
    ...
  }
@GetMapping("{userId}/offers")
    public Offer getOffer(@PathVariable("userId") String userId, @RequestParam(required = true) String offerId){
    ...
  }
@PutMapping("{userId}/offers")
    public Offer createOffer(@PathVariable("userId") String userId, @RequestBody Offer offer){
    ...
  }
@PostMapping("{userId}/offers")
    public Offer updateOffer(@PathVariable("userId") String userId, @RequestBody Offer offer){
    ...
  }
@DeleteMapping("{userId}/offers")
    public void deleteOffer(@PathVariable("userId") String userId, @RequestParam(required = true) String offerId){
    ...
  }
}

在这种情况下,我认为用于创建和更新的 POST/PUT 会更清晰,因为不会有重复的信息。准确地说是 ID。