# 12. Client-Server ORM Operations
Remote Data Access
This chapter covers how to perform ORM operations over the network, including remote CRUD, caching, batch operations, and synchronization.
The ORM works identically whether local or remote:
// Local (in-process)
Client := TRestClientDB.Create(Model, nil, 'data.db3', TRestServerDB);
// Remote (HTTP)
Client := TRestHttpClientWinHTTP.Create('server', '8080', Model);
// Same ORM API for both
Client.Orm.Add(Customer, True);
Client.Orm.Retrieve(123, Customer);
Client.Orm.Update(Customer);
Client.Orm.Delete(TOrmCustomer, 123);
Remote ORM operations require authentication:
var
Client: TRestHttpClient;
begin
Client := TRestHttpClientWinHTTP.Create('localhost', '8080', Model);
try
// Must authenticate first
if not Client.SetUser('username', 'password') then
raise Exception.Create('Authentication failed');
// Now ORM operations work
Client.Orm.Add(Customer, True);
finally
Client.Free;
end;
end;
var
Customer: TOrmCustomer;
ID: TID;
begin
Customer := TOrmCustomer.Create;
try
Customer.Name := 'ACME Corp';
Customer.Email := 'contact@acme.com';
// Remote add - returns new ID
ID := Client.Orm.Add(Customer, True);
if ID > 0 then
WriteLn('Created customer #', ID)
else
WriteLn('Failed to create customer');
finally
Customer.Free;
end;
end;
var
Customer: TOrmCustomer;
begin
// By ID
Customer := TOrmCustomer.Create(Client.Orm, 123);
try
WriteLn(Customer.Name);
finally
Customer.Free;
end;
// Alternative: Retrieve into existing instance
Customer := TOrmCustomer.Create;
try
if Client.Orm.Retrieve(123, Customer) then
WriteLn(Customer.Name);
finally
Customer.Free;
end;
// By unique field
Customer := TOrmCustomer.Create;
try
if Client.Orm.Retrieve('Email = ?', [], ['contact@acme.com'], Customer) then
WriteLn(Customer.Name);
finally
Customer.Free;
end;
end;
var
Customer: TOrmCustomer;
begin
Customer := TOrmCustomer.Create(Client.Orm, 123);
try
Customer.Email := 'new@acme.com';
if Client.Orm.Update(Customer) then
WriteLn('Updated successfully');
finally
Customer.Free;
end;
// Update specific fields only
Customer := TOrmCustomer.Create;
try
Customer.ID := 123;
Customer.Email := 'updated@acme.com';
// Only update Email field
Client.Orm.Update(Customer, 'Email');
finally
Customer.Free;
end;
end;
// By ID
Client.Orm.Delete(TOrmCustomer, 123);
// By condition
Client.Orm.Delete(TOrmCustomer, 'Status = ?', [Ord(csInactive)]);
Efficient iteration over remote results:
var
Customer: TOrmCustomer;
begin
Customer := TOrmCustomer.CreateAndFillPrepare(Client.Orm,
'Country = ?', ['USA']);
try
while Customer.FillOne do
WriteLn(Customer.Name, ': ', Customer.Email);
finally
Customer.Free;
end;
end;
Reduce network traffic by requesting only needed fields:
// Only transfer Name and Email fields
Customer := TOrmCustomer.CreateAndFillPrepare(Client.Orm,
'Country = ?', ['USA'],
'Name,Email'); // Fields to retrieve
Get results as object list:
var
List: TObjectList<TOrmCustomer>;
Customer: TOrmCustomer;
begin
List := Client.Orm.RetrieveList<TOrmCustomer>(
'Country = ?', ['USA']);
try
for Customer in List do
WriteLn(Customer.Name);
finally
List.Free;
end;
end;
var
Json: RawUtf8;
begin
// Get results directly as JSON array
Json := Client.Orm.RetrieveListJson(TOrmCustomer,
'Country = ?', ['USA'], 'Name,Email');
// Result: [{"Name":"ACME","Email":"..."},...]
end;
// Cache specific tables on client
Client.Cache.SetCache(TOrmCustomer); // Cache all customers
Client.Cache.SetCache(TOrmProduct); // Cache all products
// With timeout (milliseconds)
Client.Cache.SetTimeOut(TOrmCustomer, 60000); // 1 minute cache
┌─────────────────────────────────────────────────────────────┐
│ Client.Orm.Retrieve(123, Customer) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Check Cache │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Cache Hit │ │ Cache Miss │
│ Return cached │ │ Fetch from │
│ data │ │ server │
└───────────────┘ └───────┬───────┘
│
▼
┌───────────────┐
│ Update cache │
│ Return data │
└───────────────┘
// Clear specific record from cache
Client.Cache.NotifyDeletion(TOrmCustomer, 123);
// Clear entire table cache
Client.Cache.Clear(TOrmCustomer);
// Clear all caches
Client.Cache.Clear;
The server automatically notifies clients of changes:
// Server-side: Enable change notifications
Server.OnUpdateEvent := procedure(Sender: TRest; Event: TOrmOccasion;
aTable: TOrmClass; const aID: TID)
begin
// Automatic - client caches are invalidated
end;
Dramatically improve performance for bulk operations:
uses
mormot.orm.core;
var
Batch: TRestBatch;
Customer: TOrmCustomer;
Results: TIDDynArray;
i: Integer;
begin
Batch := TRestBatch.Create(Client.Orm, TOrmCustomer, 1000);
try
for i := 1 to 10000 do
begin
Customer := TOrmCustomer.Create;
Customer.Name := FormatUtf8('Customer %', [i]);
Customer.Email := FormatUtf8('cust%@example.com', [i]);
Batch.Add(Customer, True); // Batch owns Customer
end;
// Single network roundtrip for all 10000 records
if Client.Orm.BatchSend(Batch, Results) = HTTP_SUCCESS then
WriteLn('Inserted ', Length(Results), ' records');
finally
Batch.Free;
end;
end;
| Operation | Individual | Batch | Improvement |
|---|---|---|---|
| 10,000 inserts (local) | 10 sec | 0.1 sec | 100x |
| 10,000 inserts (network) | 100 sec | 1 sec | 100x |
| 10,000 updates | 15 sec | 0.2 sec | 75x |
var
Batch: TRestBatch;
Customer: TOrmCustomer;
begin
Batch := TRestBatch.Create(Client.Orm, TOrmCustomer);
try
// Mix operations in single batch
Customer := TOrmCustomer.Create;
Customer.Name := 'New Customer';
Batch.Add(Customer, True);
Customer := TOrmCustomer.Create(Client.Orm, 123);
Customer.Status := csActive;
Batch.Update(Customer);
Batch.Delete(TOrmCustomer, 456);
Client.Orm.BatchSend(Batch);
finally
Batch.Free;
end;
end;
For transparent batching:
// Start automatic batching
Client.BatchStart(TOrmCustomer, 1000);
try
for i := 1 to 10000 do
begin
Customer := TOrmCustomer.Create;
Customer.Name := 'Customer ' + IntToStr(i);
Client.Orm.Add(Customer, True); // Automatically batched
Customer.Free;
end;
finally
Client.BatchSend; // Send remaining batch
end;
BLOBs are not included in normal record transfers:
type
TOrmDocument = class(TOrm)
private
fName: RawUtf8;
fContent: RawBlob; // Not transferred with other fields
published
property Name: RawUtf8 read fName write fName;
property Content: RawBlob read fContent write fContent;
end;
var
Doc: TOrmDocument;
Data: RawBlob;
begin
Doc := TOrmDocument.Create;
try
Doc.Name := 'report.pdf';
Client.Orm.Add(Doc, True);
// Upload BLOB separately
Data := StringFromFile('report.pdf');
Client.Orm.UpdateBlob(TOrmDocument, Doc.ID, 'Content', Data);
finally
Doc.Free;
end;
end;
var
Data: RawBlob;
begin
if Client.Orm.RetrieveBlob(TOrmDocument, DocID, 'Content', Data) then
FileFromString(Data, 'downloaded.pdf');
end;
For large files, use streaming:
var
Stream: TStream;
begin
Stream := TFileStream.Create('large_file.zip', fmCreate);
try
Client.Orm.RetrieveBlobStream(TOrmDocument, DocID, 'Content', Stream);
finally
Stream.Free;
end;
end;
Default behavior - last write wins:
// Two clients modify same record
Client1.Orm.Retrieve(123, Customer1);
Client2.Orm.Retrieve(123, Customer2);
Customer1.Email := 'client1@example.com';
Customer2.Email := 'client2@example.com';
Client1.Orm.Update(Customer1); // Succeeds
Client2.Orm.Update(Customer2); // Overwrites Client1's change
type
TOrmCustomer = class(TOrm)
private
fName: RawUtf8;
fVersion: TRecordVersion; // Auto-incremented on changes
published
property Name: RawUtf8 read fName write fName;
property Version: TRecordVersion read fVersion write fVersion;
end;
// Server automatically increments Version on each update
var
Customer: TOrmCustomer;
OriginalVersion: TRecordVersion;
begin
Customer := TOrmCustomer.Create(Client.Orm, 123);
try
OriginalVersion := Customer.Version;
// Make changes
Customer.Name := 'Updated Name';
// Re-fetch to check version
if Client.Orm.OneFieldValue(TOrmCustomer, 'Version', 'ID = ?', [], [123])
<> Int64(OriginalVersion) then
raise Exception.Create('Record modified by another user');
Client.Orm.Update(Customer);
finally
Customer.Free;
end;
end;
// On slave: Track changes from master
var
MasterClient: TRestHttpClient;
SlaveServer: TRestServerDB;
begin
MasterClient := TRestHttpClientWinHTTP.Create('master', '8080', Model);
SlaveServer := TRestServerDB.Create(Model, 'slave.db3');
// Sync tables
SlaveServer.RecordVersionSynchronizeSlave(
TOrmCustomer, MasterClient.Orm, 1000); // Check every second
end;
type
TOrmHistory = class(TOrm)
published
property Table: TOrmClass;
property RecordID: TID;
property Event: TOrmOccasion; // ooInsert, ooUpdate, ooDelete
property SentData: RawBlob;
property TimeStamp: TModTime;
end;
// Server tracks all changes
Server.TrackChanges([TOrmCustomer, TOrmOrder]);
var
ID: TID;
begin
ID := Client.Orm.Add(Customer, True);
if ID = 0 then
begin
// Check last error
WriteLn('Error: ', Client.LastErrorMessage);
WriteLn('Code: ', Client.LastErrorCode);
end;
end;
| Code | Meaning | mORMot Constant |
|---|---|---|
| 200 | Success | HTTP_SUCCESS |
| 201 | Created | HTTP_CREATED |
| 400 | Bad Request | HTTP_BADREQUEST |
| 401 | Unauthorized | HTTP_UNAUTHORIZED |
| 403 | Forbidden | HTTP_FORBIDDEN |
| 404 | Not Found | HTTP_NOTFOUND |
| 500 | Server Error | HTTP_SERVERERROR |
// Auto-reconnect on connection loss
Client.RetryOnceOnTimeout := True;
// Custom retry logic
for Attempt := 1 to 3 do
begin
if Client.Orm.Retrieve(ID, Customer) then
Break;
if Attempt = 3 then
raise Exception.Create('Failed after 3 attempts');
Sleep(1000 * Attempt); // Exponential backoff
end;
// mORMot 1: Direct on TRest
Client.Add(Customer, True);
Client.Retrieve(123, Customer);
// mORMot 2: Via Orm interface
Client.Orm.Add(Customer, True);
Client.Orm.Retrieve(123, Customer);
// mORMot 1
Client.BatchStart(TOrmCustomer);
Client.BatchAdd(Customer);
Client.BatchSend;
// mORMot 2
Batch := TRestBatch.Create(Client.Orm, TOrmCustomer);
Batch.Add(Customer, True);
Client.Orm.BatchSend(Batch);
Next Chapter: Server-Side ORM Processing
| Previous | Index | Next |
|---|---|---|
| Chapter 11: Client-Server Architecture | Index | Chapter 13: Server-Side ORM Processing |