# Entidades maestro-detalle [← Volver al índice](../crudl.md) Muchas entidades de negocio no corresponden a una sola tabla. Una factura, un movimiento de almacén o un pedido tienen un encabezado y un conjunto de partidas que se crean, se modifican y se eliminan como una unidad. El modelo genérico admite este caso de forma declarativa: usted describe las tablas dependientes y el marco se encarga de guardarlas, relacionarlas y eliminarlas junto con el maestro, dentro de una sola transacción. ## Declaración En el `model.dk` de la entidad, agregue un miembro a `@childs` por cada tabla dependiente: ``` @childs<"partidas*"> : entity.child("factura_detalle", "ifactura", "select * from factura_detalle where ifactura=@sys_pk;") ``` ### entity.child ``` entity.child::table, join, select_query ``` | Argumento | Descripción | |---|---| | `table` | Nombre de la tabla dependiente | | `join` | Nombre del campo de la tabla dependiente que apunta al maestro (clave foránea) | | `select_query` | Consulta que obtiene las filas dependientes de un maestro | El nombre del miembro que agregue a `@childs` (`partidas` en el ejemplo) es el nombre bajo el cual las filas dependientes viajan en el JSON, tanto de entrada como de salida. La clave de las filas dependientes es siempre `sys_pk`. Puede declarar varias tablas dependientes en la misma entidad. Cada una se procesa por separado. ## Forma de los datos Los elementos dependientes son un arreglo bajo el nombre del miembro declarado: ```json { "sys_pk": 105, "sys_recver": 3, "folio": "A-1042", "icliente": 27, "total": 3480.00, "partidas": [ { "sys_pk": 511, "sys_recver": 1, "iproducto": 88, "cantidad": 2, "importe": 1200.00 }, { "sys_pk": 512, "sys_recver": 1, "iproducto": 91, "cantidad": 6, "importe": 2280.00 }, { "sys_pk": 0, "sys_recver": 0, "iproducto": 14, "cantidad": 1, "importe": 0.00 } ] } ``` Esta misma estructura es la que devuelve una lectura y la que debe enviarse en un alta o una modificación. Para agregar una fila nueva al detalle, envíela con `sys_pk` en cero o ausente. Para conservar una existente, envíela con su `sys_pk`. Para eliminarla, **omítala del arreglo**. No es necesario incluir la clave foránea hacia el maestro en las filas del detalle; si falta, el marco la establece. Puede incluirla si le resulta cómodo. ## Comportamiento ### Lectura Tras obtener el registro maestro, se ejecuta el `select_query` de cada hijo usando **el registro maestro completo como conjunto de parámetros**. Por eso la consulta del ejemplo puede referirse a `@sys_pk`: ese parámetro proviene del maestro recién leído. Esto permite consultas de detalle que dependan de cualquier campo del encabezado, no solo de la clave: ``` "select d.*, p.descripcion from factura_detalle d join producto p on p.sys_pk = d.iproducto where d.ifactura = @sys_pk order by d.renglon;" ``` El resultado se agrega al maestro bajo el nombre del miembro. ### Alta y modificación En ambas operaciones el procedimiento es el mismo: 1. Los campos que corresponden a hijos se **retiran** de los datos del maestro, de modo que no se intente guardarlos como columnas. 2. Se guarda el registro maestro y se obtiene su `sys_pk`. 3. Por cada hijo presente en la carga útil: * Se consultan las claves de las filas dependientes que existen actualmente en la base de datos. * **Las filas que existen y no vienen en el arreglo entrante se eliminan.** * Cada fila del arreglo se guarda: si trae `sys_pk` mayor que cero se actualiza, y si no, se inserta. * Si la fila no trae la clave foránea, se le asigna la clave del maestro. 4. Se confirma la transacción. Si un hijo declarado **no viene** en la carga útil, no se toca. Es decir, la ausencia del miembro completo deja el detalle intacto; un arreglo vacío elimina todas sus filas. La eliminación de filas huérfanas es **física**. ### Baja La baja de la entidad maestra **no elimina los elementos dependientes**. El marco borra únicamente la fila del maestro. Para eliminar el detalle debe hacerlo usted, mediante restricciones de integridad referencial en cascada en la base de datos, o mediante un `@custom_delete`. Ver [Implementación del modelo](modelo.md#custom_delete). ## Transacción Cuando `@use_transaction` está activo (valor predeterminado), el guardado del maestro y el procesamiento de todos los hijos ocurren dentro de una sola transacción de base de datos. Si cualquier paso falla, se deshace todo y se produce un error. No modifique este valor en entidades con elementos dependientes. ## Selección y renombrado de campos por hijo Además de los tres argumentos de `entity.child`, el objeto admite dos miembros opcionales que controlan qué campos se guardan de cada fila del detalle: ``` ref h = entity.child("factura_detalle", "ifactura", "select * from factura_detalle where ifactura=@sys_pk;") h<"fields"> : "iproducto,cantidad,precio,importe" h<"alias"> : "producto:iproducto" @childs<"partidas*"> : h ``` | Miembro | Descripción | |---|---| | `fields` | Lista de campos a considerar, delimitada por comas. El valor predeterminado `*` acepta todos los que se envíen. | | `alias` | Lista de pares delimitada por comas, con la correspondencia entre nombres. | Son útiles cuando la interfaz de usuario envía nombres distintos de los de la tabla, o cuando la vista incluye columnas calculadas que no deben guardarse. ### Declaración directa `entity.child` es una comodidad. El objeto que describe un hijo es un registro corriente y puede construirse directamente, lo que resulta más legible cuando se declaran varios miembros: ``` using @childs { member "partidas" { @"table": "factura_detalle" @"joinfield": "ifactura" @"keyfield": "sys_pk" @"select_query": @qry_detalle_factura @"fields": "sys_pk,sys_recver,iproducto,renglon,cantidad,precio,importe" } } ``` Los cinco miembros son los que consulta el marco. `keyfield` es siempre `sys_pk`. ### Campos distintos en alta y en modificación Conviene declarar el hijo dentro de cada operación en lugar de una sola vez, porque la lista de campos suele diferir. En el **alta**, omita `sys_pk` y `sys_recver` de `fields`. Si la interfaz reenvía identificadores procedentes de otro documento —al copiar una cotización a un pedido, por ejemplo— se intentaría actualizar filas ajenas en lugar de insertar nuevas. En la **modificación**, inclúyalos: son los que permiten distinguir las filas que se conservan de las que se agregan, y los que hacen funcionar el control de concurrencia sobre el detalle. ## Detalle recibido como texto JSON Un formulario HTML estándar no puede enviar un arreglo de objetos. El patrón habitual es serializar el detalle en un campo oculto y deserializarlo en el modelo: ```html ``` ``` mi_create::&db, ¶ms, &data { ref datos = record.copy(data,"*") datos<"partidas*"> : from.json(@@(data,"partidas")) ... } ``` Tras la sustitución, el miembro contiene una lista de registros y el marco lo procesa con normalidad. Los clientes REST envían el arreglo directamente y no necesitan este paso. Recuerde declarar el nombre del campo en `@create_exclude_fields` y `@update_exclude_fields` si coincide con el del miembro declarado en `@childs`, para que no se intente guardar como columna del maestro. ## Ejemplo completo Una factura con partidas. **`entidades/factura/model.dk`** ``` #include "dkli.dkh" #! module "Modelo de factura" { @table_name = "factura" @read_query = " select f.*, c.nombre as cliente_nombre from factura f left join cliente c on c.sys_pk = f.icliente where f.#<@keyfield> = @_entity_id limit 1;" @list_query = " select f.sys_pk, f.folio, f.fecha, f.total, c.nombre as cliente_nombre from factura f left join cliente c on c.sys_pk = f.icliente where ifnull(f.sys_deleted,0)=0 and (@desde='' or f.fecha >= @desde) and (@hasta='' or f.fecha <= @hasta) order by f.fecha desc, f.folio desc limit 500;" // Campos calculados que no existen como columnas @create_exclude_fields = "cliente_nombre" @update_exclude_fields = "cliente_nombre" @childs<"partidas*"> : entity.child( "factura_detalle", "ifactura", "select d.*, p.descripcion, p.sku from factura_detalle d join producto p on p.sys_pk = d.iproducto where d.ifactura = @sys_pk order by d.renglon;") // Baja lógica del encabezado y del detalle baja_factura::&db, table, sys_pk { new p { @"sys_pk": sys_pk } do dbr.execute(db, "update factura_detalle set sys_deleted=1 where ifactura=@sys_pk;", p) do dbr.execute(db, "update factura set sys_deleted=1 where sys_pk=@sys_pk;", p) } point @custom_delete to baja_factura } ``` Observe que las columnas `descripcion` y `sku` que devuelve la consulta del detalle no existen en `factura_detalle`. Si la interfaz reenvía esas columnas al guardar, conviene acotar los campos del hijo: ``` ref h = entity.child("factura_detalle", "ifactura", "...") h<"fields"> : "sys_pk,sys_recver,iproducto,renglon,cantidad,precio,importe" @childs<"partidas*"> : h ``` **Alta** ``` POST /misistema/factura/ Content-Type: application/json { "folio": "A-1043", "fecha": "2026-08-26", "icliente": 27, "total": 1200.00, "partidas": [ { "iproducto": 88, "renglon": 1, "cantidad": 2, "precio": 600.00, "importe": 1200.00 } ] } ``` La respuesta es la factura completa, releída, con `sys_pk`, `sys_guid`, `sys_recver` y las partidas ya guardadas con sus claves. **Modificación que agrega una partida y elimina otra** ``` PATCH /misistema/factura/105 Content-Type: application/json { "sys_recver": 3, "total": 3480.00, "partidas": [ { "sys_pk": 511, "sys_recver": 1, "iproducto": 88, "renglon": 1, "cantidad": 2, "precio": 600.00, "importe": 1200.00 }, { "iproducto": 14, "renglon": 2, "cantidad": 1, "precio": 2280.00, "importe": 2280.00 } ] } ``` La partida `512`, ausente del arreglo, se elimina. ## Documentos relacionados * [Implementación del modelo](modelo.md) * [Web services REST](rest.md) * [Implementación de las vistas](vistas.md)