# Implementación del modelo

[← Volver al índice](../crudl.md)

El modelo genérico `entity.dk` implementa las operaciones CRUD-L sobre entidades de datos almacenadas en tablas que siguen el patrón de diseño de Devkron (Induxsoft TableModel), previsto por la biblioteca `dbr.dkh`, e incluye los campos de control `sys_pk`, `sys_guid`, `sys_recver`, `sys_dtcreated`, `sys_timestamp` y `sys_deleted`. [Más información aquí](../../Bibliotecas-de-funciones/dbr/dbr.md)

Para definir un modelo personalizado que extienda o sustituya la funcionalidad predeterminada, cree un archivo `model.dk` en la carpeta de la entidad.

## Variables globales del modelo

```
// Nombre de la tabla subyacente. Si no se establece se asume el valor de _entities_type
@table_name = ""

// Nombre del campo clave predeterminado
@keyfield = "sys_pk"

// Consulta para obtener un elemento (operación READ)
@read_query = "select * from #<@table_name> where #<@keyfield>=@_entity_id limit 1;"

// Consulta para obtener la clave primaria a partir de una clave alterna
@get_sys_pk = "select sys_pk from #<@table_name> where #<@keyfield>=@_entity_id and ifnull(sys_deleted,0)=0 limit 1;"

// Consulta para obtener la lista de elementos (operación LIST)
@list_query = "select * from #<@table_name> where ifnull(sys_deleted,0)=0;"

// Campos que se actualizarán (UPDATE). * indica todos los que se envíen,
// o bien una lista delimitada por comas
@update_fields = "*"

// Campos que se establecerán en una inserción (CREATE)
@create_fields = "*"

// Campos que se excluirán en una operación CREATE
@create_exclude_fields = ""

// Campos que se excluirán en una operación UPDATE
@update_exclude_fields = ""

// Alias de campos en la inserción. Lista de pares alias:campo delimitados por comas
@create_alias_fields = ""

// Alias de campos en la actualización. Lista de pares alias:campo delimitados por comas
@update_alias_fields = ""

// Ejecuta las operaciones con elementos dependientes dentro de una transacción
@use_transaction = @true
```

En las consultas, la notación `#<...>` indica sustitución textual de identificadores, mientras que los nombres precedidos de `@` son parámetros enlazados. Es decir, `@table_name` y `@keyfield` se insertan como texto en la consulta, y `@_entity_id` se pasa como parámetro.

Los identificadores que provienen de la URL (`_entities_type` y `_key`) se filtran antes de interpolarse. Aun así, evite construir consultas donde valores de origen externo terminen como texto dentro de la sentencia.

## Referencias a funciones

La extensión del modelo se realiza creando funciones propias y estableciendo punteros hacia ellas mediante la sentencia `point ... to`.

| Puntero | Firma | Devuelve |
|---|---|---|
| `@create` | `&db, &params, &data` | La entidad creada, completa |
| `@read` | `&db, &params` | La entidad solicitada |
| `@update` | `&db, &params, &data` | La entidad actualizada, completa |
| `@delete` | `&db, &params` | Nulo |
| `@list` | `&db, &params` | Lista de entidades |
| `@blank` | `&db, &params` | Un elemento vacío o con valores predeterminados |
| `@custom_delete` | `&db, table_name, sys_pk` | Sin valor de retorno |

Ejemplo:

```
mifuncion_update::&db,&params,&data
{
    // Actualice aquí los datos que recibe en data, sobre la base de datos db,
    // usando los parámetros de params si fuese necesario
}

point @update to mifuncion_update
```

### Contrato de las funciones

* **Errores.** Señale los errores con `rise_error`. Si el código es igual o mayor que `400` se usará como estado HTTP; en cualquier otro caso la respuesta será `500`. Ver [El controlador](controlador.md#tratamiento-de-errores).
* **Transacción.** Si abre una transacción, es responsable de confirmarla o deshacerla. El controlador no gestiona transacciones; lo hace el modelo.
* **Forma del retorno.** El marco no interpreta lo que devuelven estas funciones: lo entrega a la vista en `output` y, en modo REST, lo serializa como JSON. Puede devolver la entidad, una estructura que la contenga junto a datos auxiliares, o una lista. La operación LIST tampoco exige que el retorno sea una lista.
* **Deshabilitar operaciones.** Establezca el puntero a nulo para que la operación devuelva `404 Operación no admitida`. Es la forma de publicar una entidad de solo lectura.

### Transacciones que abarcan más que el guardado

La variable `@use_transaction` gobierna la transacción que abren las implementaciones predeterminadas de alta y modificación, y cubre únicamente el guardado del maestro y de sus dependientes.

Cuando la operación debe abarcar más — validaciones que consultan la base de datos, asignación de folios, recálculo de totales, actualización de registros relacionados — desactive esa transacción y tome el control:

```
mi_create::&db, &params, &data
{
    @use_transaction = @false

    do dbr.begin(db)

    do validar(db, params, data)
    do asignar_folio(db, data)

    ref entidad = entity.create(db, params, data)

    do recalcular_totales(db, @@(entidad,"#sys_pk"))

    do dbr.commit(db)
    return entidad

    exception
    {
        if dbr.in_transaction(db) { do dbr.rollback(db) }
        do rise_error(last_error_code(), last_error())
    }
}

point @create to mi_create
```

De este modo su función conserva la lógica predeterminada de guardado, incluido el tratamiento de los elementos dependientes, y a la vez controla el alcance de la transacción.

Conserve el código original al reenviar la excepción, como en el ejemplo, para no perder la clasificación del error.


### `@custom_delete`

`@custom_delete` no sustituye a `@delete`, sino al método de borrado que usa la implementación predeterminada. Recibe la conexión, el nombre de la tabla y la clave primaria ya resuelta, y se ejecuta dentro de la transacción.

Es el lugar natural para implementar eliminación lógica:

```
borrado_logico::&db, table, sys_pk
{
    new p { @"sys_pk": sys_pk }
    do dbr.execute(db, "update " + table + " set sys_deleted=1 where sys_pk=@sys_pk;", p)
}

point @custom_delete to borrado_logico
```

## Comportamiento de las operaciones predeterminadas

### CREATE

1. Se resuelve `@keyfield` a partir del parámetro `_key`, y `@table_name` a partir de `_entities_type` si no estaba establecido.
2. Se copian los datos aplicando `@create_fields` y `@create_alias_fields`.
3. Se retiran los campos declarados como hijos y los indicados en `@create_exclude_fields`.
4. Se abre la transacción si `@use_transaction` está activo.
5. Se guarda el registro maestro.
6. Se procesan los elementos dependientes. Ver [Entidades maestro-detalle](maestro-detalle.md).
7. Se confirma la transacción.
8. Se relee la entidad con `@read` y se devuelve completa.

Si se produce cualquier error, se deshace la transacción.

### READ

1. Se resuelven `@table_name` y `@keyfield`.
2. Se ejecuta `@read_query` con los parámetros de la solicitud.
3. Si no se obtiene registro, se produce un error `404`.
4. Se cargan los elementos dependientes, si los hay.

Observe que `@read_query` no filtra `sys_deleted`. Si emplea eliminación lógica y necesita que la lectura directa oculte los registros marcados, añada la condición a la consulta.

### UPDATE

1. Se resuelven `@table_name` y `@keyfield`.
2. Se copian los datos aplicando `@update_fields` y `@update_alias_fields`.
3. Se retiran los campos declarados como hijos y los indicados en `@update_exclude_fields`.
4. Se determina la identidad del registro:
   * Si no se indicó `_entity_id` en la URL, se toma `sys_pk` o `sys_guid` de la carga útil. Si no está ninguno de los dos, se produce un error `404`.
   * Si `@keyfield` es `sys_pk` o `sys_guid`, se usa directamente y se retira el otro de los datos.
   * Si `@keyfield` es cualquier otro campo, se resuelve la clave primaria con `@get_sys_pk`. Si no se encuentra, se produce un error `404`.
5. Se abre la transacción, se guarda el maestro, se procesan los dependientes y se confirma.
6. Se relee la entidad y se devuelve completa.

### DELETE

1. Se resuelve `@table_name`.
2. Se resuelve la clave primaria: directamente si `@keyfield` es `sys_pk`, o mediante `@get_sys_pk` en caso contrario.
3. Se abre la transacción.
4. Si hay un `@custom_delete` establecido, se invoca. En caso contrario, **se elimina físicamente la fila**.
5. Se confirma la transacción.

La eliminación predeterminada es física. Si su diseño emplea eliminación lógica, establezca `@custom_delete`.

### LIST

Se ejecuta `@list_query` con los parámetros de la solicitud y se devuelve la lista resultante.

La consulta predeterminada filtra los registros marcados como eliminados.

## Filtros, orden y paginación

El modelo genérico ejecuta `@list_query` tal cual. No compone cláusulas de filtrado, ordenamiento ni paginación.

Todo lo que necesite en la lista debe estar en la consulta o en un `@list` propio. Los parámetros de la URL están disponibles como parámetros enlazados de la consulta, lo que permite resolver filtros de forma declarativa:

```
@list_query = "
    select sys_pk, codigo, nombre, saldo
    from cliente
    where ifnull(sys_deleted,0)=0
      and (@estado='' or estado=@estado)
      and (@texto=''  or codigo=@texto or nombre like concat('%',@texto,'%'))
    order by nombre
    limit 250;
"
```

Con esa consulta, una solicitud a `.../cliente/?estado=activo&texto=lopez` filtra sin escribir código.

Para ordenamiento dinámico o paginación con conteo total necesitará un `@list` propio. Si construye la cláusula de orden a partir de un parámetro de la URL, valide el valor contra una lista blanca de columnas antes de interpolarlo.

### Puntos de extensión en la consulta

Cuando los filtros son numerosos o dependen unos de otros, conviene dejar puntos de extensión en la consulta y componerlos desde una función. La consulta se escribe una sola vez con marcadores de sustitución textual, y la función construye el texto que se insertará en cada uno:

```
@list_query = "
    select vnt.sys_pk, vnt.referencia, vnt.fecha, cte.nombre as cliente
        #<selects>
    from venta vnt
        inner join cliente cte on cte.sys_pk = vnt.icliente
        #<joins>
    where not ifnull(vnt.sys_deleted,0)
        #<conditions>
    order by vnt.fecha desc, vnt.referencia desc
    limit #<@max_filas>;
"

componer_filtros::&params
{
    // Valores predeterminados de los filtros
    if not(field.exist(params,"estado")) { params<"estado"> : -1 }

    conditions = ""
    joins      = ""
    selects    = ""

    if @@(params,"#estado") >= 0 { conditions = conditions + " and vnt.statusadministrativo = @estado" }
    if trim(@@(params,"$texto")) != ""
    {
        conditions = conditions + " and (vnt.referencia like concat('%',@texto,'%') or cte.nombre like concat('%',@texto,'%'))"
    }

    @list_query = ftext(@list_query)
}
```

El punto importante es la separación: los **nombres** de columna y los fragmentos de sentencia se insertan como texto mediante `#<...>`, mientras que los **valores** siempre viajan como parámetros enlazados (`@estado`, `@texto`). Nunca concatene un valor recibido del cliente dentro de la cadena.

La misma técnica sirve para las consultas de lectura y de detalle. Es también la forma natural de reutilizar una consulta entre varias funciones que necesitan filtrarla de manera distinta.

## Concurrencia

### Bloqueo optimista

El campo `sys_recver` implementa bloqueo optimista. La sentencia de actualización se ejecuta con la forma:

```sql
UPDATE tabla SET sys_recver = sys_recver + 1, … WHERE (sys_pk = X AND sys_recver = Z)
```

donde `Z` es la versión que tenía el registro cuando se leyó.

**En las actualizaciones de entidades que incluyan `sys_recver`, siempre deberá enviarlo en la carga útil.** Si el valor no coincide con el almacenado, la actualización no afecta ninguna fila y se produce un error.

### Bloqueo pesimista

El bloqueo pesimista por fila se implementa con el campo `sys_lock` y las funciones `dbr.lock`, `dbr.unlock` y `dbr.chklock`. Ver [Biblioteca dbr](../../Bibliotecas-de-funciones/dbr/dbr.md).

El marco **no impone ni retira bloqueos**: establecerlos y liberarlos corresponde al modelo específico de la entidad, o a la capa de aplicación que gobierne la edición.

Ahora bien, el marco **sí los respeta al escribir**, porque las operaciones de alta y modificación guardan mediante `dbr.save`, que comprueba el bloqueo antes de afectar la fila. Esto vale tanto para el registro maestro como para las filas de detalle declaradas en `@childs`. Si otro componente del sistema mantiene un bloqueo sobre una fila, una modificación a través de CRUD-L no la sobrescribirá.

La comprobación **no** alcanza a las operaciones de baja. Ni la eliminación de la entidad ni la eliminación de filas huérfanas del detalle pasan por `dbr.save`. Si su aplicación emplea bloqueo pesimista y necesita que la baja lo respete, verifíquelo con `dbr.chklock` desde su `@delete` o su `@custom_delete` antes de eliminar.

Un flujo típico de edición con bloqueo consiste en imponerlo al entregar el formulario y retirarlo al guardar o al cancelar:

```
read_con_bloqueo::&db, &params
{
    ref entidad = entity.read(db,params)
    idbloqueo = dbr.lock(db, @table_name, @@(entidad,"#sys_pk"), sesion)
    if idbloqueo == 0 { do rise_error(423,"El registro está siendo editado por otro usuario") }
    entidad<"---idbloqueo"> : idbloqueo
    return entidad
}
```

El identificador del bloqueo viaja al formulario y regresa como campo de control, de modo que la operación de guardado pueda retirarlo. Ver [Campos de control](prg-y-redireccion.md#campos-de-control).

## Campos de sistema

Puede usar como identificador `sys_pk`, `sys_guid` o cualquier campo que defina como tal mediante `_key` o `@keyfield`.

El marco no filtra por sí mismo los campos de control que lleguen en la carga útil. Si su punto final es alcanzable desde el exterior, o si la interfaz reenvía la entidad completa tal como la leyó, declare explícitamente los campos que no deben afectarse:

```
@create_exclude_fields = "sys_lastuser"
@update_exclude_fields = "sys_pk,sys_guid,sys_dtcreated,sys_user"
```

`sys_recver` es la excepción deliberada: debe llegar en la carga útil para que funcione el control de concurrencia.

Para el catálogo completo de campos de control y su propósito, ver [Biblioteca dbr](../../Bibliotecas-de-funciones/dbr/dbr.md).

## La relectura posterior al alta y a la modificación

Las implementaciones predeterminadas de alta y modificación **releen la entidad invocando el puntero `@read`**, no la función `entity.read`.

Esto tiene una consecuencia práctica. Si su modelo sustituye `@read` por una función que devuelve una estructura enriquecida, por ejemplo la entidad más catálogos auxiliares para el formulario, entonces el alta y la modificación devolverán también esa estructura enriquecida, y no la entidad.

Cuando eso no sea lo deseado, use una variable propia que su `@read` consulte para decidir qué devolver:

```
read_venta::&db, &params
{
    if not(isset("@read_entity_only")) { @read_entity_only = @false }
    if @read_entity_only { return entity.read(db,params) }

    ref entidad = entity.read(db,params)
    new data
    {
        @"entity*": entidad
        @"catalogos*": cargar_catalogos(db)
    }
    return data
}
```

y actívela al principio de sus funciones de alta y modificación:

```
@read_entity_only = @true
```


## Documentos relacionados

* [Entidades maestro-detalle](maestro-detalle.md)
* [El controlador](controlador.md)
* [Referencia de parámetros y variables](referencia.md)
