ventajas que ejemplo arquitectura rest entity-relationship

que - ¿Cómo manejar relaciones de muchos a muchos en una API RESTful?



rest y json (7)

Imagina que tienes 2 entidades, jugador y equipo , donde los jugadores pueden estar en varios equipos. En mi modelo de datos, tengo una tabla para cada entidad y una tabla de unión para mantener las relaciones. Hibernate está bien para manejar esto, pero ¿cómo puedo exponer esta relación en una API RESTful?

Puedo pensar en un par de maneras. Primero, podría hacer que cada entidad contenga una lista de la otra, por lo que un objeto Jugador tendría una lista de Equipos a los que pertenece, y cada objeto Equipo tendría una lista de Jugadores que pertenecen a ella. Por lo tanto, para agregar un jugador a un equipo, solo POSTARÍA la representación del jugador a un punto final, algo como POST /player o POST /team con el objeto apropiado como carga útil de la solicitud. Esto me parece lo más "RESTO", pero se siente un poco raro.

/api/team/0: { name: ''Boston Celtics'', logo: ''/img/Celtics.png'', players: [ ''/api/player/20'', ''/api/player/5'', ''/api/player/34'' ] } /api/player/20: { pk: 20, name: ''Ray Allen'', birth: ''1975-07-20T02:00:00Z'', team: ''/api/team/0'' }

La otra forma en la que puedo pensar para hacer esto sería exponer la relación como un recurso por derecho propio. Entonces, para ver una lista de todos los jugadores en un equipo determinado, puedes hacer un GET /playerteam/team/{id} o algo así y recuperar una lista de entidades PlayerTeam. Para agregar un jugador a un equipo, POST /playerteam con una entidad PlayerTeam apropiadamente construida como carga útil.

/api/team/0: { name: ''Boston Celtics'', logo: ''/img/Celtics.png'' } /api/player/20: { pk: 20, name: ''Ray Allen'', birth: ''1975-07-20T02:00:00Z'', team: ''/api/team/0'' } /api/player/team/0/: [ ''/api/player/20'', ''/api/player/5'', ''/api/player/34'' ]

¿Cuál es la mejor práctica para esto?


  1. / players (es un recurso maestro)
  2. / teams / {id} / players (es un recurso de relación, por lo que reacciona de manera diferente a 1)
  3. / membresías (es una relación pero semánticamente complicada)
  4. / jugadores / membresías (es una relación pero semánticamente complicada)

Prefiero 2


En una interfaz REST, puede devolver documentos que describen las relaciones entre los recursos mediante la codificación de esas relaciones como enlaces. Por lo tanto, se puede decir que un equipo tiene un recurso de documento ( /team/{id}/players ) que es una lista de enlaces a jugadores ( /player/{id} ) en el equipo, y un jugador puede tener un recurso de documento ( /player/{id}/teams ) que es una lista de enlaces a equipos de los que el jugador es miembro. Agradable y simétrico. Puede realizar las operaciones de mapas en esa lista con bastante facilidad, incluso dando a una relación sus propias ID (posiblemente tendrían dos ID, dependiendo de si está pensando en la relación del equipo primero o del jugador) si eso facilita las cosas . Lo único delicado es que debe recordar eliminar la relación del otro extremo también si la elimina de un extremo, pero manéjela de manera rigurosa utilizando un modelo de datos subyacente y luego haga que la interfaz REST sea una vista de Ese modelo lo va a hacer más fácil.

Las ID de relación probablemente deberían basarse en UUID o algo igualmente largo y aleatorio, independientemente del tipo de ID que uses para equipos y jugadores. Eso le permitirá usar el mismo UUID como componente de ID para cada extremo de la relación sin preocuparse por las colisiones (los enteros pequeños no tienen esa ventaja). Si estas relaciones de membresía tienen propiedades distintas al simple hecho de que relacionan a un jugador y un equipo de manera bidireccional, deben tener su propia identidad que sea independiente tanto de los jugadores como de los equipos; un GET en la vista de equipo del jugador ( /player/{playerID}/teams/{teamID} ) podría hacer un redireccionamiento HTTP a la vista bidireccional ( /memberships/{uuid} ).

Recomiendo escribir enlaces en cualquier documento XML que devuelva (si está produciendo XML, por supuesto) utilizando los atributos XLink xlink:href .


Haga un conjunto separado de /memberships/ recursos.

  1. REST se trata de hacer sistemas evolutivos si nada más. En este momento, es posible que solo le importe que un jugador determinado esté en un equipo determinado, pero en algún momento en el futuro, querrá anotar esa relación con más datos: cuánto tiempo han estado en ese equipo, quién los refirió para ese equipo, quién es su entrenador mientras estuvo en ese equipo, etc., etc.
  2. REST depende del almacenamiento en caché para la eficiencia, lo que requiere cierta consideración para la atomicidad y la invalidación de la memoria caché. Si PUBLICAS una nueva entidad a /teams/3/players/ esa lista se invalidará, pero no quieres que la URL alternativa /players/5/teams/ permanezca en caché. Sí, diferentes cachés tendrán copias de cada lista con diferentes edades, y no hay mucho que podamos hacer al respecto, pero al menos podemos minimizar la confusión para el usuario que publica la actualización al limitar el número de entidades que necesitamos para invalidar en el caché local de sus clientes a uno y solo uno en /memberships/98745 (consulte la discusión de Helland sobre "índices alternativos" en Vida más allá de las transacciones distribuidas para una discusión más detallada).
  3. Puede implementar los 2 puntos anteriores simplemente eligiendo /players/5/teams o /teams/3/players (pero no ambos). Asumamos lo primero. Sin embargo, en algún momento querrá reservar /players/5/teams/ para obtener una lista de las membresías actuales y, sin embargo, poder consultar las membresías pasadas en algún lugar. Cree /players/5/memberships/ una lista de hipervínculos a /memberships/{id}/ resources, y luego puede agregar /players/5/past_memberships/ cuando lo desee, sin tener que romper los marcadores de todos para los recursos de membresía individuales. Este es un concepto general; Estoy seguro de que puede imaginar otros futuros similares que sean más aplicables a su caso específico.

Las respuestas existentes no explican los roles de consistencia e idempotencia, lo que motiva sus recomendaciones de UUIDs / números aleatorios para ID y PUT lugar de POST .

Si consideramos el caso donde tenemos un escenario simple como " Agregar un nuevo jugador a un equipo ", nos encontramos con problemas de consistencia.

Debido a que el jugador no existe, necesitamos:

POST /players { "Name": "Murray" } //=> 302 /players/5 POST /teams/1/players/5

Sin embargo, si la operación del cliente falla después de la POST a /players , hemos creado un jugador que no pertenece a un equipo:

POST /players { "Name": "Murray" } //=> 302 /players/5 // *client failure* // *client retries naively* POST /players { "Name": "Murray" } //=> 302 /players/6 POST /teams/1/players/6

Ahora tenemos un jugador duplicado huérfano en /players/5 .

Para solucionar esto, podemos escribir un código de recuperación personalizado que verifique si hay jugadores huérfanos que coincidan con alguna clave natural (por ejemplo, Name ). Este es un código personalizado que necesita ser probado, cuesta más dinero y tiempo, etc., etc.

Para evitar la necesidad de un código de recuperación personalizado, podemos implementar PUT lugar de POST .

Desde el RFC :

La intención de PUT es idempotente.

Para que una operación sea idempotente, debe excluir datos externos, como las secuencias de identificación generadas por el servidor. Esta es la razón por la que las personas recomiendan PUT y UUID s para los Id . Juntos.

Esto nos permite volver a ejecutar tanto /players PUT como /memberships PUT sin consecuencias:

PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK // *client failure* // *client YOLOs* PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK PUT /teams/1/players/23lkrjrqwlej

Todo está bien y no tuvimos que hacer nada más que volver a intentarlo por fallos parciales.

Esto es más como un addendum a las respuestas existentes, pero espero que las ubique en el contexto de una visión más amplia de cuán flexible y confiable puede ser ReST.


Mi solución preferida es crear tres recursos: Players , Teams y TeamsPlayers .

Entonces, para obtener todos los jugadores de un equipo, solo vaya al recurso Teams y obtenga a todos sus jugadores llamando a GET /Teams/{teamId}/Players .

Por otro lado, para obtener todos los equipos que ha jugado un jugador, obtenga el recurso Teams dentro de los Players . Llama a GET /Players/{playerId}/Teams .

Y, para obtener la relación de muchos a muchos, llame a GET /Players/{playerId}/TeamsPlayers o GET /Teams/{teamId}/TeamsPlayers .

Tenga en cuenta que, en esta solución, cuando llama a GET /Players/{playerId}/Teams , obtiene una variedad de recursos de Teams , que es exactamente el mismo recurso que recibe cuando llama a GET /Teams/{teamId} . El reverso sigue el mismo principio, usted obtiene una variedad de recursos de Players cuando llama a GET /Teams/{teamId}/Players .

En cualquiera de las llamadas, no se devuelve información sobre la relación. Por ejemplo, no se devuelve contractStartDate , porque el recurso devuelto no tiene información sobre la relación, solo sobre su propio recurso.

Para lidiar con la relación nn, llame a GET /Players/{playerId}/TeamsPlayers o GET /Teams/{teamId}/TeamsPlayers . Estas llamadas devuelven el recurso exacto, TeamsPlayers .

Este recurso de TeamsPlayers tiene atributos id , playerId , teamId , así como algunos otros para describir la relación. Además, cuenta con los métodos necesarios para tratar con ellos. GET, POST, PUT, DELETE, etc. que devolverá, incluirá, actualizará, eliminará el recurso de relación.

El recurso TeamsPlayers implementa algunas consultas, como GET /TeamsPlayers?player={playerId} para devolver todas TeamsPlayers relaciones de TeamsPlayers el jugador identificado por {playerId} tiene. Siguiendo la misma idea, use GET /TeamsPlayers?team={teamId} para devolver todos los TeamsPlayers que han jugado en el equipo de {teamId} . En cualquiera de las llamadas GET , se devuelve el recurso TeamsPlayers . Se devuelven todos los datos relacionados con la relación.

Al llamar a GET /Players/{playerId}/Teams (o GET /Teams/{teamId}/Players ), el recurso Players (o Teams ) llama a TeamsPlayers para devolver a los equipos (o jugadores) relacionados mediante un filtro de consulta.

GET /Players/{playerId}/Teams funciona así:

  1. Encuentra todos los TeamsPlayers que el jugador tiene id = playerId . ( GET /TeamsPlayers?player={playerId} )
  2. Loop devuelto TeamsPlayers
  3. Usando el TeamId obtenido de TeamsPlayers , llame a GET /Teams/{teamId} y almacene los datos devueltos
  4. Una vez finalizado el bucle. Devuelve todos los equipos que se pusieron en el bucle.

Puede usar el mismo algoritmo para obtener todos los jugadores de un equipo, al llamar a GET /Teams/{teamId}/Players , pero intercambiar equipos y jugadores.

Mis recursos se verían así:

/api/Teams/1: { id: 1 name: ''Vasco da Gama'', logo: ''/img/Vascao.png'', } /api/Players/10: { id: 10, name: ''Roberto Dinamite'', birth: ''1954-04-13T00:00:00Z'', } /api/TeamsPlayers/100 { id: 100, playerId: 10, teamId: 1, contractStartDate: ''1971-11-25T00:00:00Z'', }

Esta solución se basa únicamente en los recursos REST. Aunque algunas llamadas adicionales pueden ser necesarias para obtener datos de jugadores, equipos o su relación, todos los métodos HTTP se implementan fácilmente. POST, PUT, DELETE son simples y directos.

Cada vez que se crea, actualiza o elimina una relación, los recursos de Players y Teams se actualizan automáticamente.


Sé que hay una respuesta marcada como aceptada para esta pregunta, sin embargo, aquí es cómo podríamos resolver los problemas planteados anteriormente:

Digamos para PUT

PUT /membership/{collection}/{instance}/{collection}/{instance}/

A modo de ejemplo, todos los resultados resultarán en el mismo efecto sin necesidad de sincronización porque se realizan en un solo recurso:

PUT /membership/teams/team1/players/player1/ PUT /membership/players/player1/teams/team1/

Ahora, si queremos actualizar las membresías múltiples para un equipo, podríamos hacer lo siguiente (con las validaciones adecuadas):

PUT /membership/teams/team1/ { membership: [ { teamId: "team1" playerId: "player1" }, { teamId: "team1" playerId: "player2" }, ... ] }


Yo mapearía esa relación con los sub-recursos, el diseño general / transversal sería entonces:

# team resource /teams/{teamId} # players resource /players/{playerId} # teams/players subresource /teams/{teamId}/players/{playerId}

En términos de descanso, ayuda mucho a no pensar en SQL y se une, sino más en colecciones, subcolecciones y recorrido.

Algunos ejemplos:

# getting player 3 who is on team 1 # or simply checking whether player 3 is on that team (200 vs. 404) GET /teams/1/players/3 # getting player 3 who is also on team 3 GET /teams/3/players/3 # adding player 3 also to team 2 PUT /teams/2/players/3 # getting all teams of player 3 GET /players/3/teams # withdraw player 3 from team 1 (appeared drunk before match) DELETE /teams/1/players/3 # team 1 found a replacement, who is not registered in league yet POST /players # from payload you get back the id, now place it officially to team 1 PUT /teams/1/players/44

Como ve, no uso POST para ubicar a los jugadores en equipos, sino PUT, que maneja mejor su relación n: n de jugadores y equipos.