Parte II · Conectar datos
Capítulo 5 — La colección (relación N:M)
En el capítulo anterior conectaste tu primera pareja de tablas. Un jugador tiene
muchos mazos, pero cada mazo es de un solo jugador. Lo resolviste con una clave
foránea: una columna en mazo que apunta al dueño. Limpio y sencillo.
Pero hay una parte de Kriaturas que esa solución no alcanza, y es justo la más importante del juego: la colección. Las cartas que tiene cada jugador.
Piénsalo un momento. DragoFuego99 tiene muchas cartas: Chispín, Flamita, una Poción… Hasta aquí parece un 1:N más, como los mazos. Pero dale la vuelta. La carta Chispín, ¿de quién es? No es de DragoFuego99 y de nadie más. La tienen LunaVerde, PixelPunk y miles de jugadores a la vez. Una misma carta, en muchas colecciones.
Así que tenemos muchos por los dos lados: un jugador tiene muchas cartas, y
una carta la tienen muchos jugadores. Eso ya no es uno a muchos. Es muchos a
muchos, y la clave foránea de un solo lado se queda corta. En este capítulo
vas a aprender el truco que lo resuelve, y de paso a hacer tu primer JOIN: la
orden que vuelve a juntar tablas que habías separado.
El truco: una tabla en medio
Vamos a pensarlo despacio, porque esta idea la vas a usar el resto de tu vida con las bases de datos.
Tienes dos tablas, jugador y carta, cada una con su clave primaria. Lo que
quieres guardar no es un dato del jugador ni un dato de la carta. Es un dato del
vínculo entre los dos: el hecho de que "el jugador 1 tiene la carta 2". Ese
hecho no cabe en ninguna de las dos tablas, porque no pertenece a una sola: vive
justo en medio.
Así que le damos su propia tabla. Una tabla intermedia (también la llaman
tabla de cruce): una tabla cuyo trabajo es conectar las otras dos. Cada fila de
esta tabla es una pareja. La vamos a llamar coleccion:
| jugador_id | carta_id | cantidad |
|---|---|---|
| 1 | 1 | 3 |
| 1 | 2 | 1 |
| 1 | 6 | 1 |
| 2 | 3 | 2 |
Léela como una lista de hechos. La primera fila dice: "el jugador 1 tiene la carta 1, y tiene 3 copias". La segunda: "el jugador 1 tiene la carta 2, 1 copia". Y así. Recordando el reparto de siempre, el jugador 1 es DragoFuego99 y la carta 1 es Chispín, así que esa primera fila significa: DragoFuego99 tiene 3 copias de Chispín.
Fíjate en las dos primeras columnas. jugador_id apunta a la tabla jugador,
exactamente como la clave foránea del capítulo 4. Y carta_id apunta a la tabla
carta, de la misma manera. La tabla intermedia tiene dos claves foráneas,
una hacia cada lado:
jugador coleccion carta
┌────┬────────┐ ┌──────────┬─────────┐ ┌────┬──────────┐
│ id │ alias │ │jugador_id│carta_id │ │ id │ nombre │
├────┼────────┤ ├──────────┼─────────┤ ├────┼──────────┤
│ 1 │ Drago… │◀──┼─ 1 │ 1 ─┼──▶│ 1 │ Chispín │
│ 2 │ Luna… │ │ 1 │ 2 ─┼──▶│ 2 │ Flamita │
└────┴────────┘ │ 1 │ 6 ─┼──▶│ 6 │ Poción │
│ 2 ──┼─────────┼─▶ ...
└──────────┴─────────┘
Cada fila de coleccion toca las dos tablas a la vez: con la izquierda señala a
un jugador, con la derecha a una carta. Y aquí está lo bonito: con esta tabla en
medio, los dos "muchos" caben sin problema. ¿DragoFuego99 tiene cinco cartas
distintas? Pues cinco filas en coleccion con jugador_id = 1. ¿La carta
Chispín la tienen mil jugadores? Pues mil filas con carta_id = 1. Muchos por un
lado, muchos por el otro, y ni un solo dato repetido: el alias del jugador sigue
viviendo una sola vez en su tabla, y el nombre de la carta una sola vez en la
suya.
Ese es el truco. Una relación N:M (muchos a muchos) no se guarda con una clave foránea, sino con una tabla intermedia que recoge las parejas.
Crear la tabla intermedia
Vamos a crearla de verdad. Es un CREATE TABLE como los que ya conoces, pero con
un par de novedades que merecen explicación:
CREATE TABLE coleccion (
jugador_id INTEGER,
carta_id INTEGER,
cantidad INTEGER,
PRIMARY KEY (jugador_id, carta_id),
FOREIGN KEY (jugador_id) REFERENCES jugador(id),
FOREIGN KEY (carta_id) REFERENCES carta(id)
);
Vamos línea a línea:
jugador_id INTEGERycarta_id INTEGER: las dos columnas que forman la pareja. Cada una guarda unid, una haciajugadory otra haciacarta.cantidad INTEGER: cuántas copias de esa carta tiene ese jugador. Volvemos a esto en un momento; es un detalle que vale oro.PRIMARY KEY (jugador_id, carta_id): aquí hay algo nuevo. La clave primaria no es una columna, sino dos a la vez.- Las dos líneas
FOREIGN KEY: declaran quejugador_idapunta ajugador.idycarta_idacarta.id. Igual que en el capítulo 4, pero por partida doble. A partir de aquí la base de datos vigila las dos: no podrás meter encoleccionni un jugador ni una carta que no existan.
Parémonos en la clave primaria, que es la primera vez que la ves así.
Una clave hecha de dos columnas
En el capítulo 2 te dejé caer una idea "para saber más": que la clave primaria puede estar formada por varias columnas a la vez. Ese momento ha llegado.
Piensa qué identifica una fila de coleccion. ¿Sirve el jugador_id solo? No:
DragoFuego99 aparece en varias filas (tiene varias cartas). ¿Sirve el carta_id
solo? Tampoco: Chispín aparece en varias filas (la tienen varios jugadores). Por
separado, ninguno de los dos identifica una fila sin repetirse.
Pero juntos sí. La pareja (jugador_id, carta_id) no se repite nunca: no
tiene sentido anotar dos veces que el jugador 1 tiene la carta 1. O la tiene o no
la tiene. Por eso la clave primaria es la pareja.
Eso es una clave primaria compuesta: una clave primaria formada por dos o más
columnas, que solo es única cuando se miran todas a la vez. Y de regalo te impide
un error: como la base de datos no permite repetir la clave primaria, no podrás
anotar dos veces la misma pareja jugador-carta. Si DragoFuego99 consigue otra
Chispín, no añades una fila nueva: subes su cantidad de 3 a 4. Que es,
precisamente, para lo que está esa columna.
El dato que vive en el vínculo
Esa columna cantidad esconde una idea elegante. ¿Dónde guardas cuántas copias
de Chispín tiene DragoFuego99? No es un dato del jugador (no es "suyo", cambia
según la carta). No es un dato de la carta (no es "suyo", cambia según el
jugador). Es un dato de la relación entre los dos: de esa pareja concreta.
Por eso vive en la tabla intermedia. Las tablas de cruce pueden tener sus propios
atributos, y son justo los datos que solo tienen sentido para la pareja. Aquí es
cantidad. En otro juego podría ser la fecha en que conseguiste la carta, o si
la tienes marcada como favorita. Cuando un dato no sabes en qué tabla meterlo
porque "depende de las dos", casi siempre la respuesta es la tabla intermedia.
Llenar la colección
Con la tabla creada, vamos a anotar las cartas de nuestros jugadores. Recuerda el reparto: el jugador 1 es DragoFuego99, el 2 es LunaVerde; la carta 1 es Chispín, la 2 Flamita, la 3 Burbujo, la 6 Poción.
INSERT INTO coleccion (jugador_id, carta_id, cantidad) VALUES (1, 1, 3);
INSERT INTO coleccion (jugador_id, carta_id, cantidad) VALUES (1, 2, 1);
INSERT INTO coleccion (jugador_id, carta_id, cantidad) VALUES (1, 6, 1);
INSERT INTO coleccion (jugador_id, carta_id, cantidad) VALUES (2, 3, 2);
INSERT INTO coleccion (jugador_id, carta_id, cantidad) VALUES (2, 4, 1);
Léelo como frases: DragoFuego99 tiene 3 Chispín, 1 Flamita y 1 Poción; LunaVerde
tiene 2 Burbujo y 1 Hojarasca. La tabla coleccion queda así:
| jugador_id | carta_id | cantidad |
|---|---|---|
| 1 | 1 | 3 |
| 1 | 2 | 1 |
| 1 | 6 | 1 |
| 2 | 3 | 2 |
| 2 | 4 | 1 |
Mira la tabla y date cuenta de lo que tienes delante. Solo números. No hay ni un
alias ni un nombre de carta a la vista, y aun así está todo: cada fila conecta un
jugador real con una carta real a través de sus id. Es eficiente y no repite
nada. Pero también es ilegible para una persona: nadie quiere leer "1 tiene 1".
Para volver a ver alias y nombres, necesitamos juntar las tablas otra vez. Y para
eso existe el JOIN.
El reencuentro: tu primer JOIN
Aquí conviene parar y mirar lo que llevas hecho en el libro, porque hay una tensión interesante.
Llevas cinco capítulos separando datos. Sacaste los jugadores a su tabla, las cartas a la suya, los mazos a la suya. Todo para no repetir nada y no contradecirte. Y está bien: es la base de un buen diseño.
Pero ahora pagas el precio. La información está repartida. Si quieres responder
"¿qué cartas tiene DragoFuego99, con sus nombres?", el dato no está en ninguna
tabla sola: el alias está en jugador, los nombres están en carta, y quién
tiene qué está en coleccion. La respuesta vive en las tres a la vez.
El JOIN (combinación) es la orden que las vuelve a juntar. Si separar fue
guardar las piezas en cajones distintos para tenerlas ordenadas, el JOIN es
abrir los cajones y recomponer la figura cuando la necesitas. Separamos para
guardar; juntamos para preguntar.
Vamos a responder esa pregunta: las cartas de DragoFuego99, con sus nombres.
SELECT jugador.alias, carta.nombre, coleccion.cantidad
FROM jugador
JOIN coleccion ON jugador.id = coleccion.jugador_id
JOIN carta ON carta.id = coleccion.carta_id
WHERE jugador.alias = 'DragoFuego99';
Parece mucho de golpe, pero cada trozo es una idea que ya conoces. Vamos despacio:
SELECT jugador.alias, carta.nombre, coleccion.cantidad: las columnas que quieres ver. Como ahora hay varias tablas en juego, escribimostabla.columnapara que no haya dudas de cuál es cuál.FROM jugador: empezamos por la tabla de jugadores.JOIN coleccion ON jugador.id = coleccion.jugador_id: pega la tablacoleccion, emparejando cada jugador con sus filas de colección. ElONes la parte importante: dice cómo se conectan las dos tablas. "Une la fila del jugador con las filas de colección donde eljugador_idcoincida con suid."JOIN carta ON carta.id = coleccion.carta_id: y ahora pega la tablacarta, emparejando cada fila de colección con su carta por elcarta_id.WHERE jugador.alias = 'DragoFuego99': el filtro de siempre, el del capítulo 3. De todo lo que ha juntado, quédate solo con las filas de DragoFuego99.
El resultado es exactamente lo que querías:
| alias | nombre | cantidad |
|---|---|---|
| DragoFuego99 | Chispín | 3 |
| DragoFuego99 | Flamita | 1 |
| DragoFuego99 | Poción | 1 |
Léelo y disfrútalo un segundo: alias de verdad, nombres de verdad, cantidades de
verdad. La base de datos ha ido a tres tablas distintas, ha seguido los hilos de
los id y ha recompuesto la foto. Tú solo dijiste qué querías y cómo se conectan
las tablas; el trabajo de cruzarlas lo hizo ella.
Esa es la pareja de ideas con la que se construyen las bases de datos de verdad:
separas los datos para guardarlos bien, y los juntas con JOIN para
preguntarles cosas.
Una nota sobre estilos. Verás SQL antiguo que hace los
JOINde otra manera: listando las tablas separadas por comas y poniendo las conexiones en elWHERE(FROM jugador, coleccion, carta WHERE jugador.id = coleccion.jugador_id AND …). Funciona y hace lo mismo, pero mezcla las conexiones con los filtros y se lía enseguida. ElJOIN ... ONsepara cada cosa en su sitio y se lee mucho mejor. Usa este; es el recomendado hoy.
Resumen
En este capítulo modelaste la parte más viva de Kriaturas, la colección, y aprendiste a juntar tablas:
- Una relación N:M (muchos a muchos) aparece cuando los dos lados tienen muchos del otro: un jugador tiene muchas cartas y una carta la tienen muchos jugadores. La clave foránea de un solo lado, la del capítulo 4, no basta.
- Se resuelve con una tabla intermedia (o de cruce): una tabla nueva cuyas filas son las parejas. Lleva dos claves foráneas, una hacia cada tabla, y su clave primaria suele ser la pareja de ambas (una clave primaria compuesta).
- La tabla intermedia puede tener atributos propios del vínculo, como
cantidad: datos que no son de una tabla ni de la otra, sino de la relación. - Para volver a unir lo que separaste, usas el
JOIN. ConJOIN ... ONle dices a la base de datos qué tablas juntar y por qué columnas se conectan; ella recompone la información repartida. - Un
JOINsin condición de unión produce un producto cartesiano: todas las combinaciones posibles. Acuérdate delON.
Con esto tu modelo ya tiene varias tablas hablando entre sí: jugador, carta,
mazo, coleccion y mazo_carta. Empieza a ser un sistema de verdad… y también
empieza a ser difícil de tener entero en la cabeza. Antes de seguir añadiendo
piezas, conviene parar y dibujarlo: ver de un vistazo qué entidades hay y cómo
se conectan. Eso es el diseño conceptual, y es justo lo que harás en el próximo
capítulo.