# 9. External NoSQL Database Access
MongoDB and Object-Document Mapping
mORMot provides native access to NoSQL databases, with MongoDB as the primary supported engine. The ORM seamlessly transforms into an ODM (Object-Document Mapping) when working with document stores.
| Engine | Unit | Description |
|---|---|---|
| MongoDB | mormot.db.nosql.mongodb |
Full ODM support |
| In-Memory | mormot.orm.storage |
TObjectList with JSON/binary persistence |
TDocVariantmormot.db.nosql.bson.pas → BSON encoding/decoding
↓
mormot.db.nosql.mongodb.pas → MongoDB wire protocol client
↓
mormot.orm.mongodb.pas → ORM/ODM integration
Basic connection:
uses
mormot.db.nosql.mongodb;
var
Client: TMongoClient;
DB: TMongoDatabase;
begin
Client := TMongoClient.Create('localhost', 27017);
try
DB := Client.Database['mydb'];
// Use DB...
finally
Client.Free;
end;
end;
With authentication (SCRAM-SHA-1):
var
Client: TMongoClient;
DB: TMongoDatabase;
begin
Client := TMongoClient.Create('localhost', 27017);
try
// Authenticate and get database
DB := Client.OpenAuth('mydb', 'username', 'password');
// Use DB...
finally
Client.Free;
end;
end;
Replica set connection:
Client := TMongoClient.Create(
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplicaSet');
Client := TMongoClient.Create('localhost', 27017);
// Write concern settings
Client.WriteConcern := wcAcknowledged; // Default - wait for ack
Client.WriteConcern := wcUnacknowledged; // Fire and forget (fastest)
Client.WriteConcern := wcMajority; // Wait for majority
// Read preference
Client.ReadPreference := rpPrimary; // Always read from primary
Client.ReadPreference := rpSecondary; // Read from secondaries
Client.ReadPreference := rpNearest; // Nearest server
MongoDB uses BSON (Binary JSON), which extends JSON with additional types:
| BSON Type | Delphi Representation |
|---|---|
| Double | Double |
| String | RawUtf8 |
| Document | TDocVariant |
| Array | TDocVariant (array mode) |
| Binary | RawByteString |
| ObjectId | TBsonObjectID |
| Boolean | Boolean |
| DateTime | TDateTime |
| Null | Null variant |
| Int32 | Integer |
| Int64 | Int64 |
| Decimal128 | TDecimal128 |
TDocVariant seamlessly maps to MongoDB documents:
var
Doc: Variant;
begin
// Create document with late-binding
TDocVariant.New(Doc);
Doc.name := 'John Doe';
Doc.email := 'john@example.com';
Doc.age := 30;
Doc.tags := _Arr(['developer', 'delphi', 'mongodb']);
Doc.address := _Obj([
'street', '123 Main St',
'city', 'New York',
'zip', '10001'
]);
// Save to MongoDB
Coll.Insert(Doc);
end;
var
ID: TBsonObjectID;
begin
// Generate new ObjectID (client-side)
ID := TBsonObjectID.NewObjectID;
// ObjectID contains timestamp
WriteLn('Created at: ', DateTimeToStr(ID.CreateDateTime));
// Use in document
Doc._id := ID.ToVariant;
Coll.Insert(Doc);
end;
var
Coll: TMongoCollection;
begin
// Get or create collection
Coll := DB.CollectionOrCreate['customers'];
// Get existing collection
Coll := DB.Collection['customers'];
end;
Single document:
var
Doc: Variant;
begin
Doc := _ObjFast([
'name', 'John Doe',
'email', 'john@example.com',
'age', 30
]);
Coll.Insert(Doc);
WriteLn('Inserted with _id: ', Doc._id); // Auto-generated ObjectID
end;
Bulk insert (much faster):
var
Docs: TVariantDynArray;
i: Integer;
begin
SetLength(Docs, 10000);
for i := 0 to High(Docs) do
begin
Docs[i] := _ObjFast([
'_id', TBsonObjectID.NewObjectID.ToVariant,
'index', i,
'data', FormatUtf8('Record %', [i])
]);
end;
Coll.Insert(Docs); // Single network roundtrip!
end;
Find one document:
var
Doc: Variant;
begin
// By _id
Doc := Coll.FindOne(ObjectID);
Doc := Coll.FindOne(123); // Integer _id
// By query
Doc := Coll.FindDoc('{name:?}', ['John']);
Doc := Coll.FindDoc('{age:{$gt:?}}', [21]);
end;
Find multiple documents:
var
Docs: TVariantDynArray;
Doc: Variant;
begin
// Get all matching documents
Coll.FindDocs('{status:?}', ['active'], Docs);
for Doc in Docs do
WriteLn(Doc.name);
end;
Find with projection:
var
Json: RawUtf8;
begin
// Return only specific fields
Json := Coll.FindJson(
'{status:?}', // Query
['active'], // Parameters
'{name:1,email:1}' // Projection (include name and email)
);
end;
Cursor-based iteration:
var
Cursor: TMongoRequest;
begin
Cursor := Coll.Find('{age:{$gte:?}}', [18]);
try
while Cursor.Step do
WriteLn(Cursor.Document.name);
finally
Cursor.Free;
end;
end;
Replace document:
var
Doc: Variant;
begin
Doc := Coll.FindOne(123);
Doc.status := 'updated';
Coll.Save(Doc); // Replace entire document
end;
Partial update ($set):
// Update specific fields only
Coll.Update(
'{_id:?}', [123], // Query
'{$set:{status:?,updated:?}}', ['active', Now] // Update
);
Update multiple documents:
// Set all inactive users to archived
Coll.UpdateMany(
'{status:?}', ['inactive'],
'{$set:{archived:?}}', [True]
);
Upsert (insert if not exists):
Coll.Update(
'{email:?}', ['john@example.com'],
'{$set:{name:?,lastSeen:?}}', ['John', Now],
[mufUpsert] // Create if not found
);
Delete one:
Coll.Remove('{_id:?}', [ObjectID]);
Coll.RemoveOne(123); // By _id
Delete many:
Coll.Remove('{status:?}', ['deleted']); // Delete all matching
Bulk delete:
var
IDs: TBsonObjectIDDynArray;
begin
// Much faster than individual deletes
Coll.Remove('{_id:{$in:?}}', [IDs]);
end;
| Operator | Description | Example |
|---|---|---|
$eq |
Equal | {age:{$eq:30}} |
$ne |
Not equal | {status:{$ne:'deleted'}} |
$gt |
Greater than | {age:{$gt:21}} |
$gte |
Greater or equal | {age:{$gte:18}} |
$lt |
Less than | {price:{$lt:100}} |
$lte |
Less or equal | {stock:{$lte:10}} |
$in |
In array | {status:{$in:['active','pending']}} |
$nin |
Not in array | {role:{$nin:['admin']}} |
// AND (implicit)
Coll.FindDocs('{age:{$gte:?},status:?}', [18, 'active'], Docs);
// AND (explicit)
Coll.FindDocs('{$and:[{age:{$gte:?}},{age:{$lt:?}}]}', [18, 65], Docs);
// OR
Coll.FindDocs('{$or:[{status:?},{priority:{$gt:?}}]}', ['urgent', 5], Docs);
// NOT
Coll.FindDocs('{age:{$not:{$lt:?}}}', [18], Docs);
// Field exists
Coll.FindDocs('{email:{$exists:true}}', [], Docs);
// Type check
Coll.FindDocs('{age:{$type:"int"}}', [], Docs);
// Element in array
Coll.FindDocs('{tags:?}', ['mongodb'], Docs);
// All elements match
Coll.FindDocs('{tags:{$all:?}}', [_Arr(['mongodb','delphi'])], Docs);
// Array size
Coll.FindDocs('{tags:{$size:?}}', [3], Docs);
// Element match
Coll.FindDocs('{items:{$elemMatch:{qty:{$gt:?},price:{$lt:?}}}}', [10, 100], Docs);
// Create text index first
Coll.EnsureIndex('{content:"text"}');
// Text search
Coll.FindDocs('{$text:{$search:?}}', ['mongodb tutorial'], Docs);
uses
mormot.orm.mongodb,
mormot.db.nosql.mongodb;
var
Client: TMongoClient;
Model: TOrmModel;
Server: TRestServerDB;
begin
// Connect to MongoDB
Client := TMongoClient.Create('localhost', 27017);
// Create model
Model := TOrmModel.Create([TOrmCustomer, TOrmOrder]);
// Map classes to MongoDB
OrmMapMongoDB(Model, TOrmCustomer, Client.Database['mydb'], 'customers');
OrmMapMongoDB(Model, TOrmOrder, Client.Database['mydb'], 'orders');
// Create server
Server := TRestServerDB.Create(Model, ':memory:');
end;
type
TOrmArticle = class(TOrm)
private
fTitle: RawUtf8;
fContent: RawUtf8;
fTags: TRawUtf8DynArray;
fMetadata: Variant; // TDocVariant for flexible schema
published
property Title: RawUtf8 read fTitle write fTitle;
property Content: RawUtf8 read fContent write fContent;
property Tags: TRawUtf8DynArray read fTags write fTags;
property Metadata: Variant read fMetadata write fMetadata;
end;
Once mapped, use standard ORM methods:
var
Article: TOrmArticle;
begin
// Create
Article := TOrmArticle.Create;
Article.Title := 'Introduction to MongoDB';
Article.Content := 'MongoDB is a document database...';
Article.Tags := ['mongodb', 'nosql', 'database'];
Article.Metadata := _ObjFast(['author', 'John', 'views', 0]);
Server.Orm.Add(Article, True);
// Read
Article := TOrmArticle.Create(Server.Orm, ArticleID);
// Update
Article.Metadata.views := Article.Metadata.views + 1;
Server.Orm.Update(Article);
// Delete
Server.Orm.Delete(TOrmArticle, ArticleID);
// Query
Article := TOrmArticle.CreateAndFillPrepare(Server.Orm,
'Tags = ?', ['mongodb']);
while Article.FillOne do
WriteLn(Article.Title);
end;
For advanced queries, use direct MongoDB access:
var
Coll: TMongoCollection;
Docs: TVariantDynArray;
begin
// Get underlying collection
Coll := TRestStorageMongoDB(
Server.StaticDataServer[TOrmArticle]).Collection;
// Complex aggregation
Coll.AggregateJson([
'{$match:{status:"published"}}',
'{$group:{_id:"$author",count:{$sum:1}}}',
'{$sort:{count:-1}}'
], Docs);
end;
var
Results: TVariantDynArray;
begin
Coll.AggregateDoc([
// Stage 1: Filter
_ObjFast(['$match', _Obj(['status', 'active'])]),
// Stage 2: Group and count
_ObjFast(['$group', _Obj([
'_id', '$category',
'total', _Obj(['$sum', 1]),
'avgPrice', _Obj(['$avg', '$price'])
])]),
// Stage 3: Sort
_ObjFast(['$sort', _Obj(['total', -1])])
], Results);
for Doc in Results do
WriteLn(Doc._id, ': ', Doc.total, ' items, avg $', Doc.avgPrice);
end;
| Operator | Description |
|---|---|
$match |
Filter documents |
$group |
Group by field |
$sort |
Sort results |
$project |
Reshape documents |
$limit |
Limit results |
$skip |
Skip documents |
$unwind |
Deconstruct arrays |
$lookup |
Left outer join |
// Single field index
Coll.EnsureIndex('{email:1}'); // 1 = ascending
// Compound index
Coll.EnsureIndex('{status:1,created:-1}');
// Unique index
Coll.EnsureIndex('{email:1}', [ifoUnique]);
// Text index
Coll.EnsureIndex('{title:"text",content:"text"}');
// TTL index (auto-delete after time)
Coll.EnsureIndex('{createdAt:1}', [ifoExpireAfterSeconds], 3600);
// Force index usage
Coll.FindJson('{status:?}', ['active'], '',
'{$hint:{status:1}}'); // Use status index
// 1. Use bulk inserts
Coll.Insert(DocsArray); // Single call for many documents
// 2. Use unacknowledged writes for non-critical data
Client.WriteConcern := wcUnacknowledged;
try
// Fast writes (no server confirmation)
Coll.Insert(LogEntries);
finally
Client.WriteConcern := wcAcknowledged;
end;
// 3. Pre-generate ObjectIDs
for i := 0 to High(Docs) do
Docs[i]._id := TBsonObjectID.NewObjectID.ToVariant;
// 1. Use projections to limit returned fields
Coll.FindJson('{status:?}', ['active'], '{name:1,email:1}');
// 2. Use covered queries (all fields in index)
Coll.EnsureIndex('{email:1,name:1}');
Coll.FindJson('{email:?}', ['john@example.com'], '{email:1,name:1,_id:0}');
// 3. Use cursor batching for large result sets
Cursor := Coll.Find(Query, nil, [mqfNoCursorTimeout]);
Cursor.BatchSize := 1000;
$match stagesexplain() equivalent| mORMot 1 | mORMot 2 |
|---|---|
SynMongoDB.pas |
mormot.db.nosql.mongodb.pas |
mORMotMongoDB.pas |
mormot.orm.mongodb.pas |
| mORMot 1 | mORMot 2 |
|---|---|
StaticMongoDBRegister |
OrmMapMongoDB |
TMongoClient |
TMongoClient (unchanged) |
TMongoDatabase |
TMongoDatabase (unchanged) |
TMongoCollection |
TMongoCollection (unchanged) |
mORMot 2 uses the new MongoDB wire protocol (OP_MSG) introduced in MongoDB 3.6:
// For older MongoDB versions (< 3.6), define:
{$DEFINE MONGO_OLDPROTOCOL}
MongoDB integration in mORMot 2 provides:
TOrm classes with MongoDBTMongoClient for advanced operationsNext Chapter: JSON RESTful Client-Server
| Previous | Index | Next |
|---|---|---|
| Chapter 8: External SQL Database Access | Index | Chapter 10: JSON and RESTful Fundamentals |