KriaturasBases de datos para quien empieza

Parte III · Diseñar bien

Capítulo 7 — Familias de cartas (jerarquías)

En el capítulo anterior diste un paso atrás y dibujaste el plano de Kriaturas. Quedó claro y limpio: jugadores, cartas, mazos y las líneas que los conectan. Pero ese plano escondía una mentira piadosa. Mira la tabla carta que vienes arrastrando desde el capítulo 2:

id nombre rareza coste
1 Chispín común 2
2 Flamita común 2
5 Entreno intensivo común 1
6 Poción común 1

A primera vista, cuatro cartas iguales. Pero no lo son, y tú lo sabes. Chispín es una criatura: ataca, se defiende, tiene vida y pertenece a un elemento. Entreno intensivo no ataca a nadie: es una carta de entrenamiento que aplica un efecto durante unos turnos. Poción es un objeto que se usa y se gasta. Tres cosas muy distintas… metidas en la misma caja, como si fueran lo mismo.

Hasta ahora lo dejamos pasar a propósito, para no complicarte la vida. Pero el juego no avanza si una criatura no sabe cuánta vida tiene. Ha llegado el momento de modelar que estas cartas son parientes pero distintas. De eso va este capítulo.

Al acabar sabrás reconocer cuándo varias entidades forman una familia, qué es una jerarquía y, sobre todo, las tres formas de llevarla a tablas y cómo elegir entre ellas con criterio.


Cosas que se parecen, pero no son iguales

Empecemos por la idea, sin tablas todavía. Piensa en las cartas de Kriaturas y hazte dos preguntas.

¿Qué tienen en común todas las cartas? Todas tienen un nombre, una rareza y un coste para jugarlas. Da igual que sea una criatura o una poción: esos tres datos los tiene cualquier carta.

¿En qué se diferencian? Aquí cada tipo va por su lado:

  • Una criatura tiene elemento (fuego, agua, planta, eléctrico…), ataque, defensa y vida.
  • Un entrenamiento tiene un efecto y una duracion (cuántos turnos dura).
  • Un objeto tiene un efecto y un dato de si es consumible (si se gasta al usarlo o no).

Cuando varias entidades comparten una parte común pero cada una tiene rasgos propios que te interesa guardar, tienes una jerarquía: una familia con un tipo general arriba y tipos específicos debajo.

Al tipo general se le llama superclase. En nuestro caso es carta, con lo que tienen todas: nombre, rareza, coste. A los tipos específicos se les llama subclases: criatura, entrenamiento y objeto, cada uno con sus atributos propios. Y la idea que lo une todo es la herencia: cada subclase "hereda" los atributos comunes de la superclase y les añade los suyos. Una criatura es una carta (tiene nombre, rareza y coste) y además tiene ataque, defensa y vida.

A esto, en los libros, lo llaman generalización/especialización, según desde dónde lo mires. Si subes de las criaturas, entrenamientos y objetos hacia el concepto común "carta", estás generalizando. Si bajas de "carta" hacia cada tipo concreto, estás especializando. Es el mismo dibujo leído en dos sentidos; no te agobies con los dos nombres, describen la misma familia.

Antes de dibujarla, dos matices rápidos que conviene tener en el radar, sin liarse con ellos:

  • La especialización puede ser total (toda carta es de uno de los tres tipos, no hay cartas "sueltas") o parcial (podría haber cartas que no caen en ninguna subclase).
  • Las subclases pueden ser disjuntas (una carta es de un solo tipo) o solapadas (una misma carta podría ser de varios).

En Kriaturas lo tenemos fácil: total y disjunta. Cada carta es criatura o entrenamiento o objeto, una y solo una. Lo decimos y seguimos; esto basta para casi todo.

El plano de la familia

Como en el capítulo anterior, dibujemos la jerarquía antes de tocar tablas. Primero en cajas y líneas, que se lee en cualquier sitio:

                 ┌───────────────────────┐
                 │         carta          │   (superclase)
                 │  id, nombre, rareza,   │
                 │         coste          │
                 └───────────┬───────────┘
                             │  es un / es una
              ┌──────────────┼──────────────┐
              │              │              │
   ┌──────────┴───────┐ ┌────┴──────────┐ ┌─┴────────────────┐
   │     criatura     │ │ entrenamiento │ │      objeto       │
   │ elemento, ataque │ │ efecto,       │ │ efecto,          │
   │ defensa, vida    │ │ duracion      │ │ consumible        │
   └──────────────────┘ └───────────────┘ └──────────────────┘

La línea que baja de carta a sus tres tipos es la herencia: se lee "una criatura es una carta", "un objeto es una carta". Y la misma idea en Mermaid, que las herramientas web renderizan como dibujo:

Fíjate en que aquí usamos un diagrama de clases (la flecha hueca ◁ significa "hereda de"), no el erDiagram de entidades del capítulo 6. Es a propósito: una jerarquía no es una relación normal entre dos entidades, es una familia. El dibujo de clases lo deja más claro y, de paso, es el mismo que verás cuando programes con clases que heredan.

Tienes el plano. Pero un plano no se guarda: las tablas sí. Y aquí viene lo bueno, porque esta familia se puede convertir en tablas de tres maneras distintas.

De la jerarquía a las tablas: tres caminos

No hay una única forma "correcta" de guardar una jerarquía. Hay tres clásicas, y cada una tiene su precio. Vamos con las tres usando las mismas cartas de siempre, para que veas la diferencia con datos reales.

Camino 1: una sola tabla para toda la familia

La idea más directa: una única tabla carta con todas las columnas posibles, las comunes y las de cada subclase juntas. Añadimos una columna tipo para saber qué es cada fila.

SQL
CREATE TABLE carta (
    id         INTEGER       PRIMARY KEY,
    nombre     VARCHAR(40),
    rareza     VARCHAR(15),
    coste      INTEGER,
    tipo       VARCHAR(15),   -- 'criatura', 'entrenamiento' u 'objeto'
    -- atributos de criatura:
    elemento   VARCHAR(15),
    ataque     INTEGER,
    defensa    INTEGER,
    vida       INTEGER,
    -- atributos de entrenamiento y objeto:
    efecto     VARCHAR(100),
    duracion   INTEGER,
    consumible BOOLEAN
);

Metemos las cartas de siempre y mira lo que pasa:

id nombre tipo elemento ataque defensa vida efecto duracion consumible
1 Chispín criatura eléctrico 30 20 50
5 Entreno intensivo entrenamiento +10 ataque 2
6 Poción objeto cura 20 vida

¿Ves el problema? La tabla está llena de huecos. Chispín no usa efecto, duracion ni consumible. La Poción no usa elemento, ataque, defensa ni vida. Cada fila deja vacías las columnas que no le tocan.

A cambio, es la más sencilla de todas: una tabla, una consulta, nada de juntar nada. Para una familia con pocos atributos propios puede valer perfectamente. Para una con muchos, acabas con una tabla ancha y agujereada.

Camino 2: una tabla por subclase

El extremo opuesto: ninguna tabla carta, sino una tabla por cada tipo. Cada una con sus atributos propios y una copia de los comunes (nombre, rareza, coste).

SQL
CREATE TABLE criatura (
    id       INTEGER       PRIMARY KEY,
    nombre   VARCHAR(40),
    rareza   VARCHAR(15),
    coste    INTEGER,
    elemento VARCHAR(15),
    ataque   INTEGER,
    defensa  INTEGER,
    vida     INTEGER
);

CREATE TABLE entrenamiento (
    id       INTEGER       PRIMARY KEY,
    nombre   VARCHAR(40),
    rareza   VARCHAR(15),
    coste    INTEGER,
    efecto   VARCHAR(100),
    duracion INTEGER
);

CREATE TABLE objeto (
    id         INTEGER       PRIMARY KEY,
    nombre     VARCHAR(40),
    rareza     VARCHAR(15),
    coste      INTEGER,
    efecto     VARCHAR(100),
    consumible BOOLEAN
);

Ahora no hay ni un solo hueco: cada tabla tiene justo las columnas que necesita. Pero aparece otro problema. Los atributos comunes (nombre, rareza, coste) están repetidos en las tres tablas. Y, peor aún, las cartas ya no viven en un único sitio: están repartidas en tres tablas distintas.

Eso complica las preguntas que cruzan tipos. ¿Quieres listar todas las cartas de rareza "épica", sean del tipo que sean? Con este camino tienes que mirar en las tres tablas y unir los resultados a mano. Recuerda lo que aprendiste en el capítulo 5: cuando un dato común se repite en varios sitios, tarde o temprano acaba descuadrado. Esto huele a eso.

Camino 3: una tabla por entidad (la superclase y cada subclase)

El término medio, y el que vamos a adoptar como oficial del libro. Mantienes la tabla carta con lo común, y creas una tabla por subclase solo con los atributos propios de cada una. ¿Cómo se conectan? Con un viejo conocido del capítulo 4: la clave foránea.

La gracia está en un detalle: el id de cada tabla de subclase es, a la vez, su clave primaria y una clave foránea que apunta al id de carta. Es decir, una criatura y la carta de la que procede comparten el mismo id. No es un número nuevo: es el mismo de su carta.

SQL
CREATE TABLE carta (
    id     INTEGER       PRIMARY KEY,
    nombre VARCHAR(40),
    rareza VARCHAR(15),
    coste  INTEGER,
    tipo   VARCHAR(15)
);

CREATE TABLE criatura (
    id       INTEGER       PRIMARY KEY,
    elemento VARCHAR(15),
    ataque   INTEGER,
    defensa  INTEGER,
    vida     INTEGER,
    FOREIGN KEY (id) REFERENCES carta(id)
);

CREATE TABLE entrenamiento (
    id       INTEGER       PRIMARY KEY,
    efecto   VARCHAR(100),
    duracion INTEGER,
    FOREIGN KEY (id) REFERENCES carta(id)
);

CREATE TABLE objeto (
    id         INTEGER       PRIMARY KEY,
    efecto     VARCHAR(100),
    consumible BOOLEAN,
    FOREIGN KEY (id) REFERENCES carta(id)
);

Lee con calma la tabla criatura. Su id es la clave primaria (no se repite) y a la vez una clave foránea hacia carta (tiene que existir esa carta). Lo común se guarda una sola vez, en carta. Lo propio de cada tipo, en su tabla. Cero huecos y cero repeticiones.

Para saber más: el tipo BOOLEAN. Hemos puesto consumible BOOLEAN, un tipo pensado para guardar solo dos valores: verdadero o falso (sí o no). Es el más legible para una columna de "sí/no" como esta. Un detalle por si lo ves en otros sitios: no todos los motores de base de datos lo tratan igual. La mayoría lo aceptan tal cual, pero algunos no tienen BOOLEAN de serie y por dentro lo guardan como un número (0 = falso, 1 = verdadero). El concepto es idéntico; solo cambia el envoltorio. Si algún día tu motor se queja de BOOLEAN, ya sabes por dónde van los tiros.

Así quedan los datos. Primero, lo común en carta:

id nombre rareza coste tipo
1 Chispín común 2 criatura
5 Entreno intensivo común 1 entrenamiento
6 Poción común 1 objeto

Y lo propio, cada cosa en su sitio:

criatura          entrenamiento        objeto
┌────┬──────────┐ ┌────┬──────────┐    ┌────┬─────────────┬──────────┐
│ id │ ataque…  │ │ id │ duracion │    │ id │ efecto      │ consumible│
├────┼──────────┤ ├────┼──────────┤    ├────┼─────────────┼──────────┤
│ 1  │ 30…      │ │ 5  │ 2        │    │ 6  │ cura 20 vida│ sí        │
└────┴──────────┘ └────┴──────────┘    └────┴─────────────┴──────────┘
   id=1 apunta        id=5 apunta          id=6 apunta
   a carta(1)         a carta(5)           a carta(6)

Fíjate en que la criatura de la tabla criatura tiene id = 1, el mismo que Chispín en carta. Para reconstruir la criatura completa, juntas las dos tablas por ese id con un JOIN, exactamente la herramienta que aprendiste en el capítulo 5:

SQL
SELECT c.nombre, c.rareza, cr.elemento, cr.ataque, cr.vida
FROM carta c
JOIN criatura cr ON cr.id = c.id
WHERE c.tipo = 'criatura';

Resultado:

nombre rareza elemento ataque vida
Chispín común eléctrico 30 50

Lo común sale de carta, lo propio de criatura, unidos por el id compartido. Es la opción más ordenada, la que mejor refleja el plano que dibujaste, y la que seguiremos usando en el resto del libro.

Entonces, ¿cuál elijo?

No hay ganador absoluto, y esto es importante. Cada camino cambia algo:

  • Camino 1 (una tabla): lo más simple de consultar, pero con huecos. Bien si los tipos tienen pocos atributos propios.
  • Camino 2 (tabla por subclase): sin huecos, pero repite lo común y dispersa las cartas. Bien si casi nunca mezclas tipos en una consulta.
  • Camino 3 (tabla por entidad): lo más limpio y normalizado, a cambio de un JOIN para recomponer cada carta. El que mejor escala cuando la familia crece.

Lo importante no es memorizar cuál es "el bueno", sino saber que existen los tres y poder justificar tu elección según tu caso: cuántos atributos propios hay, cuántos huecos te molestan, qué preguntas vas a hacer más. Eso es diseñar con criterio. Y es justo lo que te separa de quien deja que la herramienta decida sin saber qué eligió por él.

Resumen

Has aprendido a modelar familias de cartas. Cuando varias entidades comparten una parte y difieren en otra, tienes una jerarquía: una superclase con lo común y subclases que heredan eso y añaden lo suyo. En Kriaturas, carta es la superclase, y criatura, entrenamiento y objeto las subclases.

Y has visto que esa jerarquía se lleva a tablas