Parte II · Conectar datos
Capítulo 4 — Un jugador, muchas cartas (relación 1:N)
Hasta ahora tus tablas vivían solas. La tabla jugador por su lado, la tabla
carta por el suyo. En el capítulo anterior aprendiste a hacerles preguntas,
pero siempre a una tabla cada vez: dame las cartas raras, ordéname los jugadores
por antigüedad. Cada tabla, en su mundo.
El problema es que un juego de verdad no funciona así. En Kriaturas, un jugador arma mazos: un grupo de cartas elegidas para jugar. Y un mazo no significa nada suelto. "Mazo agresivo" ¿de quién es? Si no lo sabes, es solo un nombre flotando en el vacío. Un mazo necesita estar atado a su dueño.
En este capítulo vas a conectar tu primera pareja de tablas. Al acabar sabrás qué es una relación uno a muchos, cómo se usa una clave foránea para atar una tabla a otra, y por qué la base de datos no te dejará crear un mazo fantasma sin dueño. Es el primer paso para que tus datos dejen de ser islas.
Dos tablas que se necesitan
Vamos a partir de lo que ya tienes. La tabla jugador, con tu reparto de
siempre:
| id | alias | fecha_alta | |
|---|---|---|---|
| 1 | DragoFuego99 | drago@ejemplo.com | 2026-03-01 |
| 2 | LunaVerde | luna@ejemplo.com | 2026-03-04 |
| 3 | PixelPunk | pixel@ejemplo.com | 2026-03-10 |
| 4 | ToxiRana | toxi@ejemplo.com | 2026-03-15 |
Ahora queremos guardar los mazos. Cada jugador puede tener varios: uno agresivo para partidas rápidas, otro defensivo, otro de pura colección. Y al revés, cada mazo pertenece a un solo jugador. No existe un mazo compartido entre dos personas.
Fíjate bien en esa frase, porque encierra todo el capítulo:
Un jugador tiene muchos mazos, pero cada mazo es de un jugador.
Eso es una relación uno a muchos, que se escribe 1:N (se lee "uno a ene", donde la "ene" significa "muchos, una cantidad cualquiera"). Es el tipo de conexión más común entre tablas. La vas a encontrar por todas partes: un país tiene muchas ciudades, un canal tiene muchos vídeos, un jugador tiene muchas partidas. Siempre el mismo patrón: por un lado uno, por el otro muchos.
El hilo que las une: la clave foránea
Ya sabes desde el capítulo 2 que cada tabla tiene una clave primaria: una
columna que identifica cada fila sin repetirse. En jugador es id: el jugador
1 es DragoFuego99 y nadie más en el mundo es el jugador 1.
Pues la idea es sencilla. Para decir de quién es un mazo, en la tabla mazo
añadimos una columna que guarde el id del jugador dueño. La vamos a llamar
jugador_id. Si un mazo tiene jugador_id = 1, ese mazo es de DragoFuego99,
porque el jugador con id = 1 es él.
Esa columna, jugador_id, es una clave foránea (en inglés, foreign key).
Una clave foránea es una columna de una tabla que apunta a la clave primaria
de otra tabla. No es un dato nuevo del mazo: es una referencia, una flecha que
dice "para saber más de mi dueño, ve a buscar este id en la tabla jugador".
Visualízalo así:
tabla mazo tabla jugador ┌────┬──────────────┬────────────┐ ┌────┬──────────────┐ │ id │ nombre │ jugador_id │ │ id │ alias │ ├────┼──────────────┼────────────┤ ├────┼──────────────┤ │ 1 │ Mazo eléctrico│ 1 ────┼──▶│ 1 │ DragoFuego99 │ │ 2 │ Mazo agresivo │ 1 ────┼──▶│ 1 │ ... │ │ 3 │ Mazo del bosque│ 2 ────┼──▶│ 2 │ LunaVerde │ └────┴──────────────┴────────────┘ └────┴──────────────┘
Los dos primeros mazos apuntan al jugador 1; el tercero, al jugador 2. El dato del jugador (su alias, su email) está guardado una sola vez, en su tabla. El mazo solo guarda el número que lo conecta. Nada repetido, nada que se pueda contradecir.
Una distinción que conviene tener clara desde ya, porque se confunde mucho:
- La clave primaria identifica una fila dentro de su propia tabla
(
jugador.ididentifica a cada jugador). - La clave foránea apunta desde una tabla a la clave primaria de otra
(
mazo.jugador_idapunta ajugador.id).
Una identifica; la otra señala. Misma idea de fondo —un número que representa a una fila—, pero papeles distintos.
Crear la tabla con su clave foránea
Vamos a crear la tabla mazo. Es como cualquier CREATE TABLE de los que ya
hiciste en el capítulo 2, con una línea extra al final que declara la clave
foránea:
CREATE TABLE mazo (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
jugador_id INTEGER,
FOREIGN KEY (jugador_id) REFERENCES jugador(id)
);
Léelo despacio, que cada línea cuenta una cosa:
id INTEGER PRIMARY KEY: cada mazo tiene su propio identificador. Esto es la clave primaria demazo, igual que ya viste enjugadorycarta.nombre VARCHAR(40): el nombre del mazo, un texto ("Mazo agresivo").jugador_id INTEGER: aquí guardamos eliddel jugador dueño. Es un número, porque las claves primarias dejugadorson números.FOREIGN KEY (jugador_id) REFERENCES jugador(id): esta es la línea mágica. Le dice a la base de datos: "la columnajugador_idno es un número cualquiera; es una referencia (REFERENCES) a la columnaidde la tablajugador". A partir de aquí, la base de datos vigilará que ese número siempre corresponda a un jugador que existe de verdad.
Esa última línea es la que convierte una columna normal en una clave foránea de
verdad. Sin ella, jugador_id sería solo un número suelto, sin ningún
significado especial para la base de datos.
Meter mazos de verdad
Con la tabla creada, vamos a darle mazos a nuestros jugadores. Recuerda: en
jugador_id ponemos el id del dueño. DragoFuego99 es el jugador 1, así que
sus mazos llevan jugador_id = 1:
INSERT INTO mazo (id, nombre, jugador_id) VALUES (1, 'Mazo eléctrico', 1);
INSERT INTO mazo (id, nombre, jugador_id) VALUES (2, 'Mazo agresivo', 1);
INSERT INTO mazo (id, nombre, jugador_id) VALUES (3, 'Mazo del bosque', 2);
INSERT INTO mazo (id, nombre, jugador_id) VALUES (4, 'Mazo control', 3);
La tabla mazo queda así:
| id | nombre | jugador_id |
|---|---|---|
| 1 | Mazo eléctrico | 1 |
| 2 | Mazo agresivo | 1 |
| 3 | Mazo del bosque | 2 |
| 4 | Mazo control | 3 |
Mira la columna jugador_id y léela como flechas hacia la tabla de jugadores.
DragoFuego99 (el 1) tiene dos mazos: el eléctrico y el agresivo. LunaVerde (la 2)
tiene uno. PixelPunk (el 3), otro. ToxiRana (la 4) todavía no tiene ninguno, y no
pasa nada: la relación 1:N permite que un jugador tenga muchos mazos, y "muchos"
incluye también "ninguno".
Esa última idea sorprende al principio, pero tiene sentido. El "muchos" del 1:N no obliga a nada: simplemente deja la puerta abierta a que haya varios. Cero, uno o cien, todos valen.
La base de datos vigila: integridad referencial
Aquí viene lo interesante, y la verdadera razón por la que declaramos la clave
foránea con esa línea FOREIGN KEY. A partir de ese momento, la base de datos
no te deja mentir.
La regla se llama integridad referencial, y dice algo muy simple: el valor de
una clave foránea tiene que existir en la tabla a la que apunta. Es decir, todo
jugador_id que pongas en mazo tiene que corresponder a un jugador que exista
de verdad en la tabla jugador.
¿Qué pasa si lo intentas saltar? Imagina que intentas crear un mazo del jugador 99, que no existe:
INSERT INTO mazo (id, nombre, jugador_id) VALUES (5, 'Mazo fantasma', 99);
La base de datos lo rechaza y te da un error. No hay ningún jugador con
id = 99, así que ese mazo no puede pertenecer a nadie. Y un mazo huérfano, sin
dueño real, es exactamente el tipo de basura que arruina los datos de un juego.
La base de datos te protege de tus propios errores.
Esto es enorme, y vale la pena pararse a apreciarlo. Antes, tus tablas eran
listas de datos que tú tenías que mantener a mano, con todo el cuidado del mundo
para no equivocarte. Ahora, con la clave foránea declarada, las tablas dejan de
poder mentirse entre ellas. La conexión entre mazo y jugador ya no es una
buena intención tuya: es una regla que la base de datos hace cumplir
automáticamente, en cada inserción, sin que tengas que acordarte. Le has
delegado la vigilancia.
¿Y si borro un jugador que tiene mazos?
Hay una pregunta que surge sola en cuanto entiendes la integridad referencial. Si cada mazo tiene que apuntar a un jugador que existe… ¿qué pasa si borro un jugador que todavía tiene mazos? Sus mazos se quedarían apuntando a alguien que ya no está. Eso rompería la regla.
La base de datos no se queda callada ante ese dilema: te deja elegir de antemano qué hacer. Hay tres políticas, y las dos primeras son las que de verdad vas a usar:
Restringir (en inglés, RESTRICT o NO ACTION). La opción más prudente, y
la que suele venir por defecto. La base de datos simplemente no te deja borrar
el jugador mientras tenga mazos. Si quieres borrarlo, primero tienes que ocuparte
de sus mazos (borrarlos o reasignarlos). Es como un seguro: te obliga a pararte y
pensar antes de hacer un destrozo.
En cascada (en inglés, CASCADE). Aquí le dices a la base de datos: "si
borro un jugador, borra también todos sus mazos". El borrado se propaga del
padre (el jugador) a los hijos (sus mazos), como una fila de fichas de dominó que
se van cayendo. Tiene sentido cuando los datos hijos no significan nada sin el
padre: si el jugador se va, sus mazos no le importan a nadie.
La tercera política, anular (SET NULL), deja los mazos sin dueño poniendo
su jugador_id a nulo (vacío). Solo sirve si esa columna admite valores vacíos y
se usa menos; quédate con que existe y sigue.
¿Cuál elegir? Como regla de principiante: restringir por defecto, porque es la más segura y te avisa antes de que pase algo irreversible. Reserva cascada para cuando no te quede ninguna duda de que los hijos deben morir con el padre. Borrar datos en cascada sin pensarlo es una forma estupenda de cargarte medio juego con una sola orden.
Resumen
En este capítulo conectaste tus dos primeras tablas:
- Una relación 1:N (uno a muchos) une dos tablas cuando un lado tiene muchos del otro: un jugador tiene muchos mazos, pero cada mazo es de un solo jugador.
- Para representarla, en la tabla del lado "muchos" (
mazo) añades una clave foránea (foreign key): una columna (jugador_id) que apunta a la clave primaria de la otra tabla (jugador.id). Así no repites datos: el jugador vive una sola vez en su tabla. - Se declara con
FOREIGN KEY (jugador_id) REFERENCES jugador(id)al crear la tabla. - La integridad referencial es la regla que la base de datos hace cumplir sola: toda clave foránea debe apuntar a una fila que exista. No puede haber mazos huérfanos.
- Al borrar el "padre" puedes elegir política: restringir (no deja borrar si tiene hijos, la opción segura) o en cascada (borra también los hijos).
Con esto, tus tablas ya no son islas: empiezan a hablar entre ellas.
Pero el 1:N solo cubre la mitad de la historia. Acabamos de resolver "un jugador, muchos mazos". ¿Y la colección? Una carta como Chispín la tienen muchos jugadores a la vez; y cada jugador, a su vez, tiene muchas cartas distintas. Muchos por un lado y muchos por el otro. Eso ya no es uno a muchos: es muchos a muchos, y no se resuelve con una sola clave foránea. Necesita un truco nuevo. Es justo lo que aprenderás en el próximo capítulo.