Harbour ORM Reference Manual (HDBC)
Welcome, colleague. This manual details the mechanics of the ORM (Object-Relational Mapping) layer included in the HDBC ecosystem, inspired by the Active Record and modern Query Builders patterns. This ORM provides an elegant, object-oriented, and fluid interface to interact with your databases, abstracting pure SQL statements and allowing you to manipulate records exactly as if they were native Harbour objects.
---
Index
- Setup and Fundamentals
- THModel (Active Record and Query Builder)
- THCollection (Data Collections)
- Schema Builder (Table Creation)
- Grammar Resolution (THGrammar)
---
1. Setup and Fundamentals
To use the ORM, you must have an active connection originated from the core in
Initial Binding:
#include "hdbc.ch"
// 1. Create your standard C++ connection
LOCAL oDb := THDbc():new( HDBC_DRIVER_SQLITE )
oDb:connect( "database=app.db" )
// 2. Globally mount the connection to the ORM core
THModel():setConnection( oDb )By doing this, any class inheriting from
---
2. THModel (Active Record and Query Builder)
The
To create a custom model, simply inherit from
// model_user.prg
#include "hbclass.ch"
CLASS User FROM THModel
// Override config (Optional, ORM assumes conventions otherwise)
DATA table_name INIT "users"
DATA primary_key INIT "id"
DATA timestamps INIT .T. // Automatically manages created_at and updated_at
DATA useSoftDeletes INIT .F. // Employs deleted_at instead of physical dropping
ENDCLASS2.1 Column Access and CRUD Operations (Active Record)
Interact with a single record by materializing the remote database row onto the target instance object.
Mapping Attributes as Physical Object Properties
Row boundaries are primarily deposited underneath the target's internal dictionary mapping payload, accessible conventionally through
CLASS Category FROM THModel
DATA table_name INIT "categories"
// Explicit mapping = Fluent OOP `oCat:name` property access
DATA id, name, description
ENDCLASSFind by ID (
LOCAL oCat := Category():find( 1 )
IF oCat != nil
// Formally routed
? "ID:", oCat:getAttribute( "id" )
// Fluid property mapping! (Requires assigning `DATA name` on class creation)
? "Name:", oCat:name
? "Details:", oCat:description
ENDIFCreation (
You can insert data manually via assignments or massively (
// Option A: Mass-assignment (returns the successfully saved object)
LOCAL oUser := User():create( { "name" => "Manu", "email" => "manu@test.com" } )
// Option B: Instantiate and save manually
LOCAL oNew := User():new()
oNew:setAttribute( "name", "Pepe" )
oNew:save()Updating (
LOCAL oUser := User():find( 5 )
oUser:setAttribute( "name", "Jose" )
oUser:save() // Triggers the UPDATE command automaticallyDeletion (
LOCAL oUser := User():find( 5 )
oUser:delete() // Physical drop or Logical hide if useSoftDeletes is .T.2.2 Query Builder (Chained Queries)
Leverage fluid syntax to synthesize complex filters without littering your prg code with raw textual SQL strings. They will reliably output
Multiple Selections (
// All active users
LOCAL oActives := User():where( "status", "=", "ACTIVE" ):get()
// Array batch retrieval
LOCAL oVIPS := User():whereIn( "role_id", {1, 2} ):order( "name DESC" ):get()Performance Tip (
mutable ): By convention, chainable queries proactively clone the base object whenever you route a clause (where() ). If your chaining gets significantly long, enable the mutable path:
User():mutable(.T.):where(...):orderBy(...):limit(10):get()
Groupings and Offsets
LOCAL oStats := User():select( "role_id", "COUNT(*) as total" ) ;
:groupBy( "role_id" ) ;
:having( "total", ">", 5 ) ;
:limit( 10 ) ;
:get()Obtain a single match (
LOCAL oPrime := User():where( "score", ">", 1000 ):first()Numeric Aggregators
LOCAL nMedia := User():where( "status", "=", 1 ):avg( "edad" )
LOCAL nTotal := User():count()2.3 Utilities and Upsert
upsert( hMatch, hValues ) : Highly convenient. Attempts an update if thehMatch conditions score a hit; otherwise, injects a new row seamlessly combining the payload of both Hashes under the hood.firstOrCreate( hMatch, hValues ) andfirstOrNew( hMatch, hValues ) : Pinpoints a record. If unfound, returns it fully persisted (OrCreate ) or solely as a temporary memory instance (OrNew ).pluck( cCol ) : If you only desire an elemental native array{"Pepe", "Manu"} , you may skip heavy object hydration:LOCAL aNames := User():where("age",">",18):pluck("name")
3. Relationships and Advanced Model Features
The ORM truly excels when it comes to untangling scattered records (Data Relationships) and regulating the lifetime behavior span through automated events.
3.1 Relationships
You may link varying models inherently sidestepping tedious bare-metal SQL
CLASS User FROM THModel
// ...
METHOD profile()
METHOD posts()
METHOD roles()
ENDCLASS
// 1 to 1 Mapping
METHOD profile() CLASS User ; return ::hasOne( "Profile", "user_id" )
// 1 to N Mapping
METHOD posts() CLASS User ; return ::hasMany( "Post", "user_id" )
// N to N (Relies on an intermediate pivot juncture table)
METHOD roles() CLASS User ; return ::belongsToMany( "Role", "role_user", "user_id", "role_id" )Executing Relationships:
LOCAL oUser := User():find(5)
// Lazy Loading (Fetched from DB exclusively upon invocation)
? "Profile bio:", oUser:profile:first():bio
? "Amounts of written posts:", oUser:posts:count()
// Eager Loading (Optimal pre-hydration technique to counter N+1 query struggles)
LOCAL oUsers := User():with("posts"):all()
oUsers:each({|u| ? u:name, u:posts:count() })3.2 Accessors (Read Mutators)
Accessors grant you the power to dynamically skew or reformat raw physical database attributes whenever evaluated by assembling methods following the
CLASS User FROM THModel
// ...
METHOD getNameAttribute( cName )
ENDCLASS
METHOD getNameAttribute( cName ) CLASS User
return Upper( cName ) + " (Registered)"3.3 Observers
Intercept a model entity life cycle queuing backend-managed triggers written fully in Harbour logic, such as
// First, declare an observer class grouping the logic
CLASS UserObserver
METHOD saving( oModel )
ENDCLASS
PROCEDURE saving( oModel ) CLASS UserObserver
? "About to endure persistence round for:", oModel:name
return
// Engage it anywhere on your workflow tree:
User():observe( "UserObserver" )
LOCAL oU := User():new()
oU:name := "Test"
oU:save() // <- Will instantly fire: "About to endure persistence round for: Test"3.4 Scopes (Reusable Query Boundaries)
Whenever dealing repeatedly regarding recursive logic conditionals (ex: Gathering Users assigned exclusively to admin tiers, or items presently in-stock), enclose them seamlessly behind models exploiting the prefix
CLASS User FROM THModel
// ...
METHOD scopeActives( oQuery )
ENDCLASS
METHOD scopeActives( oQuery ) CLASS User ; return oQuery:where( "status", "=", 1 )
// Implementation string:
LOCAL oActives := User():scope( "actives" ):order("id DESC"):get()3.5 Pagination & Chunking
Resist inflating memory boundaries calling
// Traditional Pagination (Splendid match facing API routes)
LOCAL oPager := User():paginate( 50, 1 ) // (ItemsPerPage, TargetPageOffset)
? "Total pages calculated:", oPager["total_pages"]
? "Matrix length per iteration:", Len( oPager["data"] )
// Memory-Friendly Chunking iteration (Ideal rendering heavy background scripts)
User():chunk( 1000, {|oSplittedChunk| ;
oSplittedChunk:each({|oU| oU:slowMailerDeliveryMethod() }), ;
.T. ; // Issue .F. manually whenever wanting to halt chunk distribution loop
})---
4. THCollection (Data Collections)
The mass yield of a Query Builder execution (
LOCAL oUsersColl := User():where("status", "=", 1):get()
? "Records found:", oUsersColl:count()
// The class is completely compatible with FOR EACH standard loops
LOCAL oUser
FOR EACH oUser IN oUsersColl
? oUser:getAttribute("email")
NEXTIntegral Functional Collection Methods
each( bBlock ) : Transpiles the CodeBlock execution pointing directly against the wrapped element instance.map( bBlock ) : Transforms the totality emitting a brand new modifiedTHCollection array.filter( bBlock ) : Helpful for distilling locally downloaded structures evaluating.T. / .F. blocks.isEmpty() andfirst() .toJson() : Fundamental feature for web-based services. Compiles a serialized String layout encompassing the global arrays into uniform entities instantly. It's essentially what you return to clients out of a Rest endpoint.
---
4. Schema Builder (Table Creation & Migration)
HDBC offers DDL abstractions relying on
Table creation is accomplished by passing a codeblock to the
#include "hdbc.ch"
// On your boot configuration / migration files:
THSchema():create( "users", {| table | ;
table:id(), ; // Generates Auto_Increment integer Primary Key
table:string("email", 120), ; // Capped length Varchar (120 chars)
table:string("password", 255), ; // Varchar (255 chars)
table:boolean("active"), ; // Truthy Booleans
table:decimal("balance", 10, 2), ; // Exact Monetary formats (Precision 10, Scale 2)
table:text("bio"), ; // Unlimited bulk text / Memo
table:json("config"), ; // Special JSON column
table:timestamps() ; // Injects `created_at` and `updated_at` (Datetimes)
} )The localized
Explicit Object-Oriented Table Creation
If you prefer not to use codeblocks (closures), or if your application requires assembling the architecture dynamically element by element during runtime, you can instantiate
// You may optionally drop the structure beforehand
THSchema():new( "products" ):dropIfExists()
LOCAL oSchema := THSchema():new( "products" )
oSchema:add( THField():id() )
oSchema:add( THField():string( "name", 150 ) )
oSchema:add( THField():decimal( "price", 10, 2 ) )
oSchema:add( THField():boolean( "active" ) )
oSchema:add( THField():timestamps() )
oSchema:create()Available Column Types
The Table Blueprint exposes the following methods for declaring column typologies:
| Blueprint Method | Resulting Column Description |
|---|---|
| Auto-Increment Primary Key. Defaults to | |
| Equivalent to | |
| Standard numeric integer ( | |
| Floating point decimal number ( | |
| Exact decimal. | |
| True/false values. Mapped to secure 1/0 integers if engine demands it. | |
| Date value solely ( | |
| Exact Date and Time structure ( | |
| SQL Temporal Marker ( | |
| Massive text storage ( | |
| JSON structure if natively encapsulated by driver, pure String otherwise. | |
| Helper utility. Automatically generates |
Post-Operations and Maintenance
You can also seamlessly intervene or destroy fully grown structures:
Destroying Tables:
THSchema():drop("users")
THSchema():dropIfExists("users") // Prevents crashing if table was already wiped out---
5. Grammar Resolution (THGrammar)
The
Each individual database driver integrated through HDBC (e.g. Postgres, MariaDB) natively introduces into the ORM their proprietary child extension originating from
You are not required to instantiate it yourself under normal conditions, nevertheless, you hold complete permission querying structural behaviors querying back towards your host THDbc:
// Retrieve the live dialect injector coupled to your current session
LOCAL oGrammar := THModel():getGrammar()
? oGrammar:sqlListTables()---
6. Complete Practical Example
This final chapter merges previously digested knowledge setting up an end-to-end sandbox application. It features booting an interconnected
#include "hdbc_conn.ch"
#include "hbclass.ch"
#include "hdbc.ch"
// ---------------------------------------------------------
// 1. MODELS DEFINITION
// ---------------------------------------------------------
CLASS Author FROM THModel
DATA table_name INIT "authors"
DATA id, name
METHOD books() // 1:N Relationship Endpoint
ENDCLASS
METHOD books() CLASS Author ; return ::hasMany( "Book", "author_id" )
CLASS Book FROM THModel
DATA table_name INIT "books"
DATA id, author_id, title, in_stock
METHOD author() // Inverse N:1 Mapping
ENDCLASS
METHOD author() CLASS Book ; return ::belongsTo( "Author", "author_id" )
// ---------------------------------------------------------
// 2. MAIN EXECUTION PROCEDURE
// ---------------------------------------------------------
PROCEDURE Main()
LOCAL oDb := THDbc():new( HDBC_DRIVER_SQLITE )
LOCAL oAuthor, oBook, oAuthorsSet
// A. CORE CONNECTION MOUNTING
oDb:connect( "database=library.db" )
THModel():setConnection( oDb )
// B. SCHEMA BUILDER (Creating structures if lacking)
// Create authors table
THSchema():new( "authors" ):dropIfExists()
oSchema := THSchema():new( "authors" )
oSchema:add( THField():id() )
oSchema:add( THField():string( "name", 150 ) )
oSchema:add( THField():timestamps() )
oSchema:create()
// Create books table
THSchema():new( "books" ):dropIfExists()
oSchema := THSchema():new( "books" )
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. CREATE (Mass Fill Injections)
// Direct Table Hydration
Author():create({ "id" => 1, "name" => "Edgar Allan Poe" })
Author():create({ "id" => 2, "name" => "H. P. Lovecraft" })
// Relational Book Inserts manually tying FK links
Book():create({ "author_id" => 1, "title" => "The Raven", "in_stock" => 1 })
Book():create({ "author_id" => 1, "title" => "The Tell-Tale Heart", "in_stock" => 0 })
Book():create({ "author_id" => 2, "title" => "The Call of Cthulhu", "in_stock" => 1 })
// D. READ & LISTINGS (Preloading with Eager Loading)
? "---- CATALOGUE BROWSER ----"
oAuthorsSet := Author():with("books"):all()
oAuthorsSet:each({|a| ;
QOut( "=> Found Author: " + a:name ), ;
a:books:each({|b| ;
QOut( " * Logged Title: " + b:title + " (Stock Flag: " + hb_ValToStr(b:in_stock) + ")" ) ;
}) ;
})
// E. MODIFICATIONS (Updates)
? "---- RE-STOCKING INVENTORY ----"
oBook := Book():where("title", "LIKE", "%Tell-Tale%"):first()
IF oBook != nil
oBook:in_stock := 1 // Changing the local attribute state
oBook:save() // Broadcasting physical SQL update remotely
? "Title successfully restocked:", oBook:title
ENDIF
// F. DELETING (Dropping Records)
? "---- PRUNING OLD DATA ----"
oAuthor := Author():find( 2 ) // Unearthing Lovecraft
IF oAuthor != nil
// Sweep inner relational associations orderly sequentially
oAuthor:books:each({|b| b:delete() })
// Ultimately drop the parent
oAuthor:delete()
? "Target author alongside its linked bibliography effectively destroyed."
ENDIF
THModel():end()
oDb:disconnect()
RETURN---
Sevilla - Andalucía