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


Imagina que tienes 2 entidades, Jugadory 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 manejando esto, pero ¿cómo podría 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 de Jugador tendría una lista de Equipos a los que pertenece, y cada objeto de Equipo tendría una lista de jugadores que pertenecen a ella. Por lo tanto, para agregar un Jugador a un Equipo, simplemente publicaría la representación del jugador en un punto final, algo así como POST /player o POST /team con el objeto apropiado como carga útil de la solicitud. Esto me parece el más "relajante", 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 que se me ocurre para hacer esto sería exponer la relación como un recurso por derecho propio. Así que para ver una lista de todos los jugadores en un equipo determinado, puede hacer a GET /playerteam/team/{id} o algo así y obtener una lista de entidades de PlayerTeam. Para agregar un jugador a un equipo, PUBLICA /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?

Author: Boris Burkov, 2011-06-13

7 answers

En una interfaz RESTful, puede devolver documentos que describan las relaciones entre recursos codificando esas relaciones como enlaces. Por lo tanto, se puede decir que un equipo tiene un recurso documental (/team/{id}/players) que es una lista de enlaces a jugadores (/player/{id}) en el equipo, y un jugador puede tener un recurso documental (/player/{id}/teams) que es una lista de enlaces a equipos de los que el jugador es miembro. Bonito y simétrico. Puede mapear las operaciones en esa lista con bastante facilidad, incluso dando a una relación sus propios identificadores (podría decirse que tendrían dos identificaciones, dependiendo de si estás pensando en la relación equipo-primero o jugador-primero) si eso hace las cosas más fáciles. La única parte difícil es que tienes que recordar eliminar la relación desde el otro extremo, así si se elimina de un extremo, pero el manejo riguroso de esto mediante el uso de un modelo de datos subyacente y luego tener la interfaz de RESTO ser una vista de ese modelo va a hacer que sea más fácil.

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

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

 103
Author: Donal Fellows,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-13 08:30:15

Crea un conjunto separado de recursos /memberships/.

  1. El RESTO 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 remitió a ese equipo, quién es/fue su entrenador mientras estuvo en ese equipo, etc, etc.
  2. El RESTO depende del almacenamiento en caché para la eficiencia, lo que requiere algunos consideración de atomicidad e invalidación de caché. Si publica una nueva entidad en /teams/3/players/ esa lista será invalidada, pero no desea 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 que el usuario publique la actualización limitando el número de entidades que necesitamos invalidar en la caché local de su cliente a uno y solo uno en /memberships/98745 (véase la discusión de Helland sobre "índices alternativos" en Life beyond Distributed Transactions 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. En algún momento, sin embargo, querrá reservar /players/5/teams/ para una lista de membresías actuales, y aún así poder referirse a membresías pasadas en algún lugar. Haga /players/5/memberships/ una lista de hipervínculos a recursos /memberships/{id}/, y luego puede agregue /players/5/past_memberships/ cuando quiera, sin tener que romper los marcadores de todos los recursos de membresía individuales. Este es un concepto general; estoy seguro de que puede imaginar otros futuros similares que son más aplicables a su caso específico.
 221
Author: fumanchu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-31 15:34:43

Mapearía tal relación con sub-recursos, diseño general / recorrido sería entonces:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

En términos Restful, ayuda mucho a no pensar en SQL y joins, sino más bien en colecciones, subcolecciones y traversal.

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 ves, no uso POST para colocar jugadores en equipos, sino PUT, que maneja mejor tu relación n:n de jugadores y equipos.

 50
Author: manuel aldana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-13 08:22:31

Las respuestas existentes no explican los roles de consistencia e idempotencia - que motivan sus recomendaciones de UUIDs/números aleatorios para IDs y PUT en lugar de POST.

Si consideramos el caso en el que tenemos un escenario simple como " Añadir 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 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 arreglar esto, podríamos 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 código de recuperación personalizado, podemos implementar PUT en lugar de POST.

De la RFC:

La intención de PUT es idempotente

Para que una operación sea idempotente, necesita excluir datos externos como secuencias de id generadas por el servidor. Esta es la razón por la que la gente está recomendando PUT y UUIDs para Ids juntos.

Esto nos permite volver a ejecutar ambos /players PUT y el /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 necesitábamos hacer nada más que reintentar fallos parciales.

Esto es más bien una adición a la las respuestas existentes, pero espero que las coloque en el contexto de la imagen más amplia de cuán flexible y confiable puede ser el descanso.

 11
Author: Seth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-06 03:44:37

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

Por lo tanto, para obtener todos los jugadores de un equipo, solo tienes que ir a Teams recurso y obtener todos sus jugadores llamando GET /Teams/{teamId}/Players.

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

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

Tenga en cuenta que, en esta solución, cuando se llama GET /Players/{playerId}/Teams, se obtiene una matriz de Teams recursos, que es exactamente el mismo recurso que se obtiene cuando se llama GET /Teams/{teamId}. Lo contrario sigue el mismo principio, se obtiene una matriz de recursos Players cuando se llama 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 tratar con la relación n-n, llame a GET /Players/{playerId}/TeamsPlayers o GET /Teams/{teamId}/TeamsPlayers. Estas llamadas devuelven exactamente el recurso, TeamsPlayers.

Este TeamsPlayers recurso ha id, playerId, teamId atributos, así como algunos otros, para describir la relación. Además, tiene los métodos necesarios para lidiar 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 las relaciones TeamsPlayers que tiene el jugador identificado por {playerId}. Siguiendo la misma idea, utilice GET /TeamsPlayers?team={teamId} para devolver todos los TeamsPlayers que han jugado en el equipo {teamId}. En cualquier llamada 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 los equipos (o jugadores) relacionados utilizando un filtro de consulta.

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

  1. Encontrar todos los TeamsPlayers que jugador tiene id = playerId. (GET /TeamsPlayers?player={playerId})
  2. Bucle el devuelto Jugadores de equipo
  3. Usando el teamId obtenido de TeamsPlayers , llama a GET /Teams/{teamId} y almacena los datos devueltos
  4. Después de que termine el bucle. Devuelva todos los equipos que se pusieron en el bucle.

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

Mis recursos se verían así: {[45]]}

/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 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 Players y Teams se actualizan automáticamente.

 3
Author: Haroldo Macedo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-08 20:52:38

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 que para PUT

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

Como ejemplo, los siguientes resultados tendrán 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 varias membresías 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"
        },
        ...
    ]
}
 1
Author: Heidar Pirzadeh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-09 03:15:15
  1. /players (es un recurso maestro)
  2. / teams / {id} / players (es un recurso de relación, por lo que reacciona diferente que 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

 -3
Author: MoaLaiSkirulais,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-21 05:05:10