# 5. Object-Relational Mapping
Persist Your Objects
The ORM layer of mORMot 2 is implemented across the mormot.orm. units. It provides database-agnostic persistence for Delphi objects, supporting SQLite3, external SQL databases (PostgreSQL, Oracle, MS SQL, MySQL, etc.), MongoDB (ODM), and in-memory storage.
Generic data access is implemented by defining high-level objects as Delphi classes descending from TOrm. These classes serve multiple purposes:
All ORM functionality relies on the TOrm class (defined in mormot.orm.core). This abstract class provides built-in methods for generic ORM processing.
TOrm defines a primary key field as ID: TID (where TID = type Int64):
type
TID = type Int64;
TOrm = class(TObject)
// ...
property ID: TID read GetID write fID;
end;
The ORM relies on Int64 primary keys, matching SQLite3's RowID. While you might prefer TEXT or GUID primary keys in traditional RDBMS design, integer keys are more efficient for ORM internals. You can always define secondary unique keys using stored AS_UNIQUE.
All published properties of TOrm descendants are automatically mapped to database columns:
uses
mormot.core.base,
mormot.orm.core;
type
/// Enumeration for gender
TSex = (sFemale, sMale);
/// Table for Baby records
TOrmBaby = class(TOrm)
private
fName: RawUtf8;
fAddress: RawUtf8;
fBirthDate: TDateTime;
fSex: TSex;
published
property Name: RawUtf8 read fName write fName;
property Address: RawUtf8 read fAddress write fAddress;
property BirthDate: TDateTime read fBirthDate write fBirthDate;
property Sex: TSex read fSex write fSex;
end;
By adding TOrmBaby to a TOrmModel, the corresponding Baby table is automatically created. No SQL required.
| Delphi Type | SQLite3 Type | Notes |
|---|---|---|
Byte, Word, Integer, Cardinal, Int64 |
INTEGER | |
Boolean |
INTEGER | 0 = false, non-zero = true |
| Enumeration | INTEGER | Stored as ordinal value |
| Set | INTEGER | Bit-packed (up to 64 elements) |
Single, Double, Extended |
FLOAT | Extended stored as double |
Currency |
FLOAT | Fixed 4 decimals, no rounding errors |
RawUtf8 |
TEXT | Preferred for text fields |
string |
TEXT | Use RawUtf8 instead when possible |
TDateTime |
TEXT | ISO 8601 with second resolution |
TDateTimeMS |
TEXT | ISO 8601 with millisecond resolution |
TTimeLog |
INTEGER | Compact proprietary format |
TModTime |
INTEGER | Auto-updated on modification |
TCreateTime |
INTEGER | Auto-set on creation |
TUnixTime |
INTEGER | Seconds since 1970-01-01 |
TUnixMSTime |
INTEGER | Milliseconds since 1970-01-01 |
TOrm |
INTEGER | Foreign key (RowID of another table) |
TID |
INTEGER | 64-bit foreign key (no table info) |
TOrmMany |
(pivot table) | Many-to-many relationship |
TRecordReference |
INTEGER | Reference to any table in model |
TSessionUserID |
INTEGER | Auto-filled with current user ID |
TPersistent |
TEXT | JSON object |
TCollection |
TEXT | JSON array of objects |
TObjectList |
TEXT | JSON array (requires registration) |
TStrings |
TEXT | JSON array of strings |
RawBlob |
BLOB | Binary data |
| Dynamic arrays | BLOB | Binary format via TDynArray.SaveTo |
Variant |
TEXT | JSON (or TDocVariant) |
TNullableInteger, etc. |
varies | Nullable types supporting SQL NULL |
record |
TEXT | JSON (Delphi XE5+) |
TRecordVersion |
INTEGER | Monotonic change counter |
Use special attributes in property declarations:
type
TOrmDiaper = class(TOrm)
private
fSerialNumber: RawUtf8;
fModel: TOrmDiaperModel;
fBaby: TOrmBaby;
published
property SerialNumber: RawUtf8
index 30 // Max 30 chars for external DB
read fSerialNumber write fSerialNumber
stored AS_UNIQUE; // Creates unique index
property Model: TOrmDiaperModel read fModel write fModel;
property Baby: TOrmBaby read fBaby write fBaby;
end;
stored AS_UNIQUE: Creates a unique database indexindex N: Maximum character length for external databasesindex N (dynamic arrays): Used for TOrm.DynArray(N) wrapper access
The preferred type for text storage is RawUtf8. This ensures:
// Business layer: Use RawUtf8
property CustomerName: RawUtf8 read fCustomerName write fCustomerName;
// UI layer: Convert for display
var
displayName: string;
begin
displayName := Utf8ToString(Customer.CustomerName);
Edit1.Text := displayName;
end;
Domain-Driven Tip: Using RawUtf8 in your domain layer prevents accidental coupling between business logic and presentation.
Stored as ISO 8601 text in the database:
property CreatedAt: TDateTime read fCreatedAt write fCreatedAt;
property PreciseTime: TDateTimeMS read fPreciseTime write fPreciseTime; // With milliseconds
Stored as INTEGER for fast comparison:
property LastModified: TModTime read fLastModified write fLastModified; // Auto-updated
property Created: TCreateTime read fCreated write fCreated; // Auto-set once
property Timestamp: TTimeLog read fTimestamp write fTimestamp; // Manual
TModTime and TCreateTime are automatically updated by the server:
TCreateTime: Set when the record is first insertedTModTime: Updated on every modificationFor interoperability with JavaScript/C#/Java:
property UnixTs: TUnixTime read fUnixTs write fUnixTs; // Seconds
property UnixMsTs: TUnixMSTime read fUnixMsTs write fUnixMsTs; // Milliseconds
Critical: TOrm published properties are NOT class instances. They store pointer(RowID):
type
TOrmOrder = class(TOrm)
published
property Customer: TOrmCustomer read fCustomer write fCustomer; // Stores ID, not instance!
end;
// WRONG - will cause Access Violation:
WriteLn(Order.Customer.Name); // AV! Customer is not an instance
// CORRECT - retrieve separately:
var
cust: TOrmCustomer;
begin
cust := TOrmCustomer.Create(Client, Order.Customer); // Load by ID
try
WriteLn(cust.Name);
finally
cust.Free;
end;
end;
var
Order: TOrmOrder;
Customer: TOrmCustomer;
begin
Customer := TOrmCustomer.Create;
Order := TOrmOrder.Create;
try
Customer.Name := 'ACME Corp';
Client.Add(Customer, True); // Customer.ID is now set
Order.Customer := Customer.AsTOrm; // Use AsTOrm for cross-platform
// or: Order.Customer := pointer(Customer.ID); // 32-bit only
Client.Add(Order, True);
finally
Order.Free;
Customer.Free;
end;
end;
Use CreateJoined to auto-instantiate and load all TOrm properties:
var
Order: TOrmOrder;
begin
Order := TOrmOrder.CreateJoined(Client, OrderID);
try
// Now Order.Customer is a real instance, loaded via JOIN
WriteLn(Order.Customer.Name); // Safe!
finally
Order.Free; // Also frees Order.Customer
end;
end;
The ORM automatically handles foreign key integrity (emulated, not via SQL constraints):
| Type | Index | Deletion Behavior |
|---|---|---|
TOrm property |
Yes | Field reset to 0 |
TID |
Yes | None (no table info) |
TOrmClassNameID |
Yes | Field reset to 0 |
TOrmClassNameToBeDeletedID |
Yes | Row deleted (cascade) |
TRecordReference |
Yes | Field reset to 0 |
TRecordReferenceToBeDeleted |
Yes | Row deleted (cascade) |
type
TOrmCustomerID = type TID; // Reset to 0 on delete
TOrmCustomerToBeDeletedID = type TID; // Cascade delete
TOrmOrder = class(TOrm)
published
property Customer: TOrmCustomerID read fCustomer write fCustomer;
property OwnerCustomer: TOrmCustomerToBeDeletedID read fOwner write fOwner;
end;
TRecordReference stores a reference to any table in the model:
type
TOrmAuditLog = class(TOrm)
published
property RelatedRecord: TRecordReference read fRelated write fRelated;
end;
// Usage
var
Log: TOrmAuditLog;
Ref: RecordRef; // Helper record
begin
// Store reference to any record
Log.RelatedRecord := RecordReference(TOrmCustomer, CustomerID);
// Retrieve via helper
Ref.Value := Log.RelatedRecord;
WriteLn('Table: ', Ref.Table(Model).SqlTableName);
WriteLn('ID: ', Ref.ID);
// Load the referenced record directly
Rec := Client.Retrieve(Log.RelatedRecord);
end;
Warning: TRecordReference encodes table index in high bits. Never change table order in TOrmModel after deployment.
Variant fields are stored as JSON TEXT:
type
TOrmDocument = class(TOrm)
published
property Data: Variant read fData write fData;
end;
// Usage - schema-less storage
var
Doc: TOrmDocument;
begin
Doc := TOrmDocument.Create;
Doc.Data := _ObjFast(['name', 'John', 'tags', _Arr(['admin', 'user'])]);
Client.Add(Doc, True);
// Later retrieval
Doc := TOrmDocument.Create(Client, DocID);
WriteLn(Doc.Data.name); // 'John'
WriteLn(Doc.Data.tags._Count); // 2
end;
For MongoDB ODM, variants are stored as native BSON documents with full query support.
Dynamic arrays are stored as BLOB in binary format:
type
TIntegerArray = array of Integer;
TOrmWithArray = class(TOrm)
private
fScores: TIntegerArray;
published
property Scores: TIntegerArray index 1 read fScores write fScores;
end;
Access via TDynArray wrapper:
var
Rec: TOrmWithArray;
DA: TDynArray;
begin
DA := Rec.DynArray(1); // Get wrapper for Scores
DA.Add(100);
DA.Add(200);
end;
For SQL NULL support, use nullable types:
type
TOrmNullableRecord = class(TOrm)
published
property OptionalInt: TNullableInteger read fOptionalInt write fOptionalInt;
property OptionalText: TNullableUtf8Text index 100 read fOptionalText write fOptionalText;
property OptionalDate: TNullableDateTime read fOptionalDate write fOptionalDate;
end;
// Usage
var
Rec: TOrmNullableRecord;
begin
Rec := TOrmNullableRecord.Create;
Rec.OptionalInt := NullableInteger(42); // Has value
Rec.OptionalText := NullableUtf8TextNull; // Is NULL
Rec.OptionalDate := NullableDateTime(Now); // Has value
if NullableIntegerIsEmptyOrNull(Rec.OptionalInt) then
WriteLn('No value')
else
WriteLn('Value: ', NullableIntegerToValue(Rec.OptionalInt));
end;
Available nullable types:
TNullableInteger (Int64)TNullableBooleanTNullableFloat (Double)TNullableCurrencyTNullableDateTimeTNullableTimeLogTNullableUtf8Text
TOrmModel defines which TOrm classes form your database:
uses
mormot.orm.core;
var
Model: TOrmModel;
begin
Model := TOrmModel.Create([
TOrmCustomer,
TOrmProduct,
TOrmOrder,
TOrmOrderLine
], 'api'); // 'api' is the root URI
// Table order matters for TRecordReference - don't change after deployment!
end;
// Add unique constraint
Model.Props[TOrmCustomer].AddUniqueConstraint(['Email']);
// Add index
Model.Props[TOrmOrder].AddIndex(['CustomerID', 'OrderDate']);
// Set table options
Model.Props[TOrmLog].SetTableOptions([sftNoCase, sftNoIndex]);
Always code against IRestOrm interface, not concrete classes:
uses
mormot.orm.core;
procedure DoWork(const Orm: IRestOrm); // Interface parameter
var
Customer: TOrmCustomer;
begin
Customer := TOrmCustomer.Create;
try
Customer.Name := 'ACME Corp';
Orm.Add(Customer, True); // Add via interface
finally
Customer.Free;
end;
end;
var
Orm: IRestOrm;
Customer: TOrmCustomer;
ID: TID;
begin
// CREATE
Customer := TOrmCustomer.Create;
Customer.Name := 'New Customer';
ID := Orm.Add(Customer, True); // Returns new ID
// READ
Customer := TOrmCustomer.Create(Orm, ID); // Load by ID
// or
Orm.Retrieve(ID, Customer); // Load into existing instance
// UPDATE
Customer.Name := 'Updated Name';
Orm.Update(Customer);
// DELETE
Orm.Delete(TOrmCustomer, ID);
end;
In mORMot 2, ORM is accessed via the .Orm property:
// mORMot 1 style (deprecated):
Server.Add(Customer, True);
// mORMot 2 style:
Server.Orm.Add(Customer, True);
// Or store interface:
var
Orm: IRestOrm;
begin
Orm := Server.Orm;
Orm.Add(Customer, True);
end;
uses
mormot.orm.storage;
// Register before server creation
Model.Props[TOrmCache].SetStorage(TRestStorageInMemory);
uses
mormot.orm.sql,
mormot.db.sql.postgres;
var
Props: TSqlDBPostgresConnectionProperties;
begin
Props := TSqlDBPostgresConnectionProperties.Create(
'localhost:5432', 'mydb', 'user', 'pass');
// Map TOrm to external database
OrmMapExternal(Model, TOrmCustomer, Props);
OrmMapExternal(Model, TOrmOrder, Props);
end;
uses
mormot.orm.mongodb,
mormot.db.nosql.mongodb;
var
Client: TMongoClient;
begin
Client := TMongoClient.Create('localhost', 27017);
OrmMapMongoDB(Model, TOrmDocument, Client.Database['mydb']);
end;
| mORMot 1 | mORMot 2 |
|---|---|
TSQLRecord |
TOrm |
TSQLRecordClass |
TOrmClass |
TSQLModel |
TOrmModel |
TSQLTable |
TOrmTable |
TSQLTableJSON |
TOrmTableJson |
TSQLRest |
TRest |
TSQLRest.Add() |
TRest.Orm.Add() |
TSQLRecordMany |
TOrmMany |
TSQLRawBlob |
RawBlob |
| mORMot 1 | mORMot 2 |
|---|---|
mORMot.pas |
mormot.orm.core + mormot.rest.core |
mORMotSQLite3.pas |
mormot.orm.sqlite3 + mormot.rest.sqlite3 |
mORMotDB.pas |
mormot.orm.sql |
mORMotMongoDB.pas |
mormot.orm.mongodb |
By default, type aliases are provided:
type
TSQLRecord = TOrm;
TSQLModel = TOrmModel;
// etc.
Define PUREMORMOT2 to disable these and use only new names.
Next Chapter: Daily ORM (Working with Objects and Queries)
| Previous | Index | Next |
|---|---|---|
| Chapter 4: Core Units | Index | Chapter 6: Daily ORM |