我们的ORM RESTful框架不仅能够通过SynDB直接RDBMS访问常规的SQL数据库引擎,还能够访问NoSQL引擎,参见NoSQL和对象文档映射(ODM)。
还记得介绍mORMot数据库层的图:
可以从mORMot的对象文档映射(ODM)功能访问以下NoSQL引擎:
| NoSQL引擎 | 描述 |
|---|---|
| TObjectList | 内存存储,具有JSON或二进制磁盘持久性 |
| MongoDB | 排名第一的NoSQL数据库引擎 |
实际上,我们可以考虑将TSQLRestStorageInMemory实例及其TObjectList存储作为一个NoSQL高速内存引擎,用纯Delphi编写。有关此特性的详细信息,请参见内存中的“静态”进程。
MongoDB(源自“humongous”)是一个跨平台的面向文档的数据库系统,当然也是最著名的NoSQL数据库。
根据http://db-engines.com 2015年12月的数据,MongoDB在最流行的数据库管理系统中排名第四,NoSQL数据库管理系统排名第一。
我们的mORMot提供了对该数据库的高级访问,具有完整的NoSQL和对象文档映射(ODM)功能。
分两个层次进行了集成:
SynMongoDB.pas单元直接底层访问MongoDB服务器;mORMotMongoDB.pas单元与ORM紧密集成(实际成为了ODM)。MongoDB绕过了传统的基于表的关系数据库结构,而是支持具有动态模式的JSON类文档(MongoDB称之为BSON格式),这完全符合mORMot的RESTful方法。
SynMongoDB.pas单元可直接访问MongoDB服务器并进行了优化。
它可以访问任何BSON数据,包括文档、数组和MongoDB的自定义类型(如ObjectID、date、binary、regex、Decimal128或Javascript):
TBSONObjectID在客户端创建一些真正的文档标识符(MongoDB不会为您生成ID:通常是在客户端生成唯一ID);TBSONWriter);TBSONVariant自定义变体类型存储MongoDB的自定义类型值;SynCommons.pas的TDocVariant自定义变体类型作为文档存储和后期绑定访问;该单元定义了一些对象,这些对象能够连接和管理任何MongoDB服务机群上的数据库和文档集合:
TMongoClient类连接到一个或多个服务器,包括辅助主机;TMongoDatabase类访问任何数据库实例;TMongoCollection类访问任何集合; 在集合级,您可以直接访问数据,使用TDocVariant/TBSONVariant等高级结构,使用易于阅读的JSON或底层BSON内容。
您还可以在很多方面优化客户端流程,例如关于错误处理或写回执(即如何确认远程数据的修改)。
下面是一些示例代码,它能够连接到MongoDB服务器,并返回服务器时间:
var Client: TMongoClient;
DB: TMongoDatabase;
serverTime: TDateTime;
res: variant; // we will return the command result as TDocVariant
errmsg: RawUTF8;
begin
Client := TMongoClient.Create('localhost',27017);
try
DB := Client.Database['mydb'];
writeln('Connecting to ',DB.Name); // will write 'mydb'
errmsg := DB.RunCommand('hostInfo',res); // run a command
if errmsg<>'' then
exit; // quit on any error
serverTime := res.system.currentTime; // direct conversion to TDateTime
writeln('Server time is ',DateTimeToStr(serverTime));
finally
Client.Free; // will release the DB instance
end;
end;
注意,对于这个底层命令,我们使用了TDocVariant及其后期绑定功能。
如果在调试期间将鼠标放在res变量上,您将看到以下JSON内容:
{"system":{"currentTime":"2014-05-06T15:24:25","hostname":"Acer","cpuAddrSize":64,"memSizeMB":3934,"numCores":4,"cpuArch":"x86_64","numaEnabled":false},"os":{"type":"Windows","name":"Microsoft Windows 7","version":"6.1 SP1 (build 7601)"},"extra":{"pageSize":4096},"ok":1}
我们只需通过编写res.system.currentTime来访问服务器时间。
这里的连接是匿名的,只有当mongod实例在同一台计算机上运行时,它才会工作。可以通过TMongoClient.OpenAuth()方法进行安全的远程连接,包括用户身份验证:支持最新的SCRAM-SHA-1挑战响应机制(MongoDB 3.x以后支持),或废弃的MONGODB-CR(用于旧版本)。
...
Client := TMongoClient.Create('localhost',27017);
try
DB := Client.OpenAuth('mydb','mongouser','mongopwd');
...
出于安全原因,MongoDB服务器不要在没有经过身份验证的情况下允许远程访问,正如http://docs.mongodb.org/manual/admination/securit-access-control所述。
TMongoDatabase.CreateUser()、createuserforisdatabase()和DropUser()方法允许轻松管理来自应用程序的凭据。
现在我们将解释如何向给定集合添加文档。
我们假设有一个DB: TMongoDatabase实例可用,我们将使用TDocVariant实例创建文档,该实例将通过后期绑定填充和doc.Clear伪方法清除之前的属性值:
var Coll: TMongoCollection;
doc: variant;
i: integer;
begin
Coll := DB.CollectionOrCreate[COLL_NAME];
TDocVariant.New(doc);
for i := 1 to 10 do
begin
doc.Clear;
doc.Name := 'Name '+IntToStr(i+1);
doc.Number := i;
Coll.Save(doc);
writeln('Inserted with _id=',doc._id);
end;
end;
由于TDocVariant后期绑定功能,代码非常容易理解和维护。
这段代码将在控制台显示如下内容:
Inserted with _id=5369029E4F901EE8114799D9
Inserted with _id=5369029E4F901EE8114799DA
Inserted with _id=5369029E4F901EE8114799DB
Inserted with _id=5369029E4F901EE8114799DC
Inserted with _id=5369029E4F901EE8114799DD
Inserted with _id=5369029E4F901EE8114799DE
Inserted with _id=5369029E4F901EE8114799DF
Inserted with _id=5369029E4F901EE8114799E0
Inserted with _id=5369029E4F901EE8114799E1
Inserted with _id=5369029E4F901EE8114799E2
这意味着Coll.Save()方法非常聪明,能够理解所提供的文档没有_id字段,因此在将文档数据发送到MongoDB服务器之前,将在客户端计算一个。
我们也可以这样写:
for i := 1 to 10 do
begin
doc.Clear;
doc._id := ObjectID;
doc.Name := 'Name '+IntToStr(i+1);
doc.Number := i;
Coll.Save(doc);
writeln('Inserted with _id=',doc._id);
end;
end;
这将在调用Coll.Save()之前显式地计算文档标识符。
在本例中,我们可以直接调用Coll.Insert(),这稍微快一些。
没有强制您使用MongoDB ObjectID作为标识符,你可以使用任何值,如果你确信它是可用的,如你可以使用整数:
for i := 1 to 10 do
begin
doc.Clear;
doc._id := i;
doc.Name := 'Name '+IntToStr(i+1);
doc.Number := i;
Coll.Insert(doc);
writeln('Inserted with _id=',doc._id);
end;
end;
控制台现在显示:
Inserted with _id=1
Inserted with _id=2
Inserted with _id=3
Inserted with _id=4
Inserted with _id=5
Inserted with _id=6
Inserted with _id=7
Inserted with _id=8
Inserted with _id=9
Inserted with _id=10
mORMot ORM是以类似的方式计算可用的整数序列,TSQLRecord将按照要求使用这些TSQLRecord.ID主键属性。
TMongoCollection类还可以写入文档列表,并将它们立即发送到MongoDB服务器:这种批量插入模式,接近一些SQL提供程序的数组绑定特性,并由我们的SynDB.pas类实现,可以将插入速度增加10倍,甚至在连接到本地实例时也是如此:想象一下,它在物理网络上可以节省多少时间!
例如,你可以这样写:
var docs: TVariantDynArray;
...
SetLength(docs,COLL_COUNT);
for i := 0 to COLL_COUNT-1 do begin
TDocVariant.New(docs[i]);
docs[i]._id := ObjectID; // compute new ObjectID on the client side
docs[i].Name := 'Name '+IntToStr(i+1);
docs[i].FirstName := 'FirstName '+IntToStr(i+COLL_COUNT);
docs[i].Number := i;
end;
Coll.Insert(docs); // insert all values at once
...
稍后您将说明由于这种批量插入而导致的速度增长的一些数字。
您可以将文档检索为TDocVariant实例:
var doc: variant;
...
doc := Coll.FindOne(5);
writeln('Name: ',doc.Name);
writeln('Number: ',doc.Number);
将在控制台写入:
Name: Name 6
Number: 5
如果需要,您可以访问整个Query参数:
doc := Coll.FindDoc('{_id:?}',[5]);
doc := Coll.FindOne(5); // same as previous
这个Query过滤器类似于SQL中的WHERE子句,如果需要,您可以编写复杂的查询模式,参见http://docs.mongodb.org/manual/reference/method/db.collection.find。
您可以将文档列表检索为TDocVariant的动态数组:
var docs: TVariantDynArray;
...
Coll.FindDocs(docs);
for i := 0 to high(docs) do
writeln('Name: ',docs[i].Name,' Number: ',docs[i].Number);
将输出:
Name: Name 2 Number: 1
Name: Name 3 Number: 2
Name: Name 4 Number: 3
Name: Name 5 Number: 4
Name: Name 6 Number: 5
Name: Name 7 Number: 6
Name: Name 8 Number: 7
Name: Name 9 Number: 8
Name: Name 10 Number: 9
Name: Name 11 Number: 10
在GUI应用中,可以使用SynVirtualDataSet.pas中定义的TDocVariantArrayDataSet填充VCL表格:
ds1.DataSet.Free; // release previous TDataSet
ds1.DataSet := ToDataSet(self,FindDocs('{name:?,age:{$gt:?}}',['John',21],null));
这个重载的FindDocs()方法接受一个JSON查询过滤器和参数(遵循MongoDB语法),以及一个投影映射(null以检索所有属性)。它返回一个TVariantDynArray结果,该结果使用重载的ToDataSet()函数映射到优化的只读TDataSet。所以在我们的例子中,DB表格中填满了所有名为'John'、年龄大于21岁的人。
如果你想直接以JSON格式检索文档,我们可以这样写:
var json: RawUTF8;
...
json := Coll.FindJSON(null,null);
writeln(json);
...
这将把以下内容输出到控制台:
[{"_id":1,"Name":"Name 2","Number":1},{"_id":2,"Name":"Name 3","Number":2},{"_id
":3,"Name":"Name 4","Number":3},{"_id":4,"Name":"Name 5","Number":4},{"_id":5,"N
ame":"Name 6","Number":5},{"_id":6,"Name":"Name 7","Number":6},{"_id":7,"Name":"
Name 8","Number":7},{"_id":8,"Name":"Name 9","Number":8},{"_id":9,"Name":"Name 1
0","Number":9},{"_id":10,"Name":"Name 11","Number":10}]
可以注意到FindJSON()有两个属性,一个是查询过滤器,另一个是投影映射(类似于SELECT col1,col2中的列名)。
所以我们可以这样写:
json := Coll.FindJSON('{_id:?}',[5]);
writeln(json);
将输出:
[{"_id":5,"Name":"Name 6","Number":5}]
这里我们使用重载的FindJSON()方法,它接受MongoDB扩展语法(这里字段名未加引号)和参数作为变量。
我们可以指定一个投影:
json := Coll.FindJSON('{_id:?}',[5],'{Name:1}');
writeln(json);
它只返回“Name”和“_id”字段(因为根据MongoDB约定,_id总是返回):
[{"_id":5,"Name":"Name 6"}]
若仅返回“Name”字段,可以使用JSON扩展语法将'_id:0,Name:1'指定为投影参数。
[{"Name":"Name 6"}]
还有其他方法可以检索数据,也可以直接作为BSON二进制数据。它们将拥有最佳速度,如与ORM结合使用,但是对于大多数最终用户代码,使用TDocVariant更安全易于维护。
TMongoCollection类有一些用于修改现有文档的方法。
首先,Save()方法可用于更个首先检索到的文档:
doc := Coll.FindOne(5);
doc.Name := 'New!';
Coll.Save(doc);
writeln('Name: ',Coll.FindOne(5).Name);
将输出:
Name: New!
注意,这里我们使用了一个整数值(5)作为关键字,但如果需要,我们可以使用一个ObjectID。
Coll.Save()方法可以更改为Coll.Update(),更新文档内容时需要显式指定Query操作符:
doc := Coll.FindOne(5);
doc.Name := 'New!';
Coll.Update(BSONVariant(['_id',5]),doc);
writeln('Name: ',Coll.FindOne(5).Name);
注意,根据MongoDB的设计,对Update()的任何调用都将替换整个文档。
例如,如果你写成:
writeln('Before: ',Coll.FindOne(3));
Coll.Update('{_id:?}',[3],'{Name:?}',['New Name!']);
writeln('After: ',Coll.FindOne(3));
那么Number字段将消失!
Before: {"_id":3,"Name":"Name 4","Number":3}
After: {"_id":3,"Name":"New Name!"}
如果只需要更新某些字段,则必须使用$set修饰符:
writeln('Before: ',Coll.FindOne(4));
Coll.Update('{_id:?}',[4],'{$set:{Name:?}}',['New Name!']);
writeln('After: ',Coll.FindOne(4));
这将在控制台输出:
Before: {"_id":4,"Name":"Name 5","Number":4}
After: {"_id":4,"Name":"New Name!","Number":4}
现在Number字段保持不变。
您还可以使用Coll.UpdateOne()方法,该方法将更新提供的字段,并保持未指定字段不变:
writeln('Before: ',Coll.FindOne(2));
Coll.UpdateOne(2,_Obj(['Name','NEW']));
writeln('After: ',Coll.FindOne(2));
将输出:
Before: {"_id":2,"Name":"Name 3","Number":2}
After: {"_id":2,"Name":"NEW","Number":2}
您可以参考`SynMongoDB.pas单元的文档,找到所有可用的函数、类和方法来使用MongoDB。
还有一些非常强大的可用特性,包括聚合(MongoDB 2.2开始提供),是标准Map/Reduce模式的一个很好的替代方案。
参考http://docs.mongodb.org/manual/reference/command/aggregate。
您可以查看一下MongoDBTests.dpr示例,位于源代码存储库SQLite3\Samples\24 - MongoDB子文件夹,及TTestDirect类,以找到一些性能信息。
实际上,这个TTestDirect被继承了两次,以便以不同的写回执运行相同的测试:
两个类之间的差异主要发生在客户端初始化:
procedure TTestDirect.ConnectToLocalServer;
...
fClient := TMongoClient.Create('localhost',27017);
if ClassType=TTestDirectWithAcknowledge then
fClient.WriteConcern := wcAcknowledged else
if ClassType=TTestDirectWithoutAcknowledge then
fClient.WriteConcern := wcUnacknowledged;
...
wcAcknowledged是默认的安全模式:MongoDB服务器需要确认收到写操作。已确认的写回执允许客户端捕获网络、重复主键和其他错误。但是它增加了从客户机到服务器的额外往返,并且在返回错误状态之前等待命令完成,因此它将减慢写进程。
在wcUnacknowledged的情况下,MongoDB不确认收到写操作,不确认就类似于忽略错误;但驱动程序试图在可能的情况下接收和处理网络错误,驱动程序检测网络错误的能力取决于系统的网络配置。
两者之间的速度差异值得一提,正如回归测试状态所述,本地MongoDB实例上运行:
1. Direct access
1.1. Direct with acknowledge:
- Connect to local server: 6 assertions passed 4.72ms
- Drop and prepare collection: 8 assertions passed 9.38ms
- Fill collection: 15,003 assertions passed 558.79ms
5000 rows inserted in 548.83ms i.e. 9110/s, aver. 109us, 3.1 MB/s
- Drop collection: no assertion 856us
- Fill collection bulk: 2 assertions passed 74.59ms
5000 rows inserted in 64.76ms i.e. 77204/s, aver. 12us, 7.2 MB/s
- Read collection: 30,003 assertions passed 2.75s
5000 rows read at once in 9.66ms i.e. 517330/s, aver. 1us, 39.8 MB/s
- Update collection: 7,503 assertions passed 784.26ms
5000 rows updated in 435.30ms i.e. 11486/s, aver. 87us, 3.7 MB/s
- Delete some items: 4,002 assertions passed 370.57ms
1000 rows deleted in 96.76ms i.e. 10334/s, aver. 96us, 2.2 MB/s
Total failed: 0 / 56,527 - Direct with acknowledge PASSED 4.56s
1.2. Direct without acknowledge:
- Connect to local server: 6 assertions passed 1.30ms
- Drop and prepare collection: 8 assertions passed 8.59ms
- Fill collection: 15,003 assertions passed 192.59ms
5000 rows inserted in 168.50ms i.e. 29673/s, aver. 33us, 4.4 MB/s
- Drop collection: no assertion 845us
- Fill collection bulk: 2 assertions passed 68.54ms
5000 rows inserted in 58.67ms i.e. 85215/s, aver. 11us, 7.9 MB/s
- Read collection: 30,003 assertions passed 2.75s
5000 rows read at once in 9.99ms i.e. 500150/s, aver. 1us, 38.5 MB/s
- Update collection: 7,503 assertions passed 446.48ms
5000 rows updated in 96.27ms i.e. 51933/s, aver. 19us, 7.7 MB/s
- Delete some items: 4,002 assertions passed 297.26ms
1000 rows deleted in 19.16ms i.e. 52186/s, aver. 19us, 2.8 MB/s
Total failed: 0 / 56,527 - Direct without acknowledge PASSED 3.77s
如您所见,读取速度不受写回执设置的影响。
但是,当写命令都不需要回执时,数据写入速度可能会快好几倍。
由于没有错误处理,wcUnacknowledged不能用于生产。您可以将其用于复制或数据整合,如以尽可能快的速度为数据库提供大量现有数据。
mORMotMongoDB.pas单元能够在远程MongoDB服务器上持久化任何TSQLRecord类。
因此,我们的ORM可以作为NoSQL和对象文档映射(ODM)框架使用,几乎不需要更改代码。任何MongoDB数据库都可以通过RESTful命令访问,使用JSON而不是HTTP。
这种集成得益于框架的其他部分(如我们的utf-8专用处理,这也是BSON原生编码),所以你可以很容易地在相同的代码中混合使用SQL和NoSQL数据库,并仍然能够根据需要在代码中调整任何SQL或MongoDB请求。
从客户端的角度来看,ORM或ODM没有区别:你可以使用一个SQL引擎用于ODM,通过存储无共享体系结构(或分区),甚至反规范化地将NoSQL数据库用于一个常规的ORM(即使这样可能损失NoSQL的优势)。
在数据库模型中,我们像往常一样定义了一个TSQLRecord类:
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;
注意,我们没有为RawUTF8属性定义任何index ...值,因为我们使用外部SQL数据库,而MongoDB不需要限制文本字段长度(据我所知,唯一原生地支持这种属性而不影响性能的SQL引擎是SQlite3和PostgreSQL)。
属性值将存储在本地MongoDB中,相对于SQL类型,我们的SynDB*单元支持的类型得更多:
| Delphi | MongoDB | 备注 |
|---|---|---|
| byte | Int32 | |
| Word | Int32 | |
| Integer | Int32 | |
| Cardinal | N/A | 应该用Int64代替 |
| Int64 | Int64 | |
| boolean | boolean | MongoDB支持boolean类型 |
| enumeration | Int32 | 存储枚举项的序号值(第一个元素从0开始) |
| set | Int64 | 每个位对应一个枚举项(因此最多可以存储64个元素) |
| single | double | |
| double | double | |
| extended | double | 存储为双精度类型(有精度损失) |
| currency | double | 存储为双精度类型 (MongoDB没有BSD类型) |
| RawUTF8 | UTF-8 | ORM中存储文本内容的首选字段类型 |
| WinAnsiString | UTF-8 | Delphi的WinAnsi字符集(1252代码页) |
| RawUnicode | UTF-8 | Delphi的UCS2字符集,作为AnsiString |
| WideString | UTF-8 | UCS2字符集,作为COM BSTR类型(Delphi所有版本的Unicode) |
| SynUnicode | UTF-8 | Delphi 2009之前为WideString,之后为UnicodeString |
| string | UTF-8 | Delphi 2009之前未使用(否则在转换过程中可能会丢失数据),在所有情况下,RawUTF8都是首选 |
| TDateTime TDateTimeMS |
datetime | ISO 8601日期时间编码 |
| TTimeLog | Int64 | 专用的快速Int64日期时间 |
| TModTime | Int64 | 修改记录时将存储为服务器日期时间(专用快速Int64) |
| TCreateTime | Int64 | 创建记录时将存储为服务器日期时间(专用快速Int64) |
| TUnixTime | Datetime | Unix时间以来的秒数 |
| TSQLRecord | Int32 | 指向另一条记录的32位RowID,字段值包含的是pointer(RowID),而不是有效的对象实例,记录内容必须通过PtrInt(Field)类型转换或Field.ID进行后期绑定ID来检索;或使用CreateJoined(),在Win64上是64位的 |
| TID | int32/int64 | 指向另一条记录的RowID,这种属性是64位兼容的,因此最大可以处理到9,223,372,03,864,775,808 |
| TSQLRecordMany | Nothing | 数据存储在单独的数据透视表中;对于MongoDB,您应该更好地使用数据分片和嵌入式子文档 |
| TRecordReference TRecordReferenceToBeDeleted |
Int32/int64 | 将ID和TSQLRecord类型存储在一个类似RecordRef的值中,在删除记录时会正确同步 |
| TPersistent | object | BSON对象(通过ObjectToJSON) |
| TCollection | array | BSON对象数组(通过ObjectToJSON) |
| TObjectList | array | BSON对象数组(通过ObjectToJSON),参见TJSONSerializer.RegisterClassForJSON |
| TStrings | array | BSON字符串数组(通过ObjectToJSON) |
| TRawUTF8List | array | BSON字符串数组(通过ObjectToJSON) |
any TObject |
object | 参见TJSONSerializer.RegisterCustomSerializer |
| TSQLRawBlob | binary | RawByteString的别名,这些属性在默认情况下不会被检索:您需要使用RetrieveBlobFields()或设置ForceBlobTransfert / ForceBlobTransertTable[]属性 |
| TByteDynArray | binary | 将BLOB属性作为BSON二进制文件存储在文档中,将来TSQLRawBlob可能被限制为GridFS外部内容 |
| dynamic arrays | array binary |
如果动态数组可以保存为真正的JSON,则存储为BSON数组,否则按TDynArray.SaveTo存储为BSON二进制格式 |
| variant | value array |
|
| object | BSON数值、文本、日期、对象或数组,取决于TDocVariant自定义变体类型或TBSONVariant存储值(如存储MongoDB原生类型,如ObjectID或Decimal128) | |
| record | binary object |
通过覆盖生成BSON的TSQLRecord.InternalRegisterCustomProperties,以生成真正的JSON |
您可以与MongoDB和其他存储方式(如外部SQL数据库)共享相同的TSQLRecord定义,忽略不支持的信息(如索引属性)。
注意TSQLRecord、TID和TRecordReference*发布属性会在对应字段自动创建索引,TSQLRecord和TRecordReference属性会跟踪ON DELETE SET DEFAULT操作,TRecordReferenceToBeDeleted会跟踪ON DELETE CASCADE 操作,但TID不会,因为我们不知道跟踪哪个表。
在服务端(客户端也不会有任何区别),您定义了一个TMongoDBClient,并通过调用StaticMongoDBRegister()将其分配给一个给定的TSQLRecord类:
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');
这就是所有!
如果mORMot服务端的所有表都应该驻留在MongoDB服务器上,那么也可以调用StaticMongoDBRegisterAll()函数:
StaticMongoDBRegisterAll(aServer,aMongoClient.Open('colllectionname'));
如果您想调用TSQLRecord.InitializeTable方法创建void表(如创建TSQLAuthGroup和TSQLAuthUser默认内容),可以执行以下命令:
Client.Server.InitializeTables(INITIALIZETABLE_NOINDEX);
之后您可以像往常一样执行任何ORM命令:
writeln(Client.TableRowCount(TSQLORM)=0);
与外部数据库一样,您可以指定对象和MongoDB集合之间的字段名映射。
默认情况下是TSQLRecord.ID属性映射到MongoDB的_id字段,ORM将用一个整数值序列填充这个_id字段,就像任何TSQLRecord表一样。
你可以指定你自己的映射,如:
aModel.Props[aClass].ExternalDB.MapField(..)
由于字段名存储在文档本身中,因此最好对MongoDB集合使用更短的命名。在处理大量文档时,它可以节省一些存储空间。
一旦将TSQLRecord映射到一个MongoDB集合,您总是可以在稍后直接访问相应的TMongoCollection实例,只需使用一个简单的类型转换:
(aServer.StaticDataServer[aClass] as TSQLRestStorageMongoDB).Collection
这可能允许任何特定任务,包括任何优化的查询或处理。
您可以像往常一样,使用ORM的标准CRUD方法添加文档:
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;
正如我们已经看到的,框架能够处理任何类型的属性,包括动态数组或变体等复杂类型。
在上面的代码中,一个TDocVariant文档存储在R.Value中,并通过索引1和TSQLRecord.DynArray()方法访问动态整数数组。
常用的Retrieve / Delete / Update方法也可用有:
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;
你可以定义一个WHERE子句,就好像后台是一个普通的SQL数据库:
R := TSQLORM.CreateAndFillPrepare(Client,'ID=?',[i]);
try
...
要执行查询并检索多个文档的内容,可以使用常规的CreateAndFillPrepare或FillPrepare方法:
R := TSQLORM.CreateAndFillPrepare(Client,WHERE_CLAUSE,[WHERE_PARAMETERS]);
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;
还可以为CreateAndFillPrepare或FillPrepare方法定义WHERE子句,WHERE子句可以包含几个与AND / OR连接的表达式。
下面这些表达都可以用:
= < <= <> > >=,IN (....)子句,IS NULL / IS NOT NULL 判断,LIKE操作,...DynArrayContains()特定函数。 mORMot ODM将把这个类似SQL的语句转换为优化的MongoDB查询表达式,例如,使用LIKE操作符的正则表达式。
LIMIT、OFFSET和ORDER BY子句也将按照要求进行处理。应该特别注意文本值上的ORDER BY:按照设计,MongoDB总是以区分大小写的方式对文本进行排序,这不是我们所期望的,所以我们的ODM将从MongoDB服务器检索到这些内容后在客户端对这些内容进行排序。对于数值字段,MongoDB的排序特性将在服务器端进行处理。
COUNT(*)函数也将被转换成适当的MongoDB API调用,以便这样的操作将尽可能地节省成本。DISTINCT() MAX() MIN() SUM() AVG()函数和GROUP BY子句也将动态转换为优化的MongoDB聚合操作,您甚至可以为列设置别名(如max(RowID) as first),并对整数值执行简单的加减操作。
下面是一些典型的WHERE子句,以及ODM生成的相应MongoDB查询文档:
| WHERE子句 | MongoDB查询 |
|---|---|
'Name=?',['Name 43'] |
{Name:"Name 43"} |
'Age<?',[51] |
{Age:{$lt:51}} |
'Age in (1,10,20)' |
{Age:{$in:[1,10,20]}} |
'Age in (1,10,20) and ID=?',[10] |
{Age:{$in:[1,10,20]},_id:10} |
'Age in (10,20) or ID=?',[30] |
{$or:[{Age:{$in:[10,20]}},{_id:30}]} |
'Name like ?',['name 1%'] |
{Name:/^name 1/i} |
'Name like ?',['name 1'] |
{Name:/^name 1$/i} |
'Name like ?',['%ame 1%'] |
{Name:/ame 1/i} |
'Data is null' |
{Data:null} |
'Data is not null' |
{Data:{$ne:null}} |
'Age<? limit 10',[51] |
{Age:{$lt:51}} + limit 10 |
'Age in (10,20) or ID=? order by ID desc',[30] |
{$query:{$or:[{Age:{$in:[10,20]}},{_id:30}]},$orderby:{_id:-1}} |
'order by Name' |
{} + client side text sort by Name |
'Age in (1,10,20) and IntegerDynArrayContains(Ints,?)',[10]) |
{Age:{$in:[1,10,20]},Ints:{$in:[10]}} |
Distinct(Age),max(RowID) as first,count(Age) as countgroup by age |
{$group:{_id:"$Age",f1:{$max:"$_id"},f2:{$sum:1}}},{$project:{_id:0,"Age":"$_id","first":"$f1","count":"$f2"}} |
min(RowID),max(RowID),Count(RowID) |
{$group:{_id:null,f0:{$min:"$_id"},f1:{$max:"$_id"},f2:{$sum:1}}},{$project:{_id:0,"min(RowID)":"$f0","max(RowID)":"$f1","Count(RowID)":"$f2"}} |
min(RowID) as a,max(RowID)+1 as b,Count(RowID) as c |
{$group:{_id:null,f0:{$min:"$_id"},f1:{$max:"$_id"},f2:{$sum:1}}},{$project:{_id:0,"a":"$f0","b":{$add:["$f1",1]},"c":"$f2"}} |
注意括号和混合AND OR表达式还没有处理。通过直接使用TMongoCollection方法,您总是可以执行任何复杂的NoSQL查询(例如使用聚合函数或Map/Reduce模式)。
但是对于大多数业务代码,mORMot允许在常规SQL数据库或NoSQL引擎之间共享相同的代码。您不需要学习MongoDB查询语法:ODM将根据所运行的数据库引擎,为您计算正确的表达式。
除了单独的CRUD操作,MongoDB还可以使用批处理模式添加或删除文档。
您可以编写与任何SQL后端完全相同的代码:
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)=HTTP_SUCCESS);
或使用删除:
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)=HTTP_SUCCESS);
对于单独的添加/删除操作,速度优势可能非常大,甚至在本地MongoDB服务器上也是如此。我们将看到一些基准数据。
您可以查看数据访问基准测试,比较MongoDB作为ORM类的后端。
对于外部SQL引擎,它具有非常高的速度、较低的CPU消耗,并且在使用上几乎没有区别。我们将BatchAdd()和BatchDelete()方法结合在一起,以利用MongoDB的BULK进程,避免了进程中大部分的内存分配。
以下是从MongoDBTests.dpr示例中提取的一些数字。它反映了ORM/ODM的性能,取决于是否使用写回执模式:
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
对于直接的MongoDB访问,wcUnacknowledged不能用于生产环境,但在某些特定场景中可能非常有用。正如预期的那样,读取过程不受写关回执模式设置的影响。