# 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, ¶ms, &data` | La entidad creada, completa | | `@read` | `&db, ¶ms` | La entidad solicitada | | `@update` | `&db, ¶ms, &data` | La entidad actualizada, completa | | `@delete` | `&db, ¶ms` | Nulo | | `@list` | `&db, ¶ms` | Lista de entidades | | `@blank` | `&db, ¶ms` | Un elemento vacío o con valores predeterminados | | `@custom_delete` | `&db, table_name, sys_pk` | Sin valor de retorno | Ejemplo: ``` mifuncion_update::&db,¶ms,&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, ¶ms, &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 # from venta vnt inner join cliente cte on cte.sys_pk = vnt.icliente # where not ifnull(vnt.sys_deleted,0) # order by vnt.fecha desc, vnt.referencia desc limit #<@max_filas>; " componer_filtros::¶ms { // 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, ¶ms { 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, ¶ms { 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)