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- Configuración y Conceptos Básicos
- THModel (Active Record y Query Builder)
- THCollection (Colecciones de Datos)
- Schema Builder (Creación de Tablas)
- Resolución de Gramáticas (THGrammar)
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
ENDCLASSInteractú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
ENDCLASSBuscar 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
ENDIFCreació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" => "[email protected]" } )
// 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 UPDATEEliminación (delete)
Code: Select all | Expand
LOCAL oUser := User():find( 5 )
oUser:delete() // Físico o Lógico si useSoftDeletes es .T.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()- 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")
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() })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)"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: TestSi 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()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
})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- 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.
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()El Table Blueprint proporciona los siguientes métodos para declarar las tipologías de columnas de tus tablas:
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íaLa 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()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.