MongoDB + ORM = ODM
The mORMotMongoDB.pas
unit is able to let any
TSQLRecord
class be persisted on a remote MongoDB
server.
As a result, our ORM is able to be used as a NoSQL and Object-Document
Mapping (ODM) framework, with almost no code change.
Any MongoDB database can be accessed via RESTful commands, using JSON
over HTTP.
This integration benefits from the other parts of the framework (e.g. our UTF-8 dedicated process, which is also the native encoding for BSON), so you can easily mix SQL and NoSQL databases with the exact same code, and are still able to tune any SQL or MongoDB request in your code, if necessary.
Register the TSQLRecord class
In the database model, we define a TSQLRecord
class, as
usual:
TSQLORM = class(TSQLRecord) private fAge: integer; fName: RawUTF8; fDate: TDateTime; fValue: variant; fInts: TIntegerDynArray; fCreateTime: TCreateTime; fData: TSQLRawBlob; published property Name: RawUTF8 read fName write fName stored AS_UNIQUE; property Age: integer read fAge write fAge; property Date: TDateTime read fDate write fDate; property Value: variant read fValue write fValue; property Ints: TIntegerDynArray index 1 read fInts write fInts; property Data: TSQLRawBlob read fData write fData; property CreateTime: TCreateTime read fCreateTime write fCreateTime; end;
Note that we did not define any index
...
values for the RawUTF8
property, as we need for
external SQL databases, since MongoDB does not expect any restriction
about text fields length.
The property values will be stored in the native MongoDB layout, i.e. with a better coverage than the SQL types:
Delphi | MongoDB | Remarks |
byte |
int32 | |
word |
int32 | |
integer |
int32 | |
cardinal |
N/A | You should use Int64 instead |
Int64 |
int64 | |
boolean |
boolean | 0 is false , anything else is true |
enumeration | int32 | store the ordinal value of the enumerated item(i.e. starting at 0 for the first element) |
set | int32 | each bit corresponding to an enumerated item (therefore a set of up to 64 elements can be stored in such a field) |
single |
double | |
double |
double | |
extended |
double | stored as double (precision lost) |
currency |
double | safely converted to/from currency type with fixed decimals,
without rounding error |
RawUTF8 |
UTF-8 | this is the preferred field type for storing some textual content in the ORM |
WinAnsiString |
UTF-8 | WinAnsi char-set (code page 1252) in Delphi |
RawUnicode |
UTF-8 | UCS2 char-set in Delphi, as AnsiString |
WideString |
UTF-8 | UCS2 char-set, as COM BSTR type (Unicode in all version of Delphi) |
SynUnicode |
UTF-8 | Will be either WideString before Delphi 2009, or
UnicodeString later |
string |
UTF-8 | Not to be used before Delphi 2009 (unless you may loose some data during
conversion) - RawUTF8 is preferred in all cases |
TDateTime |
datetime | ISO 8601 encoded date time |
TTimeLog |
int64 | as proprietary fast Int64 date time |
TModTime |
int64 | the server date time will be stored when a record is modified (as
proprietary fast Int64 ) |
TCreateTime |
int64 | the server date time will be stored when a record is created (as
proprietary fast Int64 ) |
TSQLRecord |
int32 | RowID pointing to another record (warning: the field value
contains pointer(RowID) , not a valid object instance - the record
content must be retrieved with late-binding via its ID using a
PtrInt(Field) typecast or the Field.ID method), or by
using e.g. CreateJoined() |
TSQLRecordMany |
nothing | data is stored in a separate pivot table; for MongoDB, you should better use data sharding, and an embedded sub-document |
TRecordReference |
int32 | store both ID and TSQLRecord type in a
RecordRef -like value (use e.g. TSQLRest.
Retrieve(Reference) to get a record content) |
TPersistent |
object | BSON object (from ObjectToJSON ) |
TCollection |
array | BSON array of objects (from ObjectToJSON ) |
TObjectList |
array | BSON array of objects (from ObjectToJSON ) - see
TJSONSerializer. RegisterClassForJSON TObjectList
serialization |
TStrings |
array | BSON array of strings (from ObjectToJSON ) |
TRawUTF8List |
array | BSON array of string (from ObjectToJSON ) |
any TObject |
object | See TJSONSerializer. RegisterCustomSerializer TObject
serialization |
TSQLRawBlob |
binary | BSON binary for blob storage - note that you need to retrieve explicitly
such fields, which are not part of the default Retrieve() method;
this type is an alias to RawByteString |
dynamic arrays | array binary |
if the dynamic array can be saved as true JSON, will be stored as BSON
array - otherwise, will be stored in the TDynArray.SaveTo binary
format - note that TByteDynArray will always be stored as BSON
binary, so that TSQLRawBlob may be reserved to store content on
GridFS in the future |
variant |
array object |
BSON number, text, object or array, depending on TDocVariant custom
variant type or TBSONVariant stored value |
record |
binary object |
BSON as defined in code by overriding
TSQLRecord.InternalRegisterCustomProperties |
On the server side (there won't be any difference for the client), you
define a TMongoDBClient
, and assign it to a given
TSQLRecord
class:
MongoClient := TMongoClient.Create('localhost',27017);
DB := MongoClient.Database['dbname'];
Model := TSQLModel.Create([TSQLORM]);
Client := TSQLRestClientDB.Create(Model,nil,':memory:',TSQLRestServerDB);
if StaticMongoDBRegister(TSQLORM,fClient.Server,fDB,'collectionname')=nil then
raise Exception.Create('Error');
And... that's all!
You can then use any ORM command, as usual:
writeln(Client.TableRowCount(TSQLORM)=0);
As with external databases, you can specify the field names mapping between
the objects and the MongoDB collection.
By default, the TSQLRecord.ID
property is mapped to the
MongoDB's _id
field, and the ORM will populate this
_id
field with a sequence of integer values, just like any
TSQLRecord
table.
You can specify your own mapping, using for instance:
aModel.Props[aClass].ExternalDB.MapField(..)
Since the field names are stored within the document itself, it may be a good idea to use shorter naming for the MongoDB collection. It may save some storage space, when working with a huge number of documents.
Once the TSQLRecord
is mapped to a MongoDB collection,
you can always have direct access to the TMongoCollection
instance
later on, by calling:
(aServer.StaticDataServer[aClass] as TSQLRestServerStaticMongoDB).Collection
This may allow any specific task, including any tuned query or process.
ORM/ODM CRUD methods
You can add documents with the standard CRUD methods of the ORM, as usual:
R := TSQLORM.Create;
try
for i := 1 to COLL_COUNT do begin
R.Name := 'Name '+Int32ToUTF8(i);
R.Age := i;
R.Date := 1.0*(30000+i);
R.Value := _ObjFast(['num',i]);
R.Ints := nil;
R.DynArray(1).Add(i);
assert(Client.Add(R,True)=i);
end;
finally
R.Free;
end;
As we already saw, the framework is able to handle any kind of properties,
including complex types like dynamic arrays or
variant
.
In the above code, a TDocVariant
document has been stored in
R.Value
, and a dynamic array of integer
values is
accessed via its index 1
shortcut and the
TSQLRecord.DynArray()
method.
The usual Retrieve
/ Delete
/ Update
methods are available:
R := TSQLORM.Create;
try
for i := 1 to COLL_COUNT do begin
Check(Client.Retrieve(i,R));
// here R instance contains all values of one document, excluding BLOBs
end;
finally
R.Free;
end;
You can define a WHERE clause, as if the back-end where a regular SQL database:
R := TSQLORM.CreateAndFillPrepare(Client,'ID=?',[i]); try ...
Current implementation understand one condition over one single field, with
= > >= < <= IN
clauses. More advanced queries are
possible, but won't be handled as SQL, but via direct access to the
TMongoDBCollection
.
To perform a query and retrieve the content of several documents, you can
use regular CreateAndFillPrepare
or FillPrepare
methods:
R := TSQLORM.CreateAndFillPrepare(Client,''); try n := 0; while R.FillOne do begin // here R instance contains all values of one document, excluding BLOBs inc(n); end; assert(n=COLL_COUNT); finally R.Free; end;
A WHERE clause can also be defined for CreateAndFillPrepare
or
FillPrepare
methods.
BATCH mode
In addition to individual CRUD operations, our MongoDB is able to use BATCH mode for adding or deleting documents.
You can write the exact same code as with any SQL back-end:
Client.BatchStart(TSQLORM); R := TSQLORM.Create; try for i := 1 to COLL_COUNT do begin R.Name := 'Name '+Int32ToUTF8(i); R.Age := i; R.Date := 1.0*(30000+i); R.Value := _ObjFast(['num',i]); R.Ints := nil; R.DynArray(1).Add(i); assert(Client.BatchAdd(R,True)>=0); end; finally R.Free; end; assert(Client.BatchSend(IDs)=HTML_SUCCESS);
Or for deletion:
Client.BatchStart(TSQLORM); for i := 5 to COLL_COUNT do if i mod 5=0 then assert(fClient.BatchDelete(i)>=0); assert(Client.BatchSend(IDs)=HTML_SUCCESS);
Speed benefit may be huge in regard to individual Add/Delete operations, even on a local MongoDB server. We will see some benchmark numbers now.
ORM/ODM performance
You can take a look at Data access benchmark to compare MongoDB as back-end for our ORM classes.
In respect to external SQL engines, it features very high speed, low CPU
use, and almost no difference in use. We interfaced the BatchAdd()
and BatchDelete()
methods to benefit of MongoDB BULK
process, and avoided most memory allocation during the process.
Here are some numbers, extracted from the MongoDBTests.dpr
sample, which reflects the performance of our ORM/ODM, depending on the
Write Concern mode used:
2. ORM
2.1. ORM with acknowledge: - Connect to local server: 6 assertions passed 18.65ms - Insert: 5,002 assertions passed 521.25ms 5000 rows inserted in 520.65ms i.e. 9603/s, aver. 104us, 2.9 MB/s - Insert in batch mode: 5,004 assertions passed 65.37ms 5000 rows inserted in 65.07ms i.e. 76836/s, aver. 13us, 8.4 MB/s - Retrieve: 45,001 assertions passed 640.95ms 5000 rows retrieved in 640.75ms i.e. 7803/s, aver. 128us, 2.1 MB/s - Retrieve all: 40,001 assertions passed 20.79ms 5000 rows retrieved in 20.33ms i.e. 245941/s, aver. 4us, 27.1 MB/s - Retrieve one with where clause: 45,410 assertions passed 673.01ms 5000 rows retrieved in 667.17ms i.e. 7494/s, aver. 133us, 2.0 MB/s - Update: 40,002 assertions passed 681.31ms 5000 rows updated in 660.85ms i.e. 7565/s, aver. 132us, 2.4 MB/s - Blobs: 125,003 assertions passed 2.16s 5000 rows updated in 525.97ms i.e. 9506/s, aver. 105us, 2.4 MB/s - Delete: 38,003 assertions passed 175.86ms 1000 rows deleted in 91.37ms i.e. 10944/s, aver. 91us, 2.3 MB/s - Delete in batch mode: 33,003 assertions passed 34.71ms 1000 rows deleted in 14.90ms i.e. 67078/s, aver. 14us, 597 KB/s Total failed: 0 / 376,435 - ORM with acknowledge PASSED 5.00s
2.2. ORM without acknowledge: - Connect to local server: 6 assertions passed 16.83ms - Insert: 5,002 assertions passed 179.79ms 5000 rows inserted in 179.15ms i.e. 27908/s, aver. 35us, 3.9 MB/s - Insert in batch mode: 5,004 assertions passed 66.30ms 5000 rows inserted in 31.46ms i.e. 158891/s, aver. 6us, 17.5 MB/s - Retrieve: 45,001 assertions passed 642.05ms 5000 rows retrieved in 641.85ms i.e. 7789/s, aver. 128us, 2.1 MB/s - Retrieve all: 40,001 assertions passed 20.68ms 5000 rows retrieved in 20.26ms i.e. 246718/s, aver. 4us, 27.2 MB/s - Retrieve one with where clause: 45,410 assertions passed 680.99ms 5000 rows retrieved in 675.24ms i.e. 7404/s, aver. 135us, 2.0 MB/s - Update: 40,002 assertions passed 231.75ms 5000 rows updated in 193.74ms i.e. 25807/s, aver. 38us, 3.6 MB/s - Blobs: 125,003 assertions passed 1.44s 5000 rows updated in 150.58ms i.e. 33202/s, aver. 30us, 2.6 MB/s - Delete: 38,003 assertions passed 103.57ms 1000 rows deleted in 19.73ms i.e. 50668/s, aver. 19us, 2.4 MB/s - Delete in batch mode: 33,003 assertions passed 47.50ms 1000 rows deleted in 364us i.e. 2747252/s, aver. 0us, 23.4 MB/s Total failed: 0 / 376,435 - ORM without acknowledge PASSED 3.44s
As for direct MongoDB access, the wcUnacknowledged
is
not to be used on production, but may be very useful in some particular
scenarios.
As expected, the reading process is not impacted by the Write Concern
mode set.
You can take a look at the previous blog article, about low-level MongoDB direct
access.
Or check out more complete
benchmark results, to compare MongoDB with other SQL back-end
handled by our ORM.
Feedback is welcome
on our forum, as usual!