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 |
- Schema flexibility: Documents can have varying structures
- Horizontal scaling: Built-in sharding and replication
- Document model: Natural fit for mORMot's
TDocVariant - High performance: Excellent for write-heavy workloads
- JSON native: Direct integration with mORMot's JSON handling
mormot.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 := wcJournaled; // Wait for journal sync
// Read preference
Client.ReadPreference := rpPrimary; // Always read from primary
Client.ReadPreference := rpSecondary; // Read from secondaries
Client.ReadPreference := rpNearest; // Nearest serverMongoDB 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.ComputeNew; // Use ComputeNew method on record
// 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
ID.ComputeNew; // Generate new ObjectID
Docs[i] := _ObjFast([
'_id', ID.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;Iterating multiple documents:
var
Docs: TVariantDynArray;
Doc: Variant;
begin
// FindDocs fills array with all matching documents
Coll.FindDocs('{age:{$gte:?}}', [18], Docs, Null);
for Doc in Docs do
WriteLn(Doc.name);
end;Note: mORMot2 MongoDB uses array-based retrieval (
FindDocs) rather than cursor iteration. For large result sets, use pagination withNumberToReturnandNumberToSkipparameters.
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.Update(
'{status:?}', ['inactive'],
'{$set:{archived:?}}', [True],
[mufMultiUpdate] // Update all matching documents
);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 _idDelete many:
Coll.Remove('{status:?}', ['deleted']); // Delete all matchingBulk delete:
var
IDs: TVariantDynArray;
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(BsonVariant('{content:"text"}'), null);
// 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]);
// Create server first
Server := TRestServerDB.Create(Model, ':memory:');
// Map classes to MongoDB (signature: aClass, aServer.OrmInstance, aMongoDatabase, aCollectionName)
OrmMapMongoDB(TOrmCustomer, Server.OrmInstance, Client.Database['mydb'], 'customers');
OrmMapMongoDB(TOrmOrder, Server.OrmInstance, Client.Database['mydb'], 'orders');
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;
Json: RawUtf8;
begin
// Get underlying collection
Coll := TRestStorageMongoDB(
Server.OrmInstance.GetStaticStorage(TOrmArticle)).Collection;
// Complex aggregation - operators passed as comma-separated JSON objects
Json := Coll.AggregateJson(
'{$match:{status:"published"}},' +
'{$group:{_id:"$author",count:{$sum:1}}},' +
'{$sort:{count:-1}}',
[]);
end;var
Pipeline: variant;
Results: variant;
i: Integer;
begin
Pipeline := _Arr([
// Stage 1: Filter
_ObjFast(['$match', _ObjFast(['status', 'active'])]),
// Stage 2: Group and count
_ObjFast(['$group', _ObjFast([
'_id', '$category',
'total', _ObjFast(['$sum', 1]),
'avgPrice', _ObjFast(['$avg', '$price'])
])]),
// Stage 3: Sort
_ObjFast(['$sort', _ObjFast(['total', -1])])
]);
Results := Coll.AggregateDocFromVariant(Pipeline);
with _Safe(Results)^ do
for i := 0 to Count - 1 do
WriteLn(Values[i]._id, ': ', Values[i].total, ' items, avg $', Values[i].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(BsonVariant('{email:1}'), null); // 1 = ascending
// Compound index
Coll.EnsureIndex(BsonVariant('{status:1,created:-1}'), null);
// Unique index - pass options as a variant document
Coll.EnsureIndex(BsonVariant('{email:1}'),
BsonVariant('{unique:true}'));
// Text index
Coll.EnsureIndex(BsonVariant('{title:"text",content:"text"}'), null);
// TTL index (auto-delete after time)
Coll.EnsureIndex(BsonVariant('{createdAt:1}'),
BsonVariant('{expireAfterSeconds:3600}'));MongoDB index selection is typically handled automatically by the query optimizer. For specific index selection in mORMot2, use RunCommand directly with the find command and the hint field:
var
Res: variant;
begin
// Force index usage via RunCommand
DB.RunCommand(BsonVariant(
'{find:?,filter:{status:?},hint:{status:1}}',
[], ['mycollection', 'active']), Res);
end;// 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
begin
ID.ComputeNew;
Docs[i]._id := ID.ToVariant;
end;// 1. Use projections to limit returned fields
Coll.FindJson('{status:?}', ['active'], BsonVariant('{name:1,email:1}'));
// 2. Use covered queries (all fields in index)
Coll.EnsureIndex(BsonVariant('{email:1,name:1}'), null);
Coll.FindJson('{email:?}', ['john@example.com'], BsonVariant('{email:1,name:1,_id:0}'));
// 3. Use pagination for large result sets
Coll.FindJson('{status:?}', ['active'], null, 1000, 0); // 1000 docs per page- Index fields used in
$matchstages - Index fields used in sorts
- Compound indexes for multi-field queries
- Cover queries with indexes when possible
- Monitor with
explain()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:
- Full ODM support: Use
TOrmclasses with MongoDB - Direct client access: Low-level
TMongoClientfor advanced operations - TDocVariant integration: Natural document handling
- Query flexibility: Full MongoDB query language support
- Performance: Bulk operations, connection pooling, index management
- Mixing backends: Combine MongoDB with SQL in same application
Next Chapter: JSON RESTful Client-Server
| Previous | Index | Next |
|---|---|---|
| Chapter 8: External SQL Database Access | Index | Chapter 10: JSON and RESTful Fundamentals |