Manual de Referencia ORM para Harbour (HDBC)

Post Reply
xmanuel
Posts: 817
Joined: Sun Jun 15, 2008 7:47 pm
Location: Sevilla
Been thanked: 10 times
Contact:

Manual de Referencia ORM para Harbour (HDBC)

Post by xmanuel »

Manual de Referencia ORM para Harbour (HDBC)

Bienvenido compañero. Este manual detalla el funcionamiento de la capa ORM (Object-Relational Mapping) incluida en el ecosistema HDBC, inspirada en el patrón Active Record y en Query Builders modernos. Este ORM proporciona una interfaz elegante, orientada a objetos y fluida para interactuar con tus bases de datos, abstrallendo las sentencias SQL puras y permitiendo trabajar con los registros como si fueran objetos nativos en Harbour.


Índice

  1. Configuración y Conceptos Básicos
  2. THModel (Active Record y Query Builder)
  3. THCollection (Colecciones de Datos)
  4. Schema Builder (Creación de Tablas)
  5. Resolución de Gramáticas (THGrammar)

1. Configuración y Conceptos Básicos

Para usar el ORM, debes disponer de una conexión activa originada desde el núcleo en hxdbc (habitualmente un objeto THDBC). El ORM necesita que se defina la conexión por defecto de forma global por hilo (Thread Static) antes de invocar los modelos.

Activación Inicial:

Code: Select all | Expand

#include "hdbc.ch"

// 1. Crear tu conexion C++ habitual
LOCAL oDb := THDbc():new( HDBC_DRIVER_SQLITE )
oDb:connect( "database=app.db" )

// 2. Acoplar globalmente la conexion al nucleo del ORM
THModel():setConnection( oDb )

Al hacer esto, cualquier clase que herede de THModel sabrá hacia dónde lanzar sus instrucciones.


2. THModel (Active Record y Query Builder)

La clase THModel es el corazón del sistema. Implementa tanto las propiedades del modelo (Active Record) como los métodos para encadenar instrucciones (Query Builder).

Para crear un modelo personalizado, simplemente hereda de THModel:

Code: Select all | Expand

// model_user.prg
#include "hbclass.ch"

CLASS User FROM THModel
   // Sobreescribir configuración (Opcional, ORM asume convenciones)
   DATA table_name INIT "users"
   DATA primary_key INIT "id"
   DATA timestamps INIT .T.       // Controla automáticamente created_at y updated_at
   DATA useSoftDeletes INIT .F.   // Usa deleted_at en vez de borrar físicamente
ENDCLASS

2.1 Acceso a Columnas y Operaciones CRUD (Active Record)

Interactúa con un único registro materializando la fila de la base de datos de vuelta al objeto.

Mapeo de Atributos como Propiedades OOP
Las columnas de la tabla se cargan en el diccionario interno del modelo, pudiendo acceder a ellas mediante getAttribute("campo"). Sin embargo, para un código más limpio y tipado de estilo "OOP puro", puedes enlistar tus columnas exactas en la cabecera de la Clase mediante sentencias DATA. Esto indica a Harbour que conecte las propiedades de la clase física directamente con la tupla cargada en memoria permitiendo escritura y lectura fluida por punto/dos puntos:

Code: Select all | Expand

CLASS Category FROM THModel
   DATA table_name INIT "categorias"
   
   // Mapeando explícitamente atributos = sintaxis rápida OOP oCat:name
   DATA id, name, description 
ENDCLASS

Buscar por ID (find) y Leer Atributos

Code: Select all | Expand

LOCAL oCat := Category():find( 1 )
IF oCat != nil
   // Modo tradicional
   ? "ID:", oCat:getAttribute( "id" )
   
   // ¡Modo Fluido! (Requiere haber declarado el `DATA name` arriba)
   ? "Nombre:", oCat:name
   ? "Detalle:", oCat:description
ENDIF

Creación (create / save)
Puedes insertar datos asignando manualmente o masivamente (fill()).

Code: Select all | Expand

// Opción A: Asignación masiva (devuelve el objeto guardado)
LOCAL oUser := User():create( { "name" => "Manu", "email" => "manu@test.com" } )

// Opción B: Instanciar y guardar
LOCAL oNuevo := User():new()
oNuevo:setAttribute( "name", "Pepe" )
oNuevo:save()

Actualización (update / save)

Code: Select all | Expand

LOCAL oUser := User():find( 5 )
oUser:setAttribute( "name", "Jose" )
oUser:save() // Lanza automáticamente el UPDATE

Eliminación (delete)

Code: Select all | Expand

LOCAL oUser := User():find( 5 )
oUser:delete() // Físico o Lógico si useSoftDeletes es .T.

2.2 Query Builder (Consultas Encadenadas)

Aprovecha la sintaxis fluida para generar complejas búsquedas sin ensuciar el código prg con sentencias textuales SQL. Devuelven siempre objetos de tipo THCollection.

Selección Múltiple (where, whereIn, get)

Code: Select all | Expand

// Todos los usuarios activos
LOCAL oActivos := User():where( "status", "=", "ACTIVE" ):get()

// Buscar en array
LOCAL oVIPS := User():whereIn( "role_id", {1, 2} ):order( "name DESC" ):get()

Consejo de rendimiento (mutable): Por convención, las queries clonan el objeto base preventivamente cada vez que encadenas un cláusula (where()). Si tu cadena es larguísima, aplica el modo mutable:
User():mutable(.T.):where(...):orderBy(...):limit(10):get()

Agrupaciones y Limitaciones

Code: Select all | Expand

LOCAL oStats := User():select( "role_id", "COUNT(*) as total" ) ;
                      :groupBy( "role_id" ) ;
                      :having( "total", ">", 5 ) ;
                      :limit( 10 ) ;
                      :get()

Obtener un solo resultado (first)

Code: Select all | Expand

LOCAL oPrime := User():where( "score", ">", 1000 ):first()

Agregadores Numéricos

Code: Select all | Expand

LOCAL nMedia := User():where( "status", "=", 1 ):avg( "edad" )
LOCAL nTodos := User():count()

2.3 Utilidades y Upsert

  • upsert( hMatch, hValues ): Muy útil. Intenta actualizar si los condicionales en hMatch hacen diana, y si la fila no existe la inserta uniendo los dos Hashes internamente.
  • firstOrCreate( hMatch, hValues ) y firstOrNew( hMatch, hValues ): Buscan un registro. Si no está lo devuelven persistido (OrCreate) o como instancia temporal en memoria (OrNew).
  • pluck( cCol ): Si sólo quieres una matriz elemental nativa {"Pepe", "Manu"} puedes saltarte los objetos:

    Code: Select all | Expand

      LOCAL aNombres := User():where("age",">",18):pluck("name")

3. Relaciones y Características Avanzadas del Modelo

El ORM brilla especialmente al manejar el entrelazado de datos (Data Relationships) y gestionar el ciclo de vida de los registros con eventos automáticos.

3.1 Relaciones (Relationships)

Con el ORM puedes vincular modelos sin necesidad de programar complejos JOINs manuales. Admite los principales modos de relación (Has One, Has Many, Belongs To, Belongs To Many).

Code: Select all | Expand

CLASS User FROM THModel
   // ...
   METHOD profile()
   METHOD posts()
   METHOD roles()
ENDCLASS

// 1 a 1
METHOD profile() CLASS User ; return ::hasOne( "Profile", "user_id" )

// 1 a N
METHOD posts() CLASS User ; return ::hasMany( "Post", "user_id" )

// N a N (Tabla pivote intermedia)
METHOD roles() CLASS User ; return ::belongsToMany( "Role", "role_user", "user_id", "role_id" )

Uso de Relaciones:

Code: Select all | Expand

LOCAL oUser := User():find(5)

// Lazy Loading (se carga de BBDD al invocarse)
? "Bio de su perfil:", oUser:profile:first():bio
? "Tantos posts escritos:", oUser:posts:count()

// Eager Loading (Precarga óptima para evitar el problema N+1)
LOCAL oUsers := User():with("posts"):all()
oUsers:each({|u| ? u:name, u:posts:count() })

3.2 Accessors (Mutadores de Lectura)

Puedes modificar cómo se devuelve un dato de la base de datos sin transformar el valor físico almacenado creando métodos get[Columna]Attribute:

Code: Select all | Expand

CLASS User FROM THModel
   // ...
   METHOD getNameAttribute( cName )
ENDCLASS

METHOD getNameAttribute( cName ) CLASS User 
   return Upper( cName ) + " (Registrado)"

3.3 Observadores (Observers)

Intercepta el ciclo de vida de un modelo encolando disparadores (Triggers) de Harbour puros en el backend, por ejemplo saving o saved.

Code: Select all | Expand

// Crea una clase de observador
CLASS UserObserver
   METHOD saving( oModel )
ENDCLASS

PROCEDURE saving( oModel ) CLASS UserObserver
   ? "El modelo se va a guardar:", oModel:name
return

// Actívalo en cualquier parte:
User():observe( "UserObserver" )
LOCAL oU := User():new()
oU:name := "Test"
oU:save() // <- Print: El modelo se va a guardar: Test

3.4 Scopes (Ámbitos de Búsqueda de Reuso)

Si tienes filtros recurrentes (ej: Usuarios que son administradores o que están vivos), aísla esa lógica en tus modelos usando el prefijo scope:

Code: Select all | Expand

CLASS User FROM THModel
   // ...
   METHOD scopeActivos( oQuery )
ENDCLASS

METHOD scopeActivos( oQuery ) CLASS User ; return oQuery:where( "status", "=", 1 )

// Uso:
LOCAL oActives := User():scope( "activos" ):order("id DESC"):get()

3.5 Paginadores y Chunking

En lugar de lanzar a la memoria una tabla con 50.000 registros de golpe usando get(), puedes paginarla ordenadamente para un datagrid web u optimizar procesos batch.

Code: Select all | Expand

// Paginación tradicional (útil para APIs)
LOCAL oPager := User():paginate( 50, 1 ) // (CantidadxPagina, Pagina)
? "Páginas totales:", oPager["total_pages"]
? "Registros matriz:", Len( oPager["data"] )

// Chunking para procesado silencioso en memoria baja
User():chunk( 1000, {|oColeccion_Trozo| ;
   oColeccion_Trozo:each({|oU| oU:sendEmailLento() }), ;
   .T. ; // Devuelve .F. para abortar procesamiento del chunk
})

4. THCollection (Colecciones de Datos)

El resultado masivo de un Query Builder (->get()) no es un Array pelado, es un objeto envoltorio de la matriz THCollection que facilita la iteración y el pase a formatos como JSON.

Code: Select all | Expand

LOCAL oUsersColl := User():where("status", "=", 1):get()

? "Registros encontrados:", oUsersColl:count()

// La clase es compatible con bucles FOR EACH
LOCAL oUser
FOR EACH oUser IN oUsersColl
   ? oUser:getAttribute("email")
NEXT

Métodos funcionales incorporados en la Colección

  • each( bBlock ): Aplica el CodeBlock a referenciando la instancia.
  • map( bBlock ): Transforma todo y te devuelve un nuevo THCollection.
  • filter( bBlock ): Para depurar resultados traidos en memoria evaluando un .T. / .F..
  • isEmpty() y first().
  • toJson(): Vital para apis web. Genera un String serializado de la matriz de todas las entidades automáticamente. Exáctamente lo que buscas devolver en un endpoint Rest.

4. Schema Builder (Creación y Migración de Tablas)

HDBC brinda abstracción DDL a través de THSchema. Te permite programar migraciones y creaciones de base de datos de manera agnóstica sin lidiar de forma nativa con las diferencias sintácticas entre SQLite, MySQL o PostgreSQL.

La creación de tablas se realiza pasando un bloque de código al método create, el cual recibe un objeto "blueprint" (representando la nueva tabla), que usarás para definir las columnas:

Code: Select all | Expand

#include "hdbc.ch"

// En tu bloque de inicializacion o herramienta migratoria:
THSchema():create( "users", {| table | ;
    table:id(), ;                                    // Genera Entero Clave Primaria Autonumerico
    table:string("email", 120), ;                    // Varchar limitado a 120 caracteres
    table:string("password", 255), ;                 // Varchar de 255
    table:boolean("active"), ;                       // Tipos Booleanos
    table:decimal("saldo", 10, 2), ;                 // Monetarios (Precisión 10, Escala 2)
    table:text("bio"), ;                             // Texto Largo / Memo
    table:json("config"), ;                          // Columna especial JSON
    table:timestamps() ;                             // Inyecta `created_at` y `updated_at` (Datetime)
} )

El objeto table recibido en el CodeBlock es en realidad una fábrica THBlueprint/THField. La capa gramatical traducirá transparentemente cada tipo a la versión física idónea según tu base de datos (Ej: boolean pasará a un INTEGER 1/0 si tu motor base no engloba tipos booleanos predeterminados, como SQLite).

Creación de Tablas de Forma Explícita (Orientada a Objetos)

Si prefieres no usar el uso de bloques (Closures) o estás construyendo la estructura de forma dinámica durante la ejecución de tu aplicación de forma procedimental, puedes instanciar directamente THSchema y agrupar los campos valiéndote de la factoría THField:

Code: Select all | Expand

// Opcionalmente puedes destruir tablas previas antes de crearlas
THSchema():new( "productos" ):dropIfExists()

LOCAL oSchema := THSchema():new( "productos" )
oSchema:add( THField():id() )
oSchema:add( THField():string( "nombre", 150 ) )
oSchema:add( THField():decimal( "precio", 10, 2 ) )
oSchema:add( THField():boolean( "activo" ) )
oSchema:add( THField():timestamps() )
oSchema:create()

Tipos de Columnas Disponibles

El Table Blueprint proporciona los siguientes métodos para declarar las tipologías de columnas de tus tablas:

MétodoDescripción del Tipo Resultante
table:id("nombre")Clave primaria Auto-Incremental. Por defecto el nombre es "id".
table:string("c", n)Equivalente a VARCHAR. Acepta un límite de longitud n.
table:integer("c")Número Entero estándar (INTEGER).
table:float("c")Número decimal de coma flotante (FLOAT).
table:decimal("c",p,s)Decimal exacto. p recisión total, s cantidad de decimales.
table:boolean("c")Valores verdadero/falso. Mapeado a enteros seguros 1/0 si el motor lo exige.
table:date("c")Únicamente fecha (DATE).
table:datetime("c")Fecha y Hora exactas (DATETIME).
table:timestamp("c")Marca temporal SQL (TIMESTAMP o equivalente).
table:text("c")Almacenamiento masivo de texto (TEXT o MEMO).
table:json("c")Estructura JSON si el driver lo encapsula, o String puro en su defecto.
table:timestamps()Helper. Genera autmáticamente created_at y updated_at.

Operaciones Posteriores y Mantenimiento

También puedes intervenir esquemas existentes o destruirlos de forma fluida:

Destruir Tablas:

Code: Select all | Expand

THSchema():drop("users")
THSchema():dropIfExists("users") // Evita excepciones si la tabla ya no existía

5. Resolución de Gramáticas (THGrammar)

La clase THGrammar se aloja detrás de los bastidores del ORM. Es una capa completamente Abstracta.

Cada driver individual en HDBC (Ej: Postgres, MariaDB) inyectará al ORM su propia extensión (Hija) de THGrammar que sabe cómo codificar al dialecto correcto y escapar las palabras clave o valores requeridos (Ej: LIMIT u OFFSET pueden diverger o los comillados \`` contra[]o""`).

No debes instanciarla tú explícitamente, pero puedes alterar aspectos globales y consultar la delegación actuando desde tu objeto THDbc:

Code: Select all | Expand

// Acceder al inyector de tu driver actual
LOCAL oGrammar := THModel():getGrammar()
? oGrammar:sqlListTables()

6. Ejemplo Práctico Completo

Aquí agrupamos todo el conocimiento de los bloques anteriores para levantar un escenario completo: crear las bases de datos de Autores y Libros, definir sus clases enlazadas, inyectar el juego de datos (Altas), leerlos relacionadamente (Listados), mutarlos (Modificaciones) y borrarlos (Bajas).

Code: Select all | Expand

#include "hdbc_conn.ch"
#include "hbclass.ch"
#include "hdbc.ch"

// ---------------------------------------------------------
// 1. DEFINICIÓN DE MODELOS
// ---------------------------------------------------------
CLASS Author FROM THModel
   DATA table_name INIT "autores"
   DATA id, name
   
   METHOD books() // Relacion 1:N
ENDCLASS

METHOD books() CLASS Author ; return ::hasMany( "Book", "author_id" )

CLASS Book FROM THModel
   DATA table_name INIT "libros"
   DATA id, author_id, title, in_stock
   
   METHOD author() // Inversa N:1
ENDCLASS

METHOD author() CLASS Book ; return ::belongsTo( "Author", "author_id" )

// ---------------------------------------------------------
// 2. FUNCIÓN PRINCIPAL Y EJECUCIÓN
// ---------------------------------------------------------
PROCEDURE Main()
   LOCAL oDb := THDbc():new( HDBC_DRIVER_SQLITE )
   LOCAL oAuthor, oBook, oAuthorsSet
   
   // A. CONEXIÓN AL NÚCLEO
   oDb:connect( "database=library.db" )
   THModel():setConnection( oDb )
   
   // B. GESTIÓN DEL ESQUEMA (Crear si no existe)
   
   // Crear tabla de autores
   THSchema():new( "autores" ):dropIfExists()
   oSchema := THSchema():new( "autores" )
   oSchema:add( THField():id() )
   oSchema:add( THField():string( "name", 150 ) )
   oSchema:add( THField():timestamps() )
   oSchema:create()
   
   // Crear tabla de libros
   THSchema():new( "libros" ):dropIfExists()
   oSchema := THSchema():new( "libros" )
   oSchema:add( THField():id() )
   oSchema:add( THField():integer( "author_id" ) )
   oSchema:add( THField():string( "title", 200 ) )
   oSchema:add( THField():boolean( "in_stock" ):default(.T.) )
   oSchema:add( THField():timestamps() )
   oSchema:create()
   
   // C. ALTAS (Inyección de Datos)
   // Alta masiva
   Author():create({ "id" => 1, "name" => "Gabriel Garcia Marquez" })
   Author():create({ "id" => 2, "name" => "Isabel Allende" })
   
   // Alta Relacional
   Book():create({ "author_id" => 1, "title" => "Cien Años de Soledad", "in_stock" => 1 })
   Book():create({ "author_id" => 1, "title" => "El Amor en Tiempos...", "in_stock" => 0 })
   Book():create({ "author_id" => 2, "title" => "La Casa de los Espiritus", "in_stock" => 1 })

   // D. LISTADOS Y BÚSQUEDAS (Eager Loading para Eficiencia)
   ? "---- LISTADO DE CATÁLOGO ----"
   oAuthorsSet := Author():with("books"):all()
   
   oAuthorsSet:each({|a| ;
       QOut( "=> Autor: " + a:name ), ;
       a:books:each({|b| ;
          QOut( "   * Libro: " + b:title + " (Stock: " + hb_ValToStr(b:in_stock) + ")" ) ;
       }) ;
   })
   
   // E. MODIFICACIONES (Update)
   ? "---- ACTUALIZANDO STOCK ----"
   oBook := Book():where("title", "LIKE", "%Amor en Tiempos%"):first()
   IF oBook != nil
      oBook:in_stock := 1 // Reposición de libro
      oBook:save()
      ? "Stock reabastecido de:", oBook:title
   ENDIF

   // F. BAJAS (Delete)
   ? "---- ELIMINANDO REGISTRO ----"
   oAuthor := Author():find( 2 ) // Buscando a Isabel Allende
   IF oAuthor != nil
      // Borrar todos sus libros secuencialmente primero usando su relación
      oAuthor:books:each({|b| b:delete() })
      
      // Borrar el autor
      oAuthor:delete()
      ? "Autor y libros asociados borrados correctamente."
   ENDIF
   
   THModel():end()
   oDb:disconnect()
RETURN

⚙️ Fin del manual ORM. Combina THModel con validaciones sólidas (rules) para gobernar las lógicas de negocio con elegancia y seguridad. 🚀

______________________________________________________________________________
Sevilla - Andalucía
User avatar
VictorCasajuana
Posts: 317
Joined: Wed Mar 28, 2018 4:38 pm
Location: Vinaròs
Has thanked: 24 times
Been thanked: 12 times
Contact:

Re: Manual de Referencia ORM para Harbour (HDBC)

Post by VictorCasajuana »

buen trabajo!

--------
¿ Y porque no ?
¿ And why not ?
Post Reply