# 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
<input type="hidden" name="partidas" id="partidas" value="[]">
```

```
mi_create::&db, &params, &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)
