Claves primarias compuestas en Delphi MVC Framework ActiveRecord
🇬🇧 English · 🇮🇹 Italiano · 🇩🇪 Deutsch · 🇧🇷 Português
Durante años el ActiveRecord de DMVCFramework mantuvo las claves primarias deliberadamente simples: una sola columna, sin excepciones. La versión 3.5 elimina ese límite.
Si has diseñado una base de datos relacional de cierto tamaño, te has topado con esta tabla:
CREATE TABLE user_roles (
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
PRIMARY KEY (user_id, role_id)
);
Una tabla de unión. Su identidad no es una columna, son dos. Y durante años, TMVCActiveRecord imponía una regla firme: exactamente una columna de clave primaria por entidad. Añadías foPrimaryKey a un segundo campo y te frenaba al arrancar con un error.
La regla mantenía el ORM simple. También dejaba fuera un tipo de tabla muy común: user_roles(user_id, role_id), una línea de pedido con clave (order_id, line_no), un registro por tenant con clave (tenant, code). Tablas de este tipo hay por todas partes.
Los workarounds que todos conocían
Hay que reconocerlo: siempre existió una salida de emergencia. Podías no declarar ninguna clave primaria, mapear dos campos normales y leerlos con RQL y Where<T> como cualquier otra cosa. Solo renunciabas a direccionar una fila por su clave: nada de GetByPK, así que cada búsqueda pasaba por un filtro explícito.
O bien añadías un id autoincremental surrogate como clave primaria, con una restricción UNIQUE sobre las columnas naturales. ActiveRecord quedaba contento, recuperabas el CRUD por clave, y la base de datos seguía garantizando que la clave real fuera única. Esa columna id se pasaba el resto de su vida sin que nadie la consultara, existiendo únicamente para que el ORM dejara de quejarse.
Ambos hacen el trabajo. Muchísimos esquemas válidos funcionan exactamente así todavía hoy. Pero ninguno de los dos te deja decir lo evidente: que (user_id, role_id) es la clave, y hacer que el ORM la trate como tal.
Ese es también el motivo por el que esta funcionalidad tardó tanto. Los workarounds eran lo bastante buenos, y un workaround lo bastante bueno es el enemigo natural de la solución de verdad: mientras la salida de emergencia funciona, nadie pone la puerta que falta en lo alto de la lista. Durante años un id surrogate absorbió en silencio la demanda, y yo lo dejé hacer. Esta vez me cansé de explicar el workaround, así que me senté e hice que ActiveRecord mapeara la clave natural directamente.
A partir de la 3.5, ya no tienes que elegir
Este es el cambio completo en tu modelo:
[MVCTable('user_roles')]
TUserRole = class(TMVCActiveRecord)
private
[MVCTableField('user_id', [foPrimaryKey])]
fUserID: Integer;
[MVCTableField('role_id', [foPrimaryKey])]
fRoleID: Integer;
// ...
end;
Ningún atributo nuevo que memorizar. Marcas ambas columnas con foPrimaryKey, igual que ya marcas una. Si sabes poner un foPrimaryKey, ya sabes declarar una clave compuesta: la curva de aprendizaje es un escalón de altura cero. A partir de ese momento ActiveRecord trata (user_id, role_id) como la identidad de la fila, y cada WHERE, INSERT, UPDATE y DELETE generado cubre la clave completa.
Y como un único valor ya no puede apuntar a una fila, los métodos por clave que conoces tienen ahora una contraparte plural:
lRole := TMVCActiveRecord.GetByPKs<TUserRole>([1, 42]);
Esa llamada, GetByPKs, es un método de clase, y apunta a algo que conviene saber: todo esto es un mismo engine, y no estás atado al estilo Active Record estático. DMVCFramework ya trae un repositorio listo sobre esas mismas entidades, IMVCRepository<T>, en la unit MVCFramework.Repository. Su verdadera ventaja frente a los métodos de clase es que es una interfaz: a diferencia de una llamada estática, puedes inyectarlo directamente en tus controladores y servicios a través del contenedor de inyección de dependencias. Lo registras una vez y dejas que [MVCInject] lo entregue a quien lo necesite:
// Registra el repositorio una vez, en el .dpr, antes de arrancar el servidor
Container.RegisterType(TMVCRepository<TUserRole>, IMVCRepository<TUserRole>,
TRegistrationType.SingletonPerRequest);
// Luego inyéctalo donde lo necesites, un controlador o un servicio
type
[MVCPath('/user-roles')]
TUserRolesController = class(TMVCController)
private
fRepo: IMVCRepository<TUserRole>;
public
[MVCInject]
constructor Create(UserRolesRepository: IMVCRepository<TUserRole>); reintroduce;
end;
Mismas entidades, mismo soporte de claves compuestas por debajo, ahora a través de una dependencia que puedes sustituir en un test, en lugar de una llamada estática que no puedes. Escribir tus propios repositorios sigue siendo perfectamente válido, la cuestión es que rara vez hace falta.
Esos métodos plurales con array eran la parte obvia. La pregunta más difícil era cómo darle nombre siquiera a una clave hecha ahora de varios valores. Así que, junto a los métodos con array, hay ahora una manera distinta de direccionar una fila: asignas cada campo de la clave por su nombre de propiedad, en el orden que prefieras, y luego llamas a Load, un método completamente nuevo que lee la clave directamente de la entidad en lugar de una lista de argumentos. Sobre el papel parece un cambio pequeño. Por debajo es un verdadero cambio de paradigma, porque por primera vez es la entidad la que posee su propia clave, en vez de recibirla como argumento posicional.
Esa es la esencia, y también donde termina la parte fácil. Marcar las dos columnas lleva un minuto. Las preguntas escondidas detrás de ese minuto son la razón de que el artículo completo sea tan largo:
- Los métodos posicionales con array (
GetByPKs,LoadByPKs) y el nuevoLoadpor nombre de propiedad existen ambos por un motivo: ¿qué trampa evita uno, y cuándo deberías preferir el otro de todos modos? - ¿Qué hacen
LoadyRefreshcuando la fila sencillamente no está, y por qué elegí la respuesta que resulta menos cómoda? - ¿Pueden las columnas de una misma clave ser de tipos distintos, y cuántas de ellas puede rellenar la base de datos por ti?
- Con el controlador auto-CRUD, ¿cómo construyes la URL para direccionar una fila cuya clave tiene dos columnas, y cómo se escriben en ella las claves de tipo string o GUID?
- ¿Cuál es el único breaking change de toda la release, y qué tipo concreto de código tiene que preocuparse realmente por él?
Encontrarás las respuestas a estas, y a algunas preguntas que aún no se te han ocurrido, en el artículo completo en Patreon.
Un superpoder que conocen muy pocos
Es un buen momento para señalar una pieza de DMVCFramework que considero un auténtico superpoder silencioso: TMVCActiveRecordController. Todo el que empieza a usarlo se queda con él, y sin embargo la mayoría de los desarrolladores con los que hablo ni siquiera sabe que existe.
Lo que resuelve es el código más repetitivo de cualquier API basada en datos. Para cada tabla, de otro modo, escribirías a mano el mismo controlador: un GET para la lista, un GET por id, un POST para crear, un PUT para actualizar, un DELETE, más paginación, filtrado y ordenación, multiplicado por cada entidad del esquema. Es boilerplate que has escrito cien veces y que en la ciento uno vas a equivocar en algún detalle sutil, siempre en el único endpoint que se saltó la code review.
El controlador sustituye todo eso con una sola línea:
FMVC.AddController(TMVCActiveRecordController, '/api/entities');
FMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create(CON_DEF_NAME));
A partir de ahí, cada entidad ActiveRecord registrada es un recurso REST. Obtienes CRUD completo, consultas RQL directamente desde la URL para filtrar, ordenar y paginar los resultados, y una descripción Swagger generada por ti. Sin controlador por entidad, sin DTO por entidad, sin ruta por entidad.
Lo que lo hace seguro de usar en una aplicación real es que no es un intermediario tonto que se limita a mover filas dentro y fuera de la base de datos a espaldas del ORM. El controlador hace pasar tus entidades por su ciclo de vida normal, así que toda la lógica de negocio que hayas puesto en la entidad sigue ejecutándose: la validación, los hooks OnBeforeInsert y OnBeforeUpdate, los campos calculados y de solo lectura, las reglas de serialización. Un valor que la entidad rechaza se rechaza por HTTP con la misma firmeza con que lo haría desde código Delphi. Estás exponiendo tu modelo, no dándole la vuelta.
Combina eso con el nuevo generador de entidades y las cuentas se vuelven un poco absurdas. Apuntas el generador a una base de datos existente y te escribe las clases TMVCActiveRecord, tabla por tabla; registras el controlador una vez, y un esquema con miles de tablas se convierte en una API RESTful en lo que se tarda en hacer un café. El límite está claro: una tabla que es la raíz de un agregado, una con hijos u otras relaciones cuya coherencia hay que mantener (piensa en una cabecera de factura con sus líneas), no debería exponerse fila a fila de esta manera. Le corresponde estar detrás de su Aggregate Root, tal como argumenta el Domain-Driven Design (el famoso libro de Eric Evans), para que la raíz pueda hacer cumplir los invariantes del agregado. Esos casos siguen mereciendo un controlador dedicado, escrito a mano. Pero la mayoría de los esquemas están hechos sobre todo de tablas simples e independientes, y para cada una de ellas este enfoque low-code te entrega una API funcional y validada con un esfuerzo cercano a cero.
Y aquí es exactamente donde las claves compuestas tenían que ganarse su sitio. Un controlador auto-CRUD es tan general como las claves que sabe direccionar, así que una funcionalidad que se quedara en las claves de una sola columna habría dejado las tablas de unión fuera de la única parte del framework cuyo trabajo entero es tratar a cada entidad de la misma manera.
Lee el recorrido completo
Lo escribí todo con detalle, con el código, el razonamiento detrás de cada decisión y los tests con los que se publica (SQLite, Firebird y PostgreSQL, Win32 y Win64), como artículo de análisis a fondo para los suscriptores de DelphiMVCFramework en Patreon.
Si construyes backends Delphi con DMVCFramework, ahí es donde están los detalles. Una suscripción también abre el resto del material premium: artículos de análisis a fondo como este, vídeos, los libros en profundidad y un descuento en la próxima edición de la guía oficial de DMVCFramework. Es también lo que mantiene en marcha el desarrollo del framework.
👉 Claves primarias compuestas en DMVCFramework ActiveRecord: artículo completo en Patreon

Comments
comments powered by Disqus