如何 create/update RESTful API 中的多对多关系

How to create/update many-to-many relationships in a RESTful API

在我的 API:

中,这就是您将球员添加到球队的方式
PUT /teams/1/players/1/

我现在想把球员换到另一个球队。

我该怎么做?

Changing a resource is usually done via HTTP PUT or HTTP PATCH (the latter one if only a partial update should be executed). However, using a construct like PUT /teams/1/players/1?moveToTeam=2 has some semantic issues of replacing the current representation with the payload found within the body of the request. The optional query parameter is a method you invoke on the server side to move a player from team 1 to team 2. However, HTTP PUT is an idempotent operation which basically means if you execute the same statement twice it will yield the same影响。 Though, as you are removing the player from team 1 and copying the data of the current user to a new location, invoking the same method violates the idempotent nature of HTTP PUT as a consecutive call will either fail as no /teams/1/players/1 resource is available or no content for the player is available and therefore the content of the moved player will also get set to an empty body. Therefore I do not recommend using HTTP PUT.

Probably DELETE /teams/1/players/1?moveToTeam=2 is the closest single HTTP operation you can get to move a player from one team to another. This request successfully deletes the player from team 1 which should not be available after the invocation and therefore a further invocation wont change the resource (idempotent). The operation should return a 200 OK including the new state of the player entity which link should point now to its new location. HTTP DELETE also allows resources to be moved, according to the spec. Though, the spec states that this resource should not be accessible after the operation was executed.

The DELETE method requests that the origin server delete the resource identified by the Request-URI. This method MAY be overridden by human intervention (or other means) on the origin server. The client cannot be guaranteed that the operation has been carried out, even if the status code returned from the origin server indicates that the action has been completed successfully. However, the server SHOULD NOT indicate success unless, at the time the response is given, it intends to delete the resource or move it to an inaccessible location.

A successful response SHOULD be 200 (OK) if the response includes an entity describing the status, 202 (Accepted) if the action has not yet been enacted, or 204 (No Content) if the action has been enacted but the response does not include an entity. (Source)

Therefore this operation is a bit risky if you try to be fully RESTful IMO.

I therefore would recommend to split the operation into atomic units:

  • Retrieve the current data of the user to move and store is temporarily (GET /teams/1/players/1)
  • Remove the player from team 1 (DELETE /teams/1/players/1)
  • Add the player to the team you want the player to belong to. Use the temporarily stored data as payload in the request (POST /teams/2/players)
  • Create a redirect (301 Moved Permanently) for users who still have a reference to GET /teams/1/players/1 so that they automatically get forwarded to GET /teams/2/players/n where n is the new ID of the player.

Each operation obeys to the rules defined within the HTTP specification and therefore it should be fine separating the requests into atomic portions.


UPDATE

While I agree with @EricStein that separating players to their own resource, as this simplifies a move of a player from team1 to team2 drastically, I had a look at the PATCH method also and it seems it is more appropriate than DELETE or splitting the move into multiple atomic requests.

PATCH is often confused with a partial update where just the new value for a resource-属性 is sent to the server, which it is not. The result of PATCH may be equal but PATCH sends necessary steps the server has to execute in order to transform a resource from one state to a new state. The spec clearly states:

The PATCH method requests that a set of changes described in the request entity be applied to the resource identified by the Request- URI. The set of changes is represented in a format called a "patch document" identified by a media type. If the Request-URI does not point to an existing resource, the server MAY create a new resource, depending on the patch document type (whether it can logically modify a null resource) and permissions, etc.

The difference between the PUT and PATCH requests is reflected in the way the server processes the enclosed entity to modify the resource identified by the Request-URI. In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version. The PATCH method affects the resource identified by the Request-URI, and it also MAY have side effects on other resources; i.e., new resources may be created, or existing ones modified, by the application of a PATCH. (Source)

Especially the last quoted line states that it can have side-effects and may create new resources. Therefore, if you can't change your model for whatever reason, PATCH is probably the best way to move a player from one team to an other. This method allows you to send the necessary steps the server has to execute in order to create the new resource for the moved player, delete the old representation and establish a permanent forward to the new resource in one single request. However, this operation is neither safe nor idempotent!

我可以恭敬地建议不要这样做吗?相反,将 /players/teams 设为顶级资源。使用 属性 控制玩家所在的球队。然后,您可以通过 PUT 使用新球队值对球员进行更新来更新球员的球队。

或者,创建一个包含所有玩家-团队映射的新顶级资源,比如“/team-memberships”。然后你可以查询 GET /team-memberships?teamId=7GET /team-memberships?playerId=2。您可以 post 和删除此资源以在团队中添加和删除玩家。

从概念上讲,球员不是球队的子资源。玩家是与团队关联的独立资源。我认为上述任何一种方法都会给你更大的灵活性,更容易理解和使用。

做一些类似 POST /teams/1/players/5/transfers 的事情怎么样?这将产生创建 "Team Player Transfer" 的效果。 TeamidPlayer在url中提供,正文可以是{ new_team_id: 2 }之类的东西。服务器可以随心所欲地执行 "transfer"。

"Transfer" 可能是也可能不是数据库中的对象(在我的情况下不是)。这个结构我觉得RESTful,读起来也很直观