Parte IV · Pulir y proteger
Capítulo 11 — Reglas que se cumplen solas (integridad)
Cierras la Parte III con un modelo de Kriaturas bien diseñado y bien ordenado. Cada dato en su sitio, una sola vez. Has hecho los deberes.
Pero el capítulo anterior terminó con una advertencia. Que tú coloques los datos con cuidado no impide que otro los meta mal. Un compañero de equipo despistado, un formulario sin revisar, un bug del juego que escribe en la base de datos cuando no debe. Cualquiera de ellos puede colar una carta con coste negativo, dos jugadores con el mismo correo o una partida cuyo ganador no jugó.
Hasta ahora, varias reglas del juego dependían de tu disciplina. "El coste nunca es
negativo." "Cada amistad se guarda una sola vez, con jugador_a < jugador_b." "El
ganador tiene que ser uno de los dos contendientes." Son reglas que tú sabes, pero
que la base de datos no vigila. Confías en que nadie las rompa.
En este capítulo vas a dejar de confiar. Vas a enseñarle esas reglas a la propia base de datos para que las haga cumplir sola, pase lo que pase en la aplicación. Eso se llama integridad, y es la red de seguridad de tus datos. Con esto abres la Parte IV: pulir y proteger.
Las cuatro leyes que toda base de datos respeta
Antes de escribir una sola línea de SQL, conviene conocer las cuatro reglas de fondo. Son las reglas de integridad del modelo relacional: las leyes que cualquier base de datos relacional respeta para que sus datos tengan sentido. Algunas ya las has usado sin nombrarlas. Aquí las juntamos todas.
1. Integridad de unicidad (la clave primaria no se repite). Dos filas no pueden
tener la misma clave primaria. Dos cartas no pueden compartir el id 1. Si pudieran,
volverías al caos del capítulo 2: pedirías "la carta 1" y la base de datos no sabría
cuál darte. La clave primaria identifica, y para identificar tiene que ser única.
2. Integridad de entidad (la clave primaria no es nula). Los datos que forman la
clave primaria no pueden quedar vacíos. Una carta sin id no se puede identificar; es
como un jugador sin nombre en una lista de espera. Si la clave es el dato que señala a
cada fila, no puede faltar.
3. Integridad referencial (las claves foráneas apuntan a algo real). Esta ya la
conoces del capítulo 4. Una clave foránea (foreign key) debe apuntar a una fila
que exista, o quedarse vacía. Un mazo no puede pertenecer al jugador 99 si ese
jugador no está en la tabla. La base de datos no permite mazos huérfanos. Aquí solo le
ponemos nombre formal a lo que ya hacías.
4. Integridad de dominio (cada valor es del tipo correcto). Cada columna tiene un
dominio: el conjunto de valores que admite. El coste admite números; la rareza
admite "común", "rara" o "épica", y nada más. Un valor fuera de su dominio no entra.
Esta es la que más vas a trabajar en este capítulo, porque es la que tú defines a
medida.
Fíjate en el patrón. Las dos primeras hablan de la clave primaria. La tercera, de las claves foráneas. La cuarta, de los valores de cada columna. Entre las cuatro, blindan los tres sitios donde un dato puede ir mal. Ahora vamos a escribirlas en SQL.
Cómo se escriben las reglas: las restricciones
Una restricción (constraint) es una regla que añades a una columna o a una tabla, y que la base de datos hace cumplir en cada escritura. Si una orden la incumple, la base de datos la rechaza y no guarda nada. Vamos una por una, con su ejemplo de Kriaturas.
NOT NULL — esta columna no puede quedar vacía
Algunos datos son obligatorios. Una carta tiene que tener nombre; no tiene sentido
una carta sin nombre. Para exigirlo, marcas la columna con NOT NULL:
nombre VARCHAR(40) NOT NULL
A partir de ahí, cualquier intento de guardar una carta sin nombre se rechaza. (Una
columna sin esta marca admite quedarse vacía; ese "vacío" se llama NULL, y significa
"aquí no hay valor". NOT NULL lo prohíbe.)
UNIQUE — esta columna no admite repetidos
A veces un dato no es la clave primaria, pero igual no puede repetirse. El correo
de un jugador, por ejemplo: dos cuentas no deberían compartir email. Lo declaras con
UNIQUE:
email VARCHAR(100) UNIQUE
Ahora, si alguien intenta registrar un segundo jugador con un correo que ya existe, la
base de datos lo frena. A una columna UNIQUE se la llama a veces clave
alternativa: identifica también de forma única, aunque no sea la clave oficial.
PRIMARY KEY — ni repetida ni vacía
Esta ya la usas desde el capítulo 2. Ahora puedes verla con otros ojos: PRIMARY KEY
es, en el fondo, UNIQUE y NOT NULL a la vez. No se repite (unicidad) y no se
queda vacía (entidad). Las dos primeras reglas de integridad, juntas en una sola
palabra. Por eso una clave primaria siempre identifica sin fallo.
CHECK — esta columna debe cumplir una condición
Aquí está la restricción más versátil. Con CHECK le pones a una columna una
condición que todo valor debe cumplir. ¿El coste de una carta no puede ser negativo?
Lo dices así:
coste INTEGER CHECK (coste >= 0)
A partir de ahí, intentar guardar una carta con coste = -5 se rechaza. La regla del
juego "el coste nunca es negativo" deja de depender de tu memoria: la vigila la base
de datos.
CHECK también sirve para limitar un valor a una lista cerrada. La rareza solo puede
ser una de tres:
rareza VARCHAR(15) CHECK (rareza IN ('común', 'rara', 'épica'))
Cualquier rareza distinta —"legendaria", "comun" sin tilde, un dedazo— se rechaza. Así defines a mano el dominio de la columna: la cuarta regla de integridad, escrita por ti.
REFERENCES — esta columna apunta a otra tabla
Es la clave foránea del capítulo 4. Declara que una columna apunta a la clave primaria de otra tabla, y activa la integridad referencial:
jugador_id INTEGER REFERENCES jugador(id)
La base de datos no dejará que mazo.jugador_id valga 99 si no existe el jugador 99.
Es la misma regla de siempre, ahora en el mismo saco que las demás.
Ponles nombre a tus reglas. Puedes bautizar una restricción con
CONSTRAINT nombre …. Por ejemplo,CONSTRAINT coste_no_negativo CHECK (coste >= 0). No cambia lo que hace, pero cuando la regla salte, el mensaje de error dirácoste_no_negativoen vez de un código críptico, y sabrás al instante qué pasó. En proyectos serios, nombrar las restricciones se agradece muchísimo.
Reescribiendo la tabla `carta` con todas sus reglas
Junta todo lo anterior. Recuerda que desde el capítulo 7 la carta es la superclase
de la jerarquía: además de los datos básicos lleva una columna tipo que dice si es
criatura, entrenamiento u objeto. Pero hasta ahora la hemos creado sin una sola
regla de integridad, confiada:
CREATE TABLE carta (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40),
rareza VARCHAR(15),
coste INTEGER,
tipo VARCHAR(15)
);
Y así queda ahora, con la integridad puesta:
CREATE TABLE carta (
id INTEGER PRIMARY KEY,
nombre VARCHAR(40) NOT NULL,
rareza VARCHAR(15) CHECK (rareza IN ('común', 'rara', 'épica')),
coste INTEGER NOT NULL CHECK (coste >= 0),
tipo VARCHAR(15) CHECK (tipo IN ('criatura', 'entrenamiento', 'objeto'))
);
Léela como una lista de promesas que la base de datos te hace cumplir: toda carta
tiene id único, tiene nombre (NOT NULL), su rareza es una de las tres permitidas,
su coste es un número que nunca baja de cero y su tipo es una de las tres familias de
la jerarquía. Cinco reglas del juego, convertidas en cinco reglas de datos. Ya no
dependen de que nadie se acuerde de ellas.
La misma idea vale para jugador. Su correo no puede repetirse y su alias es
obligatorio:
CREATE TABLE jugador (
id INTEGER PRIMARY KEY,
alias VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
fecha_alta DATE
);
Fíjate en email: lleva NOT NULL y UNIQUE. Obligatorio y sin repetidos. Una
columna puede acumular varias restricciones; se aplican todas a la vez.
Recogiendo los hilos sueltos: `partida` y `amistad`
Quedaron dos reglas pendientes en capítulos anteriores que dependían de tu disciplina. Es el momento de dejarlas en manos de la base de datos.
El ganador tiene que haber jugado (hilo del capítulo 8)
En la tabla partida guardabas jugador1, jugador2 y ganador, los tres apuntando
a jugador. Avisamos entonces de un peligro: nada impedía poner como ganador a un
jugador que no jugó esa partida. La integridad referencial garantiza que el
ganador existe, pero no que sea uno de los dos contendientes.
Eso es exactamente trabajo para un CHECK. La regla "el ganador es jugador1 o
jugador2" se escribe comparando columnas de la misma fila:
CREATE TABLE partida (
id INTEGER PRIMARY KEY,
jugador1 INTEGER NOT NULL REFERENCES jugador(id),
jugador2 INTEGER NOT NULL REFERENCES jugador(id),
ganador INTEGER REFERENCES jugador(id),
fecha DATE NOT NULL,
duracion INTEGER CHECK (duracion > 0),
CONSTRAINT ganador_valido CHECK (ganador = jugador1 OR ganador = jugador2)
);
Tres cosas que mirar. El ganador no lleva NOT NULL a propósito: una partida puede
quedar en empate o sin terminar, y entonces ganador se queda vacío. La duracion
lleva un CHECK propio (una partida no dura cero minutos ni menos). Y la estrella es
la última línea: ganador_valido, una restricción de tabla (no de una sola
columna, porque compara tres) que obliga al ganador a ser uno de los dos que jugaron.
La regla que antes vigilabas tú, ahora la vigila la base de datos.
Cada amistad, una sola vez (hilo del capítulo 9)
En el capítulo 9 acordaste una convención para la tabla amistad: guardar cada pareja
con jugador_a < jugador_b, para no registrar la misma amistad dos veces (A-B y B-A).
Era "pensar el dato antes de guardarlo". Pero era una convención de papel: nada impedía
saltársela.
Ahora la blindas con un CHECK:
CREATE TABLE amistad (
jugador_a INTEGER NOT NULL REFERENCES jugador(id),
jugador_b INTEGER NOT NULL REFERENCES jugador(id),
fecha DATE,
PRIMARY KEY (jugador_a, jugador_b),
CONSTRAINT orden_amistad CHECK (jugador_a < jugador_b)
);
La restricción orden_amistad rechaza cualquier intento de guardar la pareja al revés
(o de hacer a alguien amigo de sí mismo, porque 1 < 1 es falso). La clave primaria
compuesta (jugador_a, jugador_b) ya evitaba duplicar la misma pareja exacta; el
CHECK evita que entre disfrazada con el orden cambiado. Entre las dos, la simetría de
la amistad queda garantizada por la propia base de datos. Tu convención dejó de ser una
promesa y pasó a ser una ley.
Resumen
En este capítulo dejaste de confiar en que nadie rompa las reglas y se las enseñaste a
la base de datos. Conociste las cuatro reglas de integridad del modelo relacional
—unicidad, entidad, referencial y dominio— y aprendiste a escribirlas como
restricciones en SQL: NOT NULL para lo obligatorio, UNIQUE para lo que no se
repite, PRIMARY KEY que combina ambas, CHECK para condiciones a medida y
REFERENCES para las claves foráneas.
Con ellas reforzaste carta, jugador, partida y amistad, y de paso recogiste dos
hilos que arrastrabas: el ganador válido del capítulo 8 (un CHECK que lo obliga a
ser uno de los dos contendientes) y la simetría de la amistad del capítulo 9 (un
CHECK que imp