9. 外部NoSQL数据库访问

  我们的ORM RESTful框架不仅能够通过SynDB直接RDBMS访问常规的SQL数据库引擎,还能够访问NoSQL引擎,参见NoSQL和对象文档映射(ODM)

  还记得介绍mORMot数据库层的图:

direct
direct
direct
direct
virtual
virtual
direct
direct
DB.pas
TDataSet
TObjectList
NoSQL
External SQL
RDBMS
MongoDB
NoSQL
TSQLRest
redirection
mORMot
ORM
SQLite3
Oracle SQLite3 ODBC
OleDB ZDBC
FireDAC AnyDAC UniDAC
BDE DBExpress NexusDB

  可以从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方法。

9.1. SynMongoDB客户端

  SynMongoDB.pas单元可直接访问MongoDB服务器并进行了优化。

  它可以访问任何BSON数据,包括文档、数组和MongoDB的自定义类型(如ObjectID、date、binary、regex、Decimal128或Javascript):

  • 如使用TBSONObjectID在客户端创建一些真正的文档标识符(MongoDB不会为您生成ID:通常是在客户端生成唯一ID);
  • 从任何Delphi类型生成BSON内容(通过TBSONWriter);
  • BSON流的快速原地解析,无需任何内存分配(通过TBSONElement);
  • 使用TBSONVariant自定义变体类型存储MongoDB的自定义类型值;
  • 使用SynCommons.pasTDocVariant自定义变体类型作为文档存储和后期绑定访问;
  • 使用MongoDB扩展语法处理其自定义类型,支持BSON与JSON编码的相互转换。

  该单元定义了一些对象,这些对象能够连接和管理任何MongoDB服务机群上的数据库和文档集合:

  • 通过TMongoClient类连接到一个或多个服务器,包括辅助主机;
  • 通过TMongoDatabase类访问任何数据库实例;
  • 通过TMongoCollection类访问任何集合;
  • 具有速度方面的优势,比如批量插入或删除模式,以及显式设置为写关注。

  在集合级,您可以直接访问数据,使用TDocVariant/TBSONVariant等高级结构,使用易于阅读的JSON或底层BSON内容。

  您还可以在很多方面优化客户端流程,例如关于错误处理或写回执(即如何确认远程数据的修改)。

9.1.1. 连接到服务端

  下面是一些示例代码,它能够连接到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()方法允许轻松管理来自应用程序的凭据。

9.1.2. 向集合添加文档

  现在我们将解释如何向给定集合添加文档。

  我们假设有一个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
...

  稍后您将说明由于这种批量插入而导致的速度增长的一些数字。

9.1.3. 检索文件

  您可以将文档检索为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更安全易于维护。

9.1.3.1. 更新或删除文档

  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。

9.1.4. 写回执和性能

  您可以查看一下MongoDBTests.dpr示例,位于源代码存储库SQLite3\Samples\24 - MongoDB子文件夹,及TTestDirect类,以找到一些性能信息。

  实际上,这个TTestDirect被继承了两次,以便以不同的写回执运行相同的测试:

TTestDirectWithAcknowledge
TTestDirect
TTestDirectWithoutAcknowledge

  两个类之间的差异主要发生在客户端初始化:

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不能用于生产。您可以将其用于复制或数据整合,如以尽可能快的速度为数据库提供大量现有数据。

9.2. MongoDB + ORM = ODM

  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的优势)。

9.2.1. 定义TSQLRecord类

  在数据库模型中,我们像往常一样定义了一个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 IDTSQLRecord类型存储在一个类似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定义,忽略不支持的信息(如索引属性)。

  注意TSQLRecordTIDTRecordReference*发布属性会在对应字段自动创建索引,TSQLRecordTRecordReference属性会跟踪ON DELETE SET DEFAULT操作,TRecordReferenceToBeDeleted会跟踪ON DELETE CASCADE 操作,但TID不会,因为我们不知道跟踪哪个表。

9.2.2. 注册TSQLRecord类

  在服务端(客户端也不会有任何区别),您定义了一个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

  这可能允许任何特定任务,包括任何优化的查询或处理。

9.2.3. ORM / ODM CRUD方法

  您可以像往常一样,使用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
    ...

9.2.4. ODM复杂查询

  要执行查询并检索多个文档的内容,可以使用常规的CreateAndFillPrepareFillPrepare方法:

  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;

  还可以为CreateAndFillPrepareFillPrepare方法定义WHERE子句,WHERE子句可以包含几个与AND / OR连接的表达式。

  下面这些表达都可以用:

  • 简单比较器= < <= <> > >=
  • IN (....)子句,
  • IS NULL / IS NOT NULL 判断,
  • LIKE操作,
  • 甚至各种...DynArrayContains()特定函数。

  mORMot ODM将把这个类似SQL的语句转换为优化的MongoDB查询表达式,例如,使用LIKE操作符的正则表达式。

  LIMITOFFSETORDER 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将根据所运行的数据库引擎,为您计算正确的表达式。

9.2.5. 批处理模式

  除了单独的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服务器上也是如此。我们将看到一些基准数据。

9.2.6. ORM / ODM性能

  您可以查看数据访问基准测试,比较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不能用于生产环境,但在某些特定场景中可能非常有用。正如预期的那样,读取过程不受写关回执模式设置的影响。