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
- 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)
---
1. Configuraci贸n y Conceptos B谩sicos
Para usar el ORM, debes disponer de una conexi贸n activa originada desde el n煤cleo en
Activaci贸n Inicial:
#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
---
2. THModel (Active Record y Query Builder)
La clase
Para crear un modelo personalizado, simplemente hereda de
// 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
ENDCLASS2.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
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 (
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 (
Puedes insertar datos asignando manualmente o masivamente (
// 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 (
LOCAL oUser := User():find( 5 )
oUser:setAttribute( "name", "Jose" )
oUser:save() // Lanza autom谩ticamente el UPDATEEliminaci贸n (
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
Selecci贸n M煤ltiple (
// 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
LOCAL oStats := User():select( "role_id", "COUNT(*) as total" ) ;
:groupBy( "role_id" ) ;
:having( "total", ">", 5 ) ;
:limit( 10 ) ;
:get()Obtener un solo resultado (
LOCAL oPrime := User():where( "score", ">", 1000 ):first()Agregadores Num茅ricos
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 enhMatch hacen diana, y si la fila no existe la inserta uniendo los dos Hashes internamente.firstOrCreate( hMatch, hValues ) yfirstOrNew( 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: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
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:
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
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
// 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: Test3.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
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
// 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 (
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")
NEXTM茅todos funcionales incorporados en la Colecci贸n
each( bBlock ) : Aplica el CodeBlock a referenciando la instancia.map( bBlock ) : Transforma todo y te devuelve un nuevoTHCollection .filter( bBlock ) : Para depurar resultados traidos en memoria evaluando un.T. / .F. .isEmpty() yfirst() .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
La creaci贸n de tablas se realiza pasando un bloque de c贸digo al m茅todo
#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
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
// 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
| M茅todo | Descripci贸n del Tipo Resultante |
|---|---|
| Clave primaria Auto-Incremental. Por defecto el nombre es | |
| Equivalente a | |
| N煤mero Entero est谩ndar ( | |
| N煤mero decimal de coma flotante ( | |
| Decimal exacto. | |
| Valores verdadero/falso. Mapeado a enteros seguros 1/0 si el motor lo exige. | |
| 脷nicamente fecha ( | |
| Fecha y Hora exactas ( | |
| Marca temporal SQL ( | |
| Almacenamiento masivo de texto ( | |
| Estructura JSON si el driver lo encapsula, o String puro en su defecto. | |
| Helper. Genera autm谩ticamente |
Operaciones Posteriores y Mantenimiento
Tambi茅n puedes intervenir esquemas existentes o destruirlos de forma fluida:
Destruir Tablas:
THSchema():drop("users")
THSchema():dropIfExists("users") // Evita excepciones si la tabla ya no exist铆a---
5. Resoluci贸n de Gram谩ticas (THGrammar)
La clase
Cada driver individual en HDBC (Ej: Postgres, MariaDB) inyectar谩 al ORM su propia extensi贸n (Hija) de
No debes instanciarla t煤 expl铆citamente, pero puedes alterar aspectos globales y consultar la delegaci贸n actuando desde tu objeto THDbc:
// 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
#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---
Sevilla - Andaluc铆a