这个框架的核心数据库使用SQLite3库,它是一个免费的、安全的、零配置的、无服务器的、单一稳定的跨平台文件数据库引擎。
译者注:Server-less,无服务架构,旨在帮助开发者摆脱运行后端应用程序所需的服务器设备的设置和管理工作。这项技术的目标并不是为了实现真正意义上的“无服务器”,而是指由第三方供应商负责后端基础结构的维护,以服务的方式为开发者提供所需功能,例如数据库、消息,以及身份验证等。这种服务基础结构通常可以叫做后端即服务(Backend-as-a-Service,BaaS),或移动后端即服务(MobileBackend-as-a-service,MBaaS)。
现在,无服务器架构是指大量依赖第三方服务(也叫做后端即服务,即“BaaS”)或暂存容器中运行的自定义代码(函数即服务,即“FaaS”)的应用程序,函数是无服务器架构中抽象语言运行时的最小单位,在这种架构中,我们并不看重运行一个函数需要多少CPU或RAM或任何其他资源,而是更看重运行函数所需的时间,我们也只为这些函数的运行时间付费。无服务器架构中函数可以多种方式触发,如定期运行函数的定时器、HTTP请求或某些相关服务中的某个事件。
如下所述,如果您愿意,您可以使用任何其他数据库访问层:
SQlite3将被用作主要的SQL引擎,由于其虚拟表的独特特性,它能够连接所有上述这些表。实际上,您可以在相同的数据库模型中混合使用内部和外部引擎,并在一条SQL语句中访问所有数据。
这个框架使用了官方SQLite3库源代码的编译版本,并将其集成在Delphi代码中。因此,这个框架为标准的SQLite3数据库引擎添加了一些非常有用的功能,但又保留了它的所有优势,如本文前一段所述:
从技术角度来看,以下是目前构建SQLite3引擎时使用的编译选项:
译者注:SQLITE_OMIT_SHARED_CACHE,省略共享缓存,允许消除代码中性能部分的许多关键条件,可以给性能带来明显的改善。
SHARED_CACHE,同一线程或同一进程对同一数据库的多个连接(connection)可以以共享缓存的方式呈现,实际上对于数据库只有一个连接。
在您的服务端应用包含SQlite3所付出的成本是值得的,对可执行文件只增加了KB级字节,但是有很多不错的特性,即使只使用其它外部数据库。
由于框架是真正面向对象的,因此可以使用其它数据库引擎来代替框架。您可以很容易地编写自己的TSQLRestServer派生类(我们包含了一个快速内存数据库引擎TSQLRestServerFullMemory可作为参考示例)并连接到另一个引擎(比如FireBird或专用引擎)。您甚至可以使用我们的框架,而不需要连接到SQLite3引擎本身,仅使用我们提供的高速内存数据集(可以通过在磁盘上写入和读取JSON文件来实现持久化)。SQLite3引擎在一个名为SynSQLite3.pas的独立单元中实现,框架的主要单元是mORMot.pas,两个单元通过mORMotSQLite3.pas桥接,从中可看出我们的ORM框架使用了SQLite3作为其核心。
框架的ORM能够通过强大的SQLite3虚拟表机制访问任意数据库类(内部或外部)。例如,任何外部数据库(通过OleDB / ODBC / ZDBC提供程序或直接Oracle连接)都可以通过我们的SynDB.pas专用单元实现访问。
因此,除了正常的基于文件的SQLite3引擎外,框架还支持其它的数据库后端。根据应用的需求,每个引擎都有自己的用途。目前,ORM可支持SQLite3、Oracle、Jet/MSAccess、MS SQL、Firebird、DB2、PostgreSQL、MySQL、Informix和NexusDB SQL。
这里的目的不是说一个库或数据库比另一个更好或更快,而是发布mORMot持久化层针对每个数据库后端的访问能力的基本情况。
在这种情况下,我们不只是对单纯的SQL/DB层访问能力进行基准测试(SynDB.pas单元),还包括框架在C/S架构下的ORM访问能力。
以下测试过程涉及ORM的各个方面:
在这些测试中,我们只是绕过了通信层,因为TSQLRestClient和TSQLRestServer是在进程中运行的,在同一个线程中,就像TSQLRestServerDB实例一样。因此,这里有一些关于我们的框架的ORM和RESTful核心性能的第一手证据,当通过网络在高端硬件上运行时,框架会有很好的伸缩性。
在最近的笔记本电脑(Core i7和SSD驱动器)上,根据后端数据库接口,mORMot在速度方面表现出色,如下所示:
我想很难找到一个更快的ORM。
下面的表总结了所有的配置选项,并给出一些基准测试(平均每秒读写对象)。
Synchronous := smOff是否开启或DB.LockingMode := lmExclusive,参见下图;此数据库驱动程序列表未来将扩充,欢迎任何反馈!
数字表示行/秒或对象/秒,这个基准测试是用Delphi XE4编译的,因为较新的编译器往往会提供更好的结果,这主要归功于函数内联(在Delphi 6-7中不支持)。
注意,这些测试不是对每个数据库引擎的速度比较,而是在mORMot中集成了多个数据库进行访问的当前状态。
基准测试运行在i7笔记本上,运行Windows 7,配备标准SSD,包括杀毒和后台应用程序:
所以它是一个开发环境,非常类似于低成本的生产现场,不致力于提供最好的性能。在此过程中,CPU只被用于内存中的SQLite3和TObjectList,大多数情况下,瓶颈不是CPU,而是存储或网络。因此,速率和时间可能会因网络和服务器负载的不同而有所不同,但是您得到的结果与在客户端使用普通硬件配置时所期望的结果相似。当使用优化后的Linux高级服务器和存储运行时,您可以期望得到更好的数字。
测试是用Delphi XE4 32位模式编译的。大多数测试在编译为64位程序时都能通过,但是有些程序(如Jet)除外,它在这个平台上是不可用的。速度结果几乎相同,只是稍微慢一些,这里就不展示了。
您可以编译“15 - External DB performance”提供的示例代码,并在自己的配置上运行相同的基准测试。欢迎反馈!
在我们的测试中,我们使用的UniDAC版本在与DB2一起使用时存在很大的稳定性问题:测试没有通过,DB2服务器会挂起查询操作,而其他库没有问题。它现在可能已经修复了,但是在这里,您在下面的基准测试中找不到关于“UniDAC DB2”的结果。
我们将在不同的场景各插入5000行数据:
Client.Add()逐条插入。下面是一些插入速度值(单位:对象/秒):
| Direct | Batch | Trans | Batch Trans | |
|---|---|---|---|---|
| SQLite3 (file full) | 462 | 28123 | 84823 | 181455 |
| SQLite3 (file off) | 2102 | 83093 | 88006 | 202667 |
| SQLite3 (file off exc) | 28847 | 193453 | 89451 | 207615 |
| SQLite3 (mem) | 89456 | 236540 | 104249 | 239165 |
| TObjectList (static) | 314465 | 543892 | 326370 | 542652 |
| TObjectList (virtual) | 325393 | 545672 | 298846 | 545018 |
| SQLite3 (ext full) | 424 | 14523 | 102049 | 164636 |
| SQLite3 (ext off) | 2245 | 47961 | 109706 | 189250 |
| SQLite3 (ext off exc) | 41589 | 180759 | 108481 | 192071 |
| SQLite3 (ext mem) | 101440 | 211389 | 113530 | 209713 |
| WinHTTP SQLite3 | 2165 | 36464 | 2079 | 38478 |
| Sockets SQLite3 | 8118 | 75251 | 8553 | 80550 |
| MongoDB (ack) | 10081 | 84585 | 9800 | 85232 |
| MongoDB (no ack) | 33223 | 273672 | 34665 | 274393 |
| ODBC SQLite3 | 492 | 11746 | 35367 | 82425 |
| ZEOS SQlite3 | 494 | 11851 | 56206 | 85705 |
| FireDAC SQlite3 | 20605 | 38853 | 40042 | 113752 |
| UniDAC SQlite3 | 477 | 8725 | 26552 | 38756 |
| ODBC Firebird | 1495 | 18056 | 13485 | 17731 |
| ZEOS Firebird | 10452 | 62851 | 22003 | 63708 |
| FireDAC Firebird | 18147 | 46877 | 18922 | 46353 |
| UniDAC Firebird | 5986 | 14809 | 6522 | 14948 |
| Jet | 4235 | 4424 | 4954 | 5094 |
| NexusDB | 5998 | 15494 | 7687 | 18619 |
| Oracle | 226 | 56112 | 1133 | 52367 |
| ZEOS Oracle | 210 | 32725 | 1027 | 31982 |
| ODBC Oracle | 236 | 1664 | 1515 | 7709 |
| FireDAC Oracle | 118 | 48575 | 1519 | 12566 |
| UniDAC Oracle | 164 | 5701 | 1215 | 2884 |
| BDE Oracle | 489 | 927 | 839 | 1022 |
| MSSQL local | 5246 | 54360 | 12988 | 62453 |
| ODBC MSSQL | 4911 | 18652 | 11541 | 20976 |
| FireDAC MSSQL | 5016 | 7341 | 11686 | 51242 |
| UniDAC MSSQL | 4392 | 29768 | 8649 | 33464 |
| ODBC DB2 | 4792 | 48387 | 14085 | 70104 |
| FireDAC DB2 | 4452 | 48635 | 11014 | 52781 |
| ZEOS PostgreSQL | 4196 | 31409 | 9689 | 41225 |
| ODBC PostgreSQL | 4068 | 26262 | 5130 | 30435 |
| FireDAC PostgreSQL | 4181 | 26635 | 10111 | 36483 |
| UniDAC PostgreSQL | 2705 | 18563 | 4442 | 28337 |
| ODBC MySQL | 3160 | 38309 | 10856 | 47630 |
| ZEOS MySQL | 3426 | 34037 | 12217 | 40186 |
| FireDAC MySQL | 3078 | 43053 | 10955 | 45781 |
| UniDAC MySQL | 3119 | 27772 | 11246 | 33288 |
由于其ACID实现,SQLite3进程等待硬盘将数据刷新到文件中,这就是它在不使用事务逐条插入比其他引擎慢的原因(每秒小于10个对象,采用机硬盘而不是SDD)。
因此,如果您希望使用默认引擎在应用程序中获得最佳的写入性能,那么您应该更好地使用事务,并将所有写入重新分组到服务或批处理流程中。另一种可能是SQLite3处理数据前先执行DB.Synchronous := smOff和/或DB.LockingMode := lmExclusive,在掉电并凑巧的情况下可能出现数据库文件损坏,但它会增加50倍速率(硬盘),参见表中“off'”和“off exc”行。默认情况下,FireDAC库两个选项都设置了,因此与“SQLite3 off exc”行才有可比性。在SQLite3 direct逐条插入模式中,批量处理多条插入语句优势明显(与采用外部数据库一样):这解释了为什么BatchAdd()比普通的Add()快,即使在最慢和最安全的“file full”模式中也是如此。
在我们的SynDBOracle.pas单元和SynDBZeos.pas或SynDBFireDAC.pas`(在FireDAC/AnyDAC中称为数组DML)库的Oracle direct逐条访问中,数组绑定特性的批处理优势很大。
对于大多数引擎,我们的ORM内核能够生成正确的SQL语句来加速批量插入。例如:
INSERT INTO .. VALUES (..),(..),(..)..处理INSERT语句;INSERT INTO .. INTO .. SELECT 1 FROM DUAL(奇怪的语法,不是吗?);EXECUTE BLOCK。 因此,当使用BatchAdd()时,一些引擎显示了很好的速度提升,即便使用SQLite3外部引擎也比逐条执行时更快!该特性处于ORM/SQL级别,因此它对任何外部数据库都有好处。当然,如果给定的库具有更好的实现模式(如我们的direct Oracle、Zeos或带有本机数组绑定的FireDAC),也会使用它。
MongoDB支持大容量数据插入,在批处理模式下也实现了惊人的速度提升。基于MongoDB写回执模式,插入速度可以非常高,默认情况下,对每个写操作服务器都有回执,但是你可以设置wcUnacknowledged 模式旁路回执,在这种情况下,任何错误(如一个唯一字段值重复)您永远得不到通知,所以它不能用于生产,除非你需要这个特性快速填充数据库,或尽可能快地合并一些数据。
译者注:write concern mode,写回执模式?
现在通过ORM层获取相同的数据:
Client.Retrieve()调用生成SELECT * FROM table WHERE ID=?);FillPrepare方法调用会运行SELECT * FROM table),强制使用虚表层,或direct static调用。以下是一些读取速度值(单位:对象/秒):
| By one | All Virtual | All Direct | |
|---|---|---|---|
| SQLite3 (file full) | 127284 | 558721 | 550842 |
| SQLite3 (file off) | 126896 | 549450 | 526149 |
| SQLite3 (file off exc) | 128077 | 557537 | 535905 |
| SQLite3 (mem) | 127106 | 557537 | 563316 |
| TObjectList (static) | 300012 | 912408 | 913742 |
| TObjectList (virtual) | 303287 | 402706 | 866551 |
| SQLite3 (ext full) | 135380 | 267436 | 553158 |
| SQLite3 (ext off) | 133696 | 262977 | 543065 |
| SQLite3 (ext off exc) | 134698 | 264186 | 558596 |
| SQLite3 (ext mem) | 137487 | 259713 | 557475 |
| WinHTTP SQLite3 | 2198 | 209231 | 340460 |
| Sockets SQLite3 | 8524 | 210260 | 387687 |
| MongoDB (ack) | 8002 | 262353 | 271268 |
| MongoDB (no ack) | 8234 | 272079 | 274582 |
| ODBC SQLite3 | 19461 | 136600 | 201280 |
| ZEOS SQlite3 | 33541 | 200835 | 306955 |
| FireDAC SQlite3 | 7683 | 83532 | 112470 |
| UniDAC SQlite3 | 2522 | 74030 | 96420 |
| ODBC Firebird | 3446 | 69607 | 97585 |
| ZEOS Firebird | 20296 | 114676 | 117210 |
| FireDAC Firebird | 2376 | 46276 | 56269 |
| UniDAC Firebird | 2189 | 66886 | 88102 |
| Jet | 2640 | 166112 | 258277 |
| NexusDB | 1413 | 120845 | 208246 |
| Oracle | 1558 | 120977 | 159861 |
| ZEOS Oracle | 1420 | 110367 | 137982 |
| ODBC Oracle | 1620 | 43441 | 45764 |
| FireDAC Oracle | 1231 | 42149 | 54795 |
| UniDAC Oracle | 688 | 27083 | 30093 |
| BDE Oracle | 860 | 3870 | 4036 |
| MSSQL local | 10135 | 210837 | 437905 |
| ODBC MSSQL | 12458 | 147544 | 256502 |
| FireDAC MSSQL | 3776 | 72123 | 94091 |
| UniDAC MSSQL | 2505 | 93231 | 135932 |
| ODBC DB2 | 7649 | 84880 | 124486 |
| FireDAC DB2 | 3155 | 71456 | 88264 |
| ZEOS PostgreSQL | 8833 | 158760 | 223583 |
| ODBC PostgreSQL | 10361 | 85680 | 120913 |
| FireDAC PostgreSQL | 2261 | 58252 | 79002 |
| UniDAC PostgreSQL | 864 | 86900 | 122856 |
| ODBC MySQL | 10143 | 65538 | 82447 |
| ZEOS MySQL | 2052 | 171803 | 245772 |
| FireDAC MySQL | 3636 | 75081 | 105028 |
| UniDAC MySQL | 4798 | 99940 | 146968 |
SQLite3实现了惊人的读取结果,这使得它非常适合大多数典型的ORM使用。在DB.LockingMode := lmExclusive模式下运行(见"off exc"行),读取速度非常快,这得益于数据库文件的独占访问。只有当希望与其他进程共享数据时,或者为了更好的伸缩性,或使用专用数据库服务器的n层物理结构,才使用外部数据库访问。
在上表中,似乎所有基于DB.pas的数据库读取速度都比其他的慢。事实上,TDataSet 才是真正的瓶颈,由于它的内部数据编码。即使是众所周知速度优化优秀的FireDAC,也受到TDataSet结构的限制。我们的直接类,包括ZEOS/ZDBC的性能也更好,因为它们能够通过专用的ColumnsToJSON()方法输出JSON内容,而不需要额外的编码。
对于写和读,TObjectList / TSQLRestStorageInMemory引擎都给出了令人印象深刻的结果,但是它的缺点是内存模式,不是为ACID设计的,而且数据必须装在内存。注意,对于id和stored AS_UNIQUE属性可使用索引。
搜索非唯一值可能会很慢:引擎必须遍历所有数据行。但是对于唯一值(定义为stored AS_UNIQUE),插入和搜索速度都非常棒,这是由于其优化的O(1)哈希算法,特别是“TObjectList”列的“By name”行,它对应使用这种哈希算法搜索RawUTF8唯一属性值。
| SQLite3 (file full) | SQLite3 (file off) | SQLite3 (mem) | TObjectList (static) | TObjectList (virt.) | SQLite3 (ext file full) | SQLite3 (ext file off) | SQLite3 (ext mem) | Oracle | Jet | |
|---|---|---|---|---|---|---|---|---|---|---|
| By one | 10461 | 10549 | 44737 | 103577 | 103553 | 43367 | 44099 | 45220 | 901 | 1074 |
| By name | 9694 | 9651 | 32350 | 70534 | 60153 | 22785 | 22240 | 23055 | 889 | 1071 |
| All Virt. | 167095 | 162956 | 168651 | 253292 | 118203 | 97083 | 90592 | 94688 | 56639 | 52764 |
| All Direct | 167123 | 144250 | 168577 | 254284 | 256383 | 170794 | 165601 | 168856 | 88342 | 75999 |
上表的结果是在一台酷睿2双核笔记本电脑上运行的,因此数据比前表要低。
在测试期间,禁用了内部缓存,所以当数据的读比写多时,你可以在实际应用中进一步提升速度,如当从缓存中检索一个对象时,你每秒能处理超过1,00,000次读请求,无论是否使用数据库。
当声明为虚表时(通过VirtualTableRegister 调用),您就拥有了SQL(包括JOIN)的全部功能,具有非常快的CRUD操作:每秒处理100,000个对象读写请求,包括序列化和客户端-服务端通信!
一些数据库是mORMot的首选,比如SQLite3、Oracle、MS SQL、PostgreSQL、MySQL或IBM DB2。您可以连接到它们,而没有DB.pas单元导致的瓶颈,也没有任何Delphi许可限制(初学者版就足够了)。
首先,需要考虑使用SQLite3,即使对于生产服务器。由于mORMot的架构设计,这个“嵌入式”数据库在有大量的并发访问时可以作为客户端-服务端应用程序的主数据库引擎,如果您对其伸缩性有疑问,请参见线程安全。在这里,“嵌入式”也可用于“移动”环境,这是一个自包含、零配置、经过验证的引擎。
通过HTTP的远程访问取得了非常好的结果,在这个本地基准测试中,普通socket客户端(即TSQLDBSocketConnectionProperties类)取得了比WinHTTP API(在客户端使用TSQLDBWinHTTPConnectionProperties)更好的结果。但是在实际使用中,如在Internet上,WinHTTP API被认为更稳定,因此在生产中可能是优选。SQlite3后端为HTTP提供了相当好的性能,并有使用标准HTTP进行传输的好处。
大多数知名的闭源数据库是可用的:
开源数据库也是值得考虑的,尤其是与mORMot这样的开源框架一起使用时:
要访问这些数据库,还可以使用OleDB、ODBC或ZDBC驱动程序,这些驱动程序具有直接访问权限。mORMot是一种非常开放的杂食动物:你可以使用任何DB.pas驱动程序,例如FireDAC、UniDAC、DBExpress、NexusDB甚至BDE,但是在读取时TDataSet实例引入了其它层。
因此,典型使用场景可能是:
| 数据库 | 使用场景 |
|---|---|
| internal SQLite3 file | 在默认情况下创建的。 通用安全数据处理,在“off exc”模式下速度惊人 |
| internal SQLite3 in-memory | 使用':memory:'文件名创建。快速无持久性数据处理(如测试或临时存储) |
TObjectList static |
使用StaticDataCreate创建。没有ACID和SQL,为少量数据提供最佳性能 |
TObjectList virtual |
使用VirtualTableRegister创建。如果不需要ACID,也不需要复杂的SQL,那么对于少量数据(在Win64无数据数量限制)的SQL,可以获得最佳的性能 |
| external SQLite3 file | 使用VirtualTableExternalRegister创建外部后端,如跨磁盘 |
| external SQLite3 in-memory | 使用VirtualTableExternalRegister和':memory:'创建外部快速后端(例如用于测试) |
| external Oracle / MS SQL / DB2 / PostgreSQL / MySQL / Informix / Firebird | 使用VirtualTableExternalRegister创建快速、安全、符合行业标准的后端;数据可以脱离mORMot共享 |
| external NexusDB | 使用VirtualTableExternalRegister创建免费的嵌入式版本,允许将整个引擎包含在可执行文件中,并使用任何现有代码,但是SQlite3听起来是更好的选择 |
| external Jet/MSAccess | 使用VirtualTableExternalRegister创建可以用作数据交换格式(如与Office应用) |
| external Zeos | 使用VirtualTableExternalRegister创建允许访问几个外部引擎,使用直接的Zeos/ZDBC访问,绕过了 DB.pas单元及其TDataSet瓶颈,我们也更喜欢这个活跃的开源项目! |
| external FireDAC/UniDAC | 使用VirtualTableExternalRegister创建允许访问多个外部引擎,包括 DB.pas单元及其TDataSet瓶颈 |
| external MongoDB | 使用StaticMongoDBRegister()创建高速基于文档的存储,具有水平伸缩和嵌套子文档的高级查询能力 |
无论使用哪种数据库后端,不要忘记mORMot设计将允许您从一个库切换到另一个库,只需更改TSQLDBConnectionProperties类型即可。您可以混合使用外部引擎,不需要绑定到单个引擎,但是需要根据项目为每个ORM表优化数据库访问。
从框架的1.15版本开始,SQLite3引擎从mORMotSQLite3.pas单元剥离,独立定义为SynSQLite3.pas单元。
可以这样来使用它:
我们将在这里明确一些使用SQLite3引擎的重点内容,并参考http://sqlite.org上这个伟大的开源项目的官方文档,以了解其常用特性的基本信息。
框架从1.18版,SynSQlite3.pas单元可以通过两种方式调用SQLite3引擎:
SQLite3 API和常量在SynSQlite3.pas中定义,可以通过定义TSQLite3Library类来访问。如下定义了一个全局sqlite3变量:
var
sqlite3: TSQLite3Library;
要使用SQLite3引擎,需要将TSQLite3Library的类实例分配给这个全局变量,然后mORMot的所有调用都将通过它进行,例如调用sqlite3.open()而不是sqlite3_open()。
它有两个实现类:
| 类 | 单元 | 用途 |
|---|---|---|
| TSQLite3LibraryStatic | SynSQLite3Static.pas | 静态链接的引擎(.obj链接到.exe) |
| TSQLite3LibraryDynamic | SynSQLite3.pas | 实例化一个sqlite3.dll外部实例 |
通过use引用SynSQLite3Static.pas即可将.obj引擎链接到可执行文件中。
规则变化:在框架1.18版本前,链接静态.obj是必须的,所以你必须在你的项目中添加一个对SynSQLite3Static的引用来保证正常工作。
为了使用外部sqlite3.dll动态连接库,您必须设置全局sqlite3变量:
FreeAndNil(sqlite3); // release any previous instance (e.g. static)
sqlite3 := TSQLite3LibraryDynamic.Create;
FreeAndNil(sqlite3)不是必需的,只有在分配了另一个sqlite3引擎实例时(如果在项目单元的某个地方引用了SynSQLite3Static,就可能会出现这种情况)才需要使用FreeAndNil(sqlite3)。
下面是一些用Delphi XE3编译的基准测试,运行在32位项目中,使用bcc编译的静态引擎或MinGW、Visual c++编译的sqlite3.dll外部动态连接库。
为了减少在SQLite3引擎中的时间消耗(对于高端服务器可能有用),框架能够在本地处理SQL语句缓存。
从框架1.12版本开始,我们在数据库访问中添加了一个内部SQL语句缓存,可用于所有SQL请求。以前,只缓存了一条SELECT * FROM ... WHERE RowID=…SQL语句(如用于TSQLRest. Retrieve方法)。
这样,如果前面的SQL语句运行时带有一些给定的参数,那么就会使用缓存中现成可用的这个版本,并在SQLite3执行之前重新绑定为新参数。
在某些情况下,这样做可以极大加快SQLite3进程。根据我们的分析,语句缓存在内存数据库(':memory:'指定为文件名)中至少比一般的请求快两倍(如查询/插入/更新某一行)。
为了使用语句缓存,任何SQL语句都必须有一个参数并使用':('和'):'围起来,通过在SQL请求中添加可选标记参数的方法来增强SQL格式,以加强语句的准备和缓存。
因此,现在有两种编写同样的SQL请求的方式:
像以前是这样编写SQL语句:
SELECT * FROM TABLE WHERE ID=10;
在这种情况下,SQL语句将由SQLite3引擎解析,然后编译一条执行一条。
使用新的标记选项来绑定并替换参数:
SELECT * FROM TABLE WHERE ID=:(10):;
在这种情况下,任何已经就绪的匹配的缓存语句都将直接被重新用来执行。
在后面的示例中,将维护一个就绪的TSQLRequest语句内部缓存池。我们通常这样在SQL代码中进行参数匹配:
SELECT * FROM TABLE WHERE ID=?;
在执行之前,整数10将被绑定到已就绪的缓存语句。
可采用如下方式的内联值(注意文本参数允许双引号,除此以外SQL语句应该只使用单引号):
:(1234): :(12.34): :(12E-34): :("text"): :('It''s great'):
ORM内部生成的所有SQL语句现在都使用这个新的参数语法。
例如,下面是SQlite3引擎如何实现对象删除:
function TSQLRestServerDB.EngineDelete(Table: TSQLRecordClass; ID: TID): boolean;
begin
if Assigned(OnUpdateEvent) then
OnUpdateEvent(self,seDelete,Table,ID); // notify BEFORE deletion
result := ExecuteFmt('DELETE FROM % WHERE RowID=:(%):;',[Table.SQLTableName,ID]);
end;
使用:(%):将使DELETE FROM table_name WHERE RowID=?语句在调用时完成缓存和重用准备。
在您的代码中,这样来用更方便,例如:
aName := OneFieldValue(TSQLMyRecord,'Name','ID=:(%):',[aID]);
更简单的方式:
aName := OneFieldValue(TSQLMyRecord,'Name','ID=?',[],[aID]);
而不是:
aName := OneFieldValue(TSQLMyRecord,'Name','ID=%',[aID]);
也不是:
aName := OneFieldValue(TSQLMyRecord,'Name','ID='+Int32ToUtf8(aID));
实际上,从您的客户端代码中,您可能不会直接在请求中使用:(…):表达式,而是使用重载的TSQLRecord.Create, TSQLRecord.FillPrepare, TSQLRecord.CreateAndFillPrepare, TSQLRest.OneFieldValue, TSQLRest.MultiFieldValues, TQLRestClient.ExecuteFmt和TSQLRestClient.ListFmt方法,从框架的1.15版本开始,SQL的WHERE格式化文本可以接受'%'和'?'内联参数。
我们发现在Delphi代码中使用这种SQL增强格式要比通过名称或索引使用参数容易得多(而且更快),就像这个VCL的典型代码那样:
SQL.Text := 'SELECT Name FROM Table WHERE ID=:Index';
SQL.ParamByName('Index').AsInteger := aID;
在底层,语句中带边界的内联值可以在C/S体系结构中实现更好的序列化,服务器端缓存更容易,整个SQL查询包含的所有参数都是RawUTF8值,因此可以直接与缓存的条目进行比较。这样,我们的框架能够处理已就绪的语句,而不需要将绑定参数与主SQL文本分离。
同样,外部数据库也将受益于语句缓存。内联值将单独绑定到外部SQL语句,以达到最佳的速度。
2010-06-25源代码存储库更新后,RTREE扩展已默认编译到所提供的.obj文件中。
R-Tree是用于执行空间范围查询的特殊索引。R-Tree最常用在地理空间系统中,其中每个条目都是具有最小和最大XY坐标的矩形。给定一个查询矩形,R-Tree能够快速找到包含在查询矩形内或与查询矩形重叠的所有条目。该思路很容易推广到三维CAD系统中使用。R-Tree也可以用于时域范围查找。例如,假设数据库记录了大量事件的开始和结束时间,R-Tree能够快速查找所有事件,例如,在给定时间范围内始终处于活动状态的事件,或在特定时间范围内开始的所有事件,或在给定时间范围内包含开始和结束的所有事件,等等。参见http://www.sqlite.org/rtree.html。
可以使用专用的TSQLRecordRTree ORM类来创建这样的表,它继承自TSQLRecordVirtual,就像其他虚拟表类型(例如TSQLRecordFTS5)一样。
从这个TSQLRecordRTree类继承的所有记录都只有sftFloat(如Delphi的double)发布属性,最多5维度,每个维度都包含最小值和最大值(共11列,包括ID属性)。在向数据库添加TSQLRecordRTree记录之前,必须设置其ID: TID属性,如将R-Tree表链接到包含主数据的TSQLRecord表。
对ID或坐标范围的查询几乎是实时的,因此,您可以从主TSQLRecord表中提取一些坐标,然后使用TSQLRecordRTree连接查询来加快处理速度,TSQLRestClient.RTreeMatch就是采取的这中方法,例如,以下代码用[-81,-79.6,35,36.2]对aMapData.BlobField赋值后运行:
aClient.RTreeMatch(TSQLRecordMapData,'BlobField',TSQLRecordMapBox,
aMapData.BlobField,ResultID);
将执行以下SQL语句:
SELECT MapData.ID From MapData, MapBox WHERE MapData.ID=MapBox.ID
AND minX>=:(-81.0): AND maxX<=:(-79.6): AND minY>=:(35.0): AND :(maxY<=36.2):
AND MapBox_in(MapData.BlobField,:('\uFFF0base64encoded-81,-79.6,35,36.2'):);
MapBox_in SQL功能通过TSQLRestServerDB.Create构造函数注册,并为当前数据库模型的所有TSQLRecordRTree类创建。BlobToCoord和ContainedIn类方法都用于处理BLOB中存储的范围值。默认情况下,它将处理一个double的原始数组,并使用默认的方式进行范围匹配 (ContainedIn方法将简单地按minX>=…maxY<=…where子句进行匹配)。
FTS3/FTS4/FTS5是SQLite3虚拟表模块,允许用户对一组文档执行全文搜索。全文搜索的概念最好也最有效的描述是“谷歌、雅虎和Altavista如何处理放在互联网上的文档”。用户输入一个术语或一系列术语,这些术语可能由二进制操作符连接,或者组成一个短语,全文查询系统会找到一组文档,这些文档既满足用户指定的操作符和分组要求,也最匹配这些术语。
有关FTS3/FTS4在SQLite3中使用的参考资料见http://www.sqlite.org/fts3.html,有关FTS5的参考资料见https://www.sqlite.org/fts5.html。简而言之,FTS5是FTS4的一个新版本,它包含了在不牺牲向下兼容性的情况下无法在FTS4中修复的各种问题的修复和解决方案。
框架的最新版本,sqlite3.obj静态文件已包含FTS3/FTS4/FTS5引擎(也适用于FPC的其他平台)。
为了便于使用FTS特性,我们定义了一些类型:
译者注:stem-词干,stemming-提取词干,在英语中,一个单词常常是另一个单词的“变种”,如:happy=>happiness,这里happy叫做happiness的词干(stem)。在信息检索系统中,我们常常做的一件事,就是在Term规范化过程中,提取词干,即除去英文单词分词变换形式的结尾。
下面的图将详细描述这个类层次结构:
实际上,在处理拉丁内容时,您应该使用TSQLRecordFTS5,使用TSQLRecordFTS5Unicode61对非拉丁支持更好,如果您的内容是纯英语文本,则应该使用TSQLRecordFTS5Porter。
“词干提取”算法(参见http://sqlite.org/fts3.html#tokenizer)是对英文文本进行解析以便从原始文本创建单词索引的方法。
默认的简单分词器提取符号特征或使用基本的FTS全文检索时会遵循以下规则:
例如,当一个文档包含“Right now, they're very frustrated.”的文本时。从文档中提取并添加到全文索引中的术语,按照顺序是“right now they re very frustrated”。这样的文档将匹配全文查询,如"MATCH 'Frustrated'",因为在检索全文索引之前,简单的分词器将查询中的术语转换成了小写。
波特词干提取算法分词器使用相同的规则将输入文档分隔成术语,同时将所有术语转换成小写,并使用波特词干提取算法将相关英语单词减少到只剩公共根。例如,使用与上文相同的输入文档,波特分词器提取了以下标记:“right now thei veri frustrat”。尽管这些术语中有些甚至不是英语单词,但在某些情况下,使用它们构建全文索引比使用简单的分词器生成更容易理解的输出更有用。使用波特分词器,文档不仅匹配全文查询(如"MATCH 'Frustrated'"),还匹配"MATCH 'Frustration'",因为“Frustration”一词通过波特词干提取算法减为“frustrat”,就像“Frustrated”一样。因此,在使用波特分词器时,FTS能够找到的不仅是查询词的精确匹配,还能与类似的英语词汇匹配。有关波特词干提取算法的更多信息,请参阅http://tartarus.org/~martin/PorterStemmer。
Unicode61的词干提取算法分词器的工作原理与“简单”非常类似,只是它根据unicode 6.1版本中的规则进行简单的unicode大小写处理,并识别unicode空间和标点字符,并使用这些字符分隔特征符号。默认情况下,“Unicode61”还从拉丁字符中删除所有的变音符号。
一个好的方法是将数据存储在普通的TSQLRecord表中,然后将文本内容存储在分离的FTS表中,通过ID / DocID属性将其关联到这个TSQLRecordFTS5表中。对于TSQLRecordFTS*类型,ID属性被重命名为DocID,这是FTS虚拟表定义的唯一整数键ID属性的内部名称。
例如(从回归测试代码中提取),您可以定义这个新类:
TSQLFTSTest = class(TSQLRecordFTS5)
private
fSubject: RawUTF8;
fBody: RawUTF8;
published
property Subject: RawUTF8 read fSubject write fSubject;
property Body: RawUTF8 read fBody write fBody;
end;
注意,FTS表仅支持UTF-8文本字段,即RawUTF8(在Delphi 2009及以上版本,您还可以使用Unicode字符串类型,它会被SQLite3引擎映射为UTF-8文本字段)。
然后,您可以通过框架的ORM特性向这个FTS表添加一些Body/Subject内容,就像任何普通TSQLRecord内容一样:
FTS := TSQLFTSTest.Create;
try
Check(aClient.TransactionBegin(TSQLFTSTest)); // MUCH faster with this
for i := StartID to StartID+COUNT-1 do
begin
FTS.DocID := IntArray[i];
FTS.Subject := aClient.OneFieldValue(TSQLRecordPeople,'FirstName',FTS.DocID);
FTS.Body := FTS.Subject+' bodY'+IntToStr(FTS.DocID);
aClient.Add(FTS,true);
end;
aClient.Commit; // Commit must be BEFORE OptimizeFTS3, memory leak otherwise
Check(FTS.OptimizeFTS3Index(Client.fServer));
上面的步骤是典型的,与“标准”ORM方法的唯一区别是,在添加TSQLRecordFTS5实例之前必须设置DocID属性,SQLite没有自动创建ID,必须手工指定ID,以便通过ID将FTS记录链接到TSQLRecordPeople行。
为了支持全文查询,FTS维护了一个反向索引,将数据中出现的每个唯一的术语或单词映射到表内容中出现的位置。调用专用的OptimizeFTS3Index方法将所有现有的b-trees索引合并到一个大型包含整个b-trees索引中,该方法可与FTS3、FTS4和FTS5类一起工作,不管它的名字是什么。这可能是一个昂贵的操作,但能加快将来的查询,不要在每次修改FTS表之后就调用这个方法,而是在添加了一些文本之后。
然后FTS搜索查询将使用自定义的FTSMatch方法:
Check(aClient.FTSMatch(TSQLFTSTest,'Subject MATCH ''salVador1''',IntResult));
匹配的ID存储在IntResult动态整数数组中,您也可以使用通常的SQL查询代替,FTSMatch方法并不是强制使用的,实际上,它只是一个OneFieldValues方法的包装器,只是用RowID列名来返回结果:
function TSQLRest.FTSMatch(Table: TSQLRecordFTS3Class;
const WhereClause: RawUTF8; var DocID: TIntegerDynArray): boolean;
begin // FTS3 tables do not have any ID, but RowID or DocID
result := OneFieldValues(Table,'RowID',WhereClause,DocID);
end;
上面的代码定义了一个重载的FTSMatch方法,并将处理详细的匹配信息,能够使用排序算法,排序结果将按相关性排序:
Check(aClient.FTSMatch(TSQLFTSTest,'body1*',IntResult,[1,0.5]));
这个方法需要一些额外的常量参数来对每个FTS表列进行加权(必须有与TSQLRecordFTS5表中的列相同数量的PerFieldWeight参数)。在上面的示例代码中,Subject字段的权重为1.0,Body的权重为0.5,即“Body”列内容中的所有匹配都要比“Subject”列中的匹配的权重小两倍,这可能是精度最高的匹配。
以上查询将调用以下SQL语句:
SELECT RowID FROM FTSTest WHERE FTSTest MATCH 'body1*'
ORDER BY rank(matchinfo(FTSTest),1.0,0.5) DESC
在Delphi中已经实现了rank内部SQL函数,依照SQLite3官方文档指南,可以在其Internet网站http://www.sqlite.org/fts3.html#appendix_a中找到,以实现最有效的排名方式。它将返回匹配全文查询的RowID的文档,该查询从最相关到最不相关排序。在计算相关性时,“subject”列中的查询术语权重是“body”列的两倍。
正如SQlite3所允许的,框架允许FTS4放弃存储被索引的文本,让被索引的文档存储在由用户创建和管理的数据库表中(一个“外部内容”的FTS4表)。
由于索引文档本身通常比全文索引大得多,因此可以使用此选项来节省大量存储空间。无内容的FTS4表仍然支持SELECT语句。但是,尝试检索docid列以外的任何表列的值都是错误的。可以使用matchinfo()辅助函数,TSQLRest.FTSMatch方法将按预期工作,但是snippet()和offsets()将在执行时导致异常。
例如在示例“30 - MVC Server”中,我们定义了这两个表:
TSQLArticle = class(TSQLContent)
private
fContent: RawUTF8;
fTitle: RawUTF8;
fAbstract: RawUTF8;
fPublishedMonth: Integer;
fTags: TIntegerDynArray;
published
property Title: RawUTF8 index 80 read fTitle write fTitle;
property Content: RawUTF8 read fContent write fContent;
property PublishedMonth: Integer read fPublishedMonth write fPublishedMonth;
property Abstract: RawUTF8 index 1024 read fAbstract write fAbstract;
property Tags: TIntegerDynArray index 1 read fTags write fTags;
end;
TSQLArticleSearch = class(TSQLRecordFTS4Porter)
private
fContent: RawUTF8;
fTitle: RawUTF8;
fAbstract: RawUTF8;
published
property Title: RawUTF8 read fTitle write fTitle;
property Abstract: RawUTF8 read fAbstract write fAbstract;
property Content: RawUTF8 read fContent write fContent;
end;
我们对数据库模型进行了初始化,使所有数据仅存储在TSQLArticle中,而不存储在TSQLArticleSearch中,使用“外部内容”FTS4表对TSQLArticle所选Title,Abstract和Content字段中的文本进行索引:
function CreateModel: TSQLModel;
begin
result := TSQLModel.Create([TSQLBlogInfo,TSQLAuthor,
TSQLTag,TSQLArticle,TSQLComment,TSQLArticleSearch],'blog');
result.Props[TSQLArticleSearch].FTS4WithoutContent(TSQLArticle);
...
TSQLModelRecordProperties.FTS4WithoutContent()将实际创建所需的SQLite3触发器,以便在主文章行更改时自动填充ArticleSearch全文索引。
由于FTS4特性是特定于SQlite3的,并且触发器目前还不能在虚拟表上工作,因此如果TSQLArticleSearch或TSQLArticle位于外部数据库上,那么这个方法就什么也做不了,两个表都需要存储在SQLite3 DB中。
在30 - MVC服务器示例中,搜索将这样执行:
if scop^.GetAsRawUTF8('match',match) and fHasFTS then begin
if scop^.GetAsDouble('lastrank',rank) then
whereClause := 'and rank<? ';
whereClause := 'join (select docid,rank(matchinfo(ArticleSearch),1.0,0.7,0.5) as rank '+
'from ArticleSearch where ArticleSearch match ? '+whereClause+
'order by rank desc limit 100) as r on (r.docid=Article.id)';
articles := RestModel.RetrieveDocVariantArray(
TSQLArticle,'',whereClause,[match,rank],
'id,title,tags,author,authorname,createdat,abstract,contenthtml,rank');
在上面的查询表达式中,rank()函数用于统计matchinfo()返回的FTS4详细搜索数据,对于标题列中的所有匹配使用1.0权值,抽象列使用0.7权值,内容使用0.5权值。然后,匹配的文章内容通过articles: TDocVariant数组返回,准备渲染到web页面。
在任何数据库中,都需要定义如何比较列数据,用于正确搜索和排序数据。这就是所谓排序规则。
默认情况下,当SQLite比较两个字符串时,它使用顺序排序或排序函数(两个单词表示相同的东西)来确定哪个字符串更大,或者两个字符串是否相等。SQLite有三个内置的排序函数:BINARY、NOCASE和RTRIM:
在mORMot ORM中,我们通过对sqlite3_create_collation() API的内部调用定义了一些额外的排序规则:
| TSQLFieldType | 默认排序规则 |
|---|---|
| sftAnsiText | NOCASE,不区分大小写 |
| sftUTF8Text | SYSTEMNOCASE,如使用UTF8ILComp(),它将忽略Win-1252拉丁口音 |
| sftEnumerate | BINARY,用于表示那些数值 |
| sftSet | |
| sftInteger | |
| sftID | |
| sftTID | |
| sftRecord | |
| sftBoolean | |
| sftFloat | |
| sftCurrency | |
| ftTimeLog | |
| sftModTime | |
| sftCreateTime | |
| sftDateTime | ISO8601,即在比较前将文本解码为日期/时间值 |
| ftDateTimeMS | |
| sftObject | BINARY,因为它是作为纯JSON内容存储的 |
| sftVariant | |
| sftBlob | BINARY |
| sftBlobDynArray | |
| sftBlobCustom |
您可以通过调用TSQLRecordProperties.SetCustomCollationForAll()来修改默认的排序方案(它将修改给定类型的所有字段排序)或在重载的class procedure InternalRegisterCustomProperties()或InternalDefineModel()中调用SetCustomCollation()方法(它将修改给定字段),可以通用于所有数据库模型、客户端和服务端、对应的TSQLRecord每次使用时。
您也可以调用TSQLModel.SetCustomCollationForAll()方法,它将对给定的模型中执行此操作。
因此,当在mORMot ORM中使用SQLite3时,可以使用以下排序:
| 排序 | 描述 |
|---|---|
| BINARY | 默认memcmp()的比较 |
| NOCASE | 默认的ASCII 7位比较 |
| RTRIM | 默认memcmp()的比较,先删除右侧空格 |
| SYSTEMNOCASE | mORMot的Win-1252 8位比较 |
| ISO8601 | mORMot日期/时间的比较 |
| WIN32CASE | mORMot使用区分大小写的Windows API进行比较 |
| WIN32NOCASE | mORMot使用不区分大小写的Windows API进行比较 |
注意,WIN32CASE/WIN32NOCASE比其他的要慢,但是可以正确地处理任何复杂的脚本。例如,如果您想在数据库级别上使用用于unicode的Windows API,您可以为每个数据库模型设置:
aModel.SetCustomCollationForAll(sftUTF8Text,'WIN32CASE');
aModel.SetCustomCollationForAll(sftDateTime,'NOCASE');
如果使用非默认排序(例如SYSTEMNOCASE/ISO8601/WIN32CASE/WIN32NOCASE),那么使用“普通”SQLite3工具运行请求可能会有问题。但是您可以安全地使用我们的SynDBExplorer,因为它将声明所有上述排序。
当使用外部数据库,如果直接用数据库驱动检索内容虚拟表机制,返回的数据可能不是你希望的,自定义排序需要自定义外部表定义,为每个外部数据库引擎定义适当的SQL语句。
我们的SQLite3引擎可以在SQL查询中使用正则表达式,在标准SQL操作符(= == != <> IS IN LIKE GLOB MATCH)之外启用正则表达式运算符,它将使用开源PCRE库来执行查询。
为了启用运算符,您需要使用use子句引用SynSQLite3RegEx.pas单元,并将RegExp() SQL函数注册到给定的SQLite3数据库实例中,如下所示:
uses SynCommons, mORmot, mORMotSQLite3,
SynSQLite3RegEx;
...
Server := TSQLRestServerDB.Create(Model,'test.db3');
try
CreateRegExpFunction(Server.DB.DB);
with TSQLRecordPeople.CreateAndFillPrepare(Client,
'FirstName REGEXP ?',['\bFinley\b']) do
try
while FillOne do begin
Check(LastName='Morse');
Check(IdemPChar(pointer(FirstName),'SAMUEL FINLEY '));
end;
finally
Free;
end;
finally
Server.Free;
end;
以上代码将执行以下SQL语句(为正则表达式自身准备一个参数):
SELECT * from People WHERE Firstname REGEXP '\bFinley\b';
也就是说,它将找到TSQLRecordPeople.FirstName包含“Finley”单词的所有对象,在正则表达式中,\b定义了一个单词边界搜索。
实际上,REGEXP运算符是REGEXP()用户函数的特殊语法。默认情况下不定义regexp()用户函数,因此使用regexp操作符通常会导致错误消息。为给定的连接调用CreateRegExFunction()将在运行时添加一个名为“regexp()”的SQL函数,该函数将被调用以实现regexp操作符。
PCRE静态链接库在Delphi XE之后可用,旧版本的Delphi使用PCRE.pas包装单元,发布在http://www.regular-expressions.info/download/TPerlRegEx.zip中。
这个单元将直接调用PCRE库的UTF-8 API,并维护已编译正则表达式的每个连接缓存,以确保最佳性能。
如上面的数据访问基准测试所述,在普通硬盘上运行时,默认的SQlite3写入速度相当慢。默认情况下,引擎在发出OS级的写命令后会暂停。这保证了数据被写入磁盘,并具有数据库引擎的ACID属性。
ACID是“Atomicity Consistency Isolation Durability”属性的缩写,它保证数据库事务被可靠地处理。例如,在电源丢失或硬件故障的情况下,数据能以一致的方式保存在磁盘上,而不会有潜在的数据丢失。
在SQLite3中,ACID是通过两种方法在文件级实现的:
更改这些默认设置可以提升写入性能。
可以通过设置TSQLDataBase.Synchronous修改默认的ACID行为,设置为smOff,而不是默认的smFull。当将Synchronous属性设置为smOff时,SQLite在将数据传递给操作系统之后会继续运行,而不会等待同步。这样,如果运行SQLite的应用程序崩溃,数据将是安全的,但是如果操作系统崩溃,或者在数据写入磁盘表面之前计算机失去动力,数据库可能会损坏。另一方面,在此设置下,有些操作要快50倍甚至更多。
当在数据访问基准测试期间执行的测试使用Synchronous:= smOff时,在物理硬盘上(SSD或NAS驱动器可能不会受到这种延迟的影响),“写入”的速度会从每秒8-9行提高到大约每秒400行。
因此,根据您的应用程序需求,您可以将同步设置切换为off。
要更改主要SQLite3引擎同步参数,您可以编写以下代码:
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
Client.Server.DB.Synchronous := smOff;
注意,这个设置对于整个TSQLDatabase实例来说是通用的,因此会影响TSQLRestServerDB实例对所有表的处理。
但是如果您定义了一些SQLite3外部表,如下所示,您可以为特定的外部连接定义设置,例如:
Props := TSQLDBSQLite3ConnectionProperties.Create(DBFileName,'''','');
VirtualTableExternalRegister(Model,TSQLRecordSample,Props,'SampleRecord');
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
TSQLDBSQLite3Connection(Props.MainConnection).Synchronous := smOff;
不要忘记在一个mORMot服务中可以有几个SQlite3引擎!
您可以修改ACID默认行为,通过设置TSQLDataBase.LockingMode属性为lmExclusive,而不是默认的lmNormal。当锁定模式设置为lmExclusive时,SQLite会在整个会话期间锁定数据库文件供独占使用。它将阻止其他进程(例如数据库查看器工具)同时访问文件,这样小的写事务要快很多,通常大于40倍。涉及几百/几千条写入操作的更大的事务则不会被加速,主要是对单条插入会有加速,参见数据访问基准。
要更改SQLite3引擎锁定模式参数,您可以编写以下代码:
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
Client.Server.DB.LockingMode := lmExclusive;
注意,这个设置对于整个TSQLDatabase实例来说是通用的,因此会影响TSQLRestServerDB实例处理的所有表。
但是如果您定义了一些SQLite3外部表,如下所示,您可以为特定的外部连接修改设置,例如:
Props := TSQLDBSQLite3ConnectionProperties.Create(DBFileName,'''','');
VirtualTableExternalRegister(Model,TSQLRecordSample,Props,'SampleRecord');
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
TSQLDBSQLite3Connection(Props.MainConnection).LockingMode := lmExclusive;
实际上,排它性的文件锁定将读取速度提高了4倍(在单独的行检索情况下)。因此,定义LockingMode := lmExclusive,而不使用Synchronous:= smOff,对于主要为客户机提供ORM内容的服务器来说会很有效。
默认情况下,缓慢但真正的ACID设置将用于mORMot,就像SQlite3一样。我们不会改变这一策略,因为它将确保最好的安全,代价是在交易之外写入慢。
最好的性能将通过结合前面两个选项来实现:
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
Client.Server.DB.LockingMode := lmExclusive;
Client.Server.DB.Synchronous := smOff;
或者,对于外部表:
Props := TSQLDBSQLite3ConnectionProperties.Create(DBFileName,'''','');
VirtualTableExternalRegister(Model,TSQLRecordSample,Props,'SampleRecord');
Client := TSQLRestClientDB.Create(Model,nil,MainDBFileName,TSQLRestServerDB,false,'');
TSQLDBSQLite3Connection(Props.MainConnection).Synchronous := smOff;
TSQLDBSQLite3Connection(Props.MainConnection).LockingMode := lmExclusive;
如果你能承受在一些极端条件下损失一些数据的可能,或者如果你确定你的硬件配置是安全的(例如,如果服务器是连接到逆变器和RAID磁盘),你手头有数据备份,设置Synchronous := smOff将提升应用程序写入性能。设置LockingMode := lmExclusive对读写速度都有好处。如果您的安全要求非常高,并且默认的安全但缓慢的设置对您来说也不满足要求,那么可以考虑使用外部和专用数据库(如Firebird、Oracle、PostgreSQL、MySQL、DB2、Informix或MS SQL)。
在所有情况下,不要忘记尽可能频繁地执行SQlite3数据库备份(至少一天多次)。在服务器端添加备份功能就像跑步一样简单:
Server.DB.BackupBackground('backup.db3',1024,10,nil);
上面的一行代码将对SQLite3主数据库执行后台实时备份,备份周期为每1024内存页备份一次(即它将每次备份1 MB,因为默认的内存页大小是1024字节),每拷贝1MB休眠10毫秒,这样主CRUD / ORM操作在备份期间才能保证持续不间断工作。
您甚至可以指定OnProgress: TSQLDatabaseBackupEvent回调事件,以监视备份过程。
注意在运行的mORMot数据库中TSQLRestServerDB.Backup或TSQLRestServerDB.BackupGZ方法不再被推荐,因为虚拟表存在一些潜在问题,尤其是在Win64平台上。您应该明确地使用TSQLDatabase.BackupBackground()。
可以使用相同的备份过程,例如,将内存中的SQLite3数据库保存到SQLite3文件中,例如:
if aInMemoryDB.BackupBackground('backup.db3',-1,0,nil) then
aInMemoryDB.BackupBackgroundWaitUntilFinished;
以上代码将把aInMemoryDB数据库保存到“backup.db3”文件。
SQlite3引擎具有从代码创建虚拟表的独特能力。从SQL语句的角度来看,虚拟表对象与其他表或视图类似。但是在幕后,对虚拟表的查询和更新调用虚拟表对象上的回调方法,而不是读写数据库文件。
虚拟表机制允许应用程序发布从SQL语句访问的接口,就好像它们是表一样。SQL语句通常可以对虚拟表做任何它们可以对真实表做的事情,但有以下例外:
不能在虚拟表上创建触发器。
不能在虚拟表上创建额外的索引(虚拟表可以有索引,但必须构建到虚拟表实现中。不能使用
CREATE INDEX语句单独添加索引。
不能对虚拟表执行 ALTER TABLE ... ADD COLUMN 命令。
特定的虚拟表实现可能会带来额外的约束。例如,一些虚拟实现可能提供只读表。或者一些虚拟表实现可能允许插入或删除,但不允许更新。或者一些虚拟表实现可能会限制可以进行的更新类型。
已经包含在SQLite3引擎中的虚拟表的例子是FTS或RTREE表。
我们的框架引入了新的定制虚拟表类型。您将发现TSQLVirtualTableJSON或TSQLVirtualTableBinary类,它们处理内存中的数据结构,或者它可能表示不是SQLite3格式(例如TSQLVirtualTableLog)的磁盘数据视图。它可以用于访问任何外部数据库,就像它们是本地SQLite3表一样,参见下面。或者应用程序可以根据需要计算虚拟表的内容。
得益于SQLite3中的虚拟表的通用实现,您可以在SQL语句中使用这些表,甚至可以安全地在SELECT语句中使用JOIN或定制函数,混合普通的SQLite3表和任何其他虚拟表。从ORM的观点来看,虚拟表只是表,即它们继承自TSQLRecordVirtual,而TSQLRecordVirtual继承自公共基类TSQLRecord。
从版本1.13开始,向框架添加了专用机制,以便使用纯Delphi代码轻松地添加这样的虚拟表。
为了实现新的虚拟表类型,您必须定义一个所谓的模块来处理字段和数据访问,并为SELECT语句定义一个关联的游标。这是由mORMot.pas单元中定义的TSQLVirtualTable和TSQLVirtualTableCursor两个类实现的。
例如,以下是从这些类派生出来的默认虚拟表类:
TSQLVirtualTableJSON、TSQLVirtualTableBinary和TSQLVirtualTableCursorJSON类将使用TSQLRestStorageInMemory实例实现虚拟表,以处理内存中的快速静态数据库。磁盘存储将被编码为UTF-8 JSON(用于TSQLVirtualTableJSON类,即“JSON”模块),或以专用的SynLZ压缩格式(用于TSQLVirtualTableBinary类,即“二进制”模块)。磁盘上的文件扩展名简单地为. JSON代表“JSON”模块,.data代表“二进制”模块。需要提一下磁盘存储的大小差异,502kb的People.json内容(由包含的回归测试创建)需92 KB的People.data存储文件,这是特有的格式优化。
注意,虚拟表模块名是根据类名定义的。例如,TSQLVirtualTableJSON类在SQL代码中将其模块命名为“JSON”。
为了处理外部数据库,将以类似的方式定义两个专用类TSQLVirtualTableExternal和TSQLVirtualTableCursorExternal,请参阅下面的外部数据库类层次结构。
您可能已经知道,所有这些虚拟表机制都是在mORMot.pas中实现的。因此,它独立于SQLite3引擎,据我所知,没有其他SQL数据库引擎实现了这一优秀特性。
下面是如何定义TSQLVirtualTableLog类的,它将实现一个名为“Log”的虚拟表模块。添加一个新的模块只需要重载一些Delphi方法:
TSQLVirtualTableLog = class(TSQLVirtualTable)
protected
fLogFile: TSynLogFile;
public
class procedure GetTableModuleProperties(
var aProperties: TVirtualTableModuleProperties); override;
constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8;
FieldCount: integer; Fields: PPUTF8CharArray); override;
destructor Destroy; override;
end;
这个模块将允许对.log文件内容的直接只读访问,该文件的文件名将由相应的SQL表名指定。
下面的方法将定义这个虚拟表模块的属性:
class procedure TSQLVirtualTableLog.GetTableModuleProperties(
var aProperties: TVirtualTableModuleProperties);
begin
aProperties.Features := [vtWhereIDPrepared];
aProperties.CursorClass := TSQLVirtualTableCursorLog;
aProperties.RecordClass := TSQLRecordLogFile;
end;
提供的特性集定义了一个只读模块(因为没有选择vtWrite),vtWhereIDPrepared指示任何RowID=?SQL语句在游标类中这样处理(我们将使用日志行作为ID号,从1开始计数,这样我们可以加速RowID=?WHERE子句)。返回关联的游标类,指定一个TSQLRecord类来定义处理的字段,其发布的属性定义将被构造方法继承,用于向SQLite3引擎指定SQL语句中预期的字段类型:
TSQLRecordLogFile = class(TSQLRecordVirtualTableAutoID)
protected
fContent: RawUTF8;
fDateTime: TDateTime;
fLevel: TSynLogInfo;
published
/// the log event time stamp
property DateTime: TDateTime read fDateTime;
/// the log event level
property Level: TSynLogInfo read fLevel;
/// the textual message associated to the log event
property Content: RawUTF8 read fContent;
end;
您可以重载构造方法,以实现CREATE TABLE SQL语句。但是,使用Delphi类RTTI就可以构建需要的列类型和排序规则的SQL语句,ORM其它部分也需要这样的实现。
当然,这个RecordClass属性不是必须的。例如,TSQLVirtualTableJSON.GetTableModuleProperties方法不会返回任何关联的TSQLRecordClass,因为它将依赖于它所实现的表,即正在运行的TSQLRestStorageInMemory实例。相反,将重载构造方法,并返回每个关联表的相应字段定义。
下面是如何实现Prepare方法,并处理vtWhereIDPrepared特性:
function TSQLVirtualTable.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean;
begin
result := Self<>nil;
if result then
if (vtWhereIDPrepared in fModule.Features) and
Prepared.IsWhereIDEquals(true) then
with Prepared.Where[0] do begin // check ID=?
Value.VType := varAny; // mark TSQLVirtualTableCursorJSON expects it
OmitCheck := true;
Prepared.EstimatedCost := 1;
end else
Prepared.EstimatedCost := 1E10; // generic high cost
end;
下面是如何创建每个“log”虚拟表模块实例:
constructor TSQLVirtualTableLog.Create(aModule: TSQLVirtualTableModule;
const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray);
var aFileName: TFileName;
begin
inherited;
if (FieldCount=1) then
aFileName := UTF8ToString(Fields[0]) else
aFileName := aModule.FileName(aTableName);
fLogFile := TSynLogFile.Create(aFileName);
end;
它只根据提供的文件名关联TSynLogFile实例(我们的SQL CREATE VIRTUAL TABLE语句只需要一个参数,即磁盘上的.log文件名,如果没有指定这个文件名,它将使用SQL表名)。
TSQLVirtualTableLog.Destroy析构将销毁这个fLogFile实例:
destructor TSQLVirtualTableLog.Destroy;
begin
FreeAndNil(fLogFile);
inherited;
end;
然后对应的游标类定义为:
TSQLVirtualTableCursorLog = class(TSQLVirtualTableCursorIndex)
public
function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override;
function Column(aColumn: integer; var aResult: TVarData): boolean; override;
end;
由于该类继承自TSQLVirtualTableCursorIndex,因此它将具有通用的fCurrent / fMax保护字段,并将具有HasData、Next和Search方法属性用来处理游标导航。
被重载的搜索方法:
function TSQLVirtualTableCursorLog.Search(
const Prepared: TSQLVirtualTablePrepared): boolean;
begin
result := inherited Search(Prepared); // mark EOF by default
if result then begin
fMax := TSQLVirtualTableLog(Table).fLogFile.Count-1;
if Prepared.IsWhereIDEquals(false) then begin
fCurrent := Prepared.Where[0].Value.VInt64-1; // ID=? -> index := ID-1
if cardinal(fCurrent)<=cardinal(fMax) then
fMax := fCurrent else // found one
fMax := fCurrent-1; // out of range ID
end;
end;
end;
这个方法的唯一目的是处理SELECT WHERE语句的RowID=?条件,对于任何有效ID返回fCurrent=fMax=ID-1,或fMax<fCurrent,即如果ID超出范围,则没有返回结果。实际上,游标类的Search方法必须处理调用Prepare方法时通知的所有情况。在我们的例子中,由于我们已经设置了vtwhereidprepare特性,而Prepare方法在请求中标明了该特性并设置了OmitCheck标志,所以我们的Search方法必须处理RowID=?的情况。
如果WHERE子句不是RowID=?(即Prepared.IsWhereIDEquals返回false),它将返回fCurrent=0和fMax=fLogFile.Count-1,并让SQLite3引擎循环遍历所有搜索数据的行。
每个列值都是通过这个方法检索到的:
function TSQLVirtualTableCursorLog.Column(aColumn: integer;
var aResult: TVarData): boolean;
var LogFile: TSynLogFile;
begin
result := false;
if (self=nil) or (fCurrent>fMax) then
exit;
LogFile := TSQLVirtualTableLog(Table).fLogFile;
if LogFile=nil then
exit;
case aColumn of
-1: SetColumn(aResult,fCurrent+1); // ID = index + 1
0: SetColumn(aResult,LogFile.EventDateTime(fCurrent));
1: SetColumn(aResult,ord(LogFile.EventLevel[fCurrent]));
2: SetColumn(aResult,LogFile.LinePointers[fCurrent],LogFile.LineSize(fCurrent));
else exit;
end;
result := true;
end;
正如TSQLVirtualTableCursor类的文档所述,-1是RowID的列索引,后面是代码中定义的列并通过构造方法返回(在我们的例子中,是TSQLRecordLogFile的DateTime、Level和Content字段)。
SetColumn重载方法用于将正确的结果赋给aResult变量。对于UTF-8文本,它将使用一个临时内存空间,以确保至少在下一次Column方法调用之前文本内存仍然可用。
从SQLite3底层的角度来看,下面是如何直接从SQLite3引擎使用这个“日志”虚拟表模块。
首先,我们将这个模块注册到一个DB连(这个方法只在这种低级访问的情况下使用,在我们的ORM中,您永远不应该调用这个方法,而是用TSQLModel. VirtualTableRegister,参看下一段):
RegisterVirtualTableModule(TSQLVirtualTableLog,Demo);
然后,我们可以执行以下SQL语句来创建Demo数据库连接的虚拟表:
Demo.Execute('CREATE VIRTUAL TABLE test USING log(temptest.log);');
这将创建虚拟表,因为TSQLVirtualTableLog类已经知道所有字段,所以没有必要在这里指定字段。我们只指定日志文件名,TSQLVirtualTableLog. Create构造函数将获取该文件名。
Demo.Execute('select count(*) from test',Res);
Check(Res=1);
s := Demo.ExecuteJSON('select * from test');
s2 := Demo.ExecuteJSON('select * from test where rowid=1');
s3 := Demo.ExecuteJSON('select * from test where level=3');
从SQL的角度来看,您可以看到它与普通SQLite3表没有区别。实际上,SQLite3实现的全功能SQL语言(参见http://sqlite.org/lang.html)可以与任何类型的数据一起使用,只要您定义相应虚拟表模块的适用方法。
框架ORM能够使用虚拟表模块,只需定义一些继承TSQLRecordVirtual专用类的TSQLRecord:
可以为Delphi中实现的虚拟表定义TSQLRecordVirtualTableAutoID派生类,并在INSERT时自动生成一个新ID。
可以为Delphi中实现的虚拟表定义TSQLRecordVirtualTableForcedID派生类,在INSERT时强制使用ID值(类似于TSQLRecordRTree或TSQLRecordFTS3/FTS4/FTS5)。
TSQLRecordLogFile被定义为对应TSQLVirtualTableLog ('log')模块检索到的列名,不应该用于任何其他目的。
这些虚拟表模块关联的类可从TSQLModel服务端获取。在C/S应用程序中,在客户端不需要关联(也不需要使用关联,因为它可能会增加代码大小)。但在服务端,必须调用TSQLModel.VirtualTableRegister方法将TSQLVirtualTableClass(即虚拟表模块实现)与TSQLRecordVirtualClass(即其ORM用户接口)关联起来。
例如,下面的代码将注册两个TSQLRecord类,第一个使用“JSON”虚拟表模块,第二个使用“二进制”模块:
Model.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON);
Model.VirtualTableRegister(TSQLRecordDali2,TSQLVirtualTableBinary);
在调用TSQLRestServer.Create之前,需在服务器端完成此注册(或TSQLRestClientDB.Create,对独立应用程序)。否则,在创建虚拟表时将引发异常。
我们已经看到TSQLVirtualTableJSON、TSQLVirtualTableBinary和TSQLVirtualTableCursorJSON类使用TSQLRestStorageInMemory实例实现了一个虚拟表模块,以处理快速的静态内存数据库。
既然可以使用:memory:文件名在内存中创建SQLite3表时,为什么还要使用这种类型的数据库呢?这就是问题所在…
ATTACH DATABASESQL语句,两者都不是由我们的C/S框架进行处理的;ATTACH DATABASE语句可以提供类似的功能);独立于SQLite3引擎使用静态表的第一种方法是调用TSQLRestServer.StaticDataCreate方法。
当然,这个方法只能在服务端调用。对于客户端,普通的表和静态表没有区别。
稍后可以通过TSQLRestServer的StaticDataServer[]属性数组访问TSQLRestStorageInMemory实例管理的存储内容。
正如我们刚刚指出的,这个简单但高效的数据库引擎可以在不需要SQLite3数据库引擎的情况下编译链接到可执行文件中使用,这样可以减少KB级的代码尺寸,也足以处理大多数基本的RESTful请求。
使用静态表的更高级和更强大的方法是定义TSQLRecordVirtualTableAutoID派生类,并与一些TSQLVirtualTable类关联起来。TSQLRecordVirtualTableAutoID父类将定义相关联的虚拟表模块行为,类似于普通的SQLite3表,在执行INSERT时会计算它们的RowID属性。
例如,在提供的回归测试中定义了两个表,表的发布属性定义了三个列,分别是FirstName、YearOfBirth和YearOfDeath:
TSQLRecordDali1 = class(TSQLRecordVirtualTableAutoID)
private
fYearOfBirth: integer;
fFirstName: RawUTF8;
fYearOfDeath: word;
published
property FirstName: RawUTF8 read fFirstName write fFirstName;
property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
property YearOfDeath: word read fYearOfDeath write fYearOfDeath;
end;
TSQLRecordDali2 = class(TSQLRecordDali1);
然后,将这两个类添加到客户端和服务端应用程序的TSQLModel实例中:
ModelC := TSQLModel.Create(
[TSQLRecordPeople, (...)
TSQLRecordDali1,TSQLRecordDali2],'root');
然后,在服务端,将这两个类与对应的虚拟表模块关联:
ModelC.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON);
ModelC.VirtualTableRegister(TSQLRecordDali2,TSQLVirtualTableBinary);
在服务器端,由于VirtualTableRegister调用,当初始化SQLite3数据库连接时,将自动启动“JSON”和“二进制”虚拟表模块:
Client := TSQLRestClientDB.Create(ModelC,nil,Demo,TSQLRestServerTest);
这个TSQLRestClientDB实际上有一个正在运行的TSQLRestServerDB实例,它将用于所有数据库访问,包括虚拟表进程。
如上所述,将在磁盘上创建名为'Dali1.json'和‘Dali2.data'的两个文件,JSON版本将更大,也更容易支持外部应用程序处理。
从代码的角度来看,与普通的TSQLRecord表相比,ORM处理这些虚拟表没有什么不同。例如,下面是从提供的回归测试代码中提取的一些代码:
if aClient.TransactionBegin(TSQLRecordDali1) then
try
// add some items to the file
V2.FillPrepare(aClient,'LastName=:("Dali"):');
n := 0;
while V2.FillOne do begin
VD.FirstName := V2.FirstName;
VD.YearOfBirth := V2.YearOfBirth;
VD.YearOfDeath := V2.YearOfDeath;
inc(n);
Check(aClient.Add(VD,true)=n,Msg);
end;
// update some items in the file
for i := 1 to n do begin
Check(aClient.Retrieve(i,VD),Msg);
Check(VD.ID=i);
Check(IdemPChar(pointer(VD.FirstName),'SALVADOR'));
Check(VD.YearOfBirth=1904);
Check(VD.YearOfDeath=1989);
VD.YearOfBirth := VD.YearOfBirth+i;
VD.YearOfDeath := VD.YearOfDeath+i;
Check(aClient.Update(VD),Msg);
end;
// check SQL requests
for i := 1 to n do begin
Check(aClient.Retrieve(i,VD),Msg);
Check(VD.YearOfBirth=1904+i);
Check(VD.YearOfDeath=1989+i);
end;
Check(aClient.TableRowCount(TSQLRecordDali1)=1001);
aClient.Commit;
except
aClient.RollBack;
end;
需要从客户端调用Commit来写入磁盘内容。在服务器端,为了创建磁盘内容,您必须显示地调用这些代码。
正如我们已经注意到的,默认情况下,将使用基于TSQLRestStorageInMemory的虚拟表在磁盘上写入数据。实际上,上面代码中的Commit方法将调用TSQLRestStorageInMemory.UpdateFile方法。
需要注意的是,SQlite3引擎将处理所有虚拟表,就像处理普通的SQlite3表一样,涉及到数据的原子性。也就是说,如果没有显式定义任何事务(通过TransactionBegin / Commit方法),那么对于每个数据库的变更都将执行该事务(所有CRUD操作,如INSERT / UPDATE / DELETE)。TSQLRestStorageInMemory.UpdateToFile方法不是立即执行的,因为它会每次向磁盘写入所有缓存的表数据。因此,出于性能原因,为了获得更好的性能,必须在虚拟表中嵌套多个变更事务。在所有情况下,这都是使用ORM的标准方法。如果由于某些原因,您后来改变了主意,例如将表从TSQLVirtualTableJSON / TSQLVirtualTableBinary引擎移动到普通的SQlite3引擎,您的代码可以保持不变。
可以使用以下属性强制内存中的虚拟表数据留在内存中,而COMMIT语句在磁盘上什么也不写:
Server.StaticVirtualTable[TSQLRecordDali1].CommitShouldNotUpdateFile := true;
之后为了创建磁盘内容,必须显式地调用相应的方法:
Server.StaticVirtualTable[TSQLRecordDali1].UpdateToFile;
由于StaticVirtualTable属性仅在服务器端可用,所以如果您的客户机更新了表数据,而这个更新根本没有磁盘,那么您需要重新检查代码!
对于存储在内存中的数据,TSQLRestStorageInMemory表是满足ACID特性的。
这意味着并发访问的一致性,可以实现期望的安全运行。
在磁盘上,这种表只有在其内容写入文件时才体现出ACID特性。
意思是,整个文件写操作是满足ACID特性的,文件将始终保持一致。
这些内存表的确切处理过程是,每次向TSQLRestStorageInMemory表写入一些新数据时:
当您将数据写入文件时,将重写整个文件,在每次写入时都将数据写入磁盘可能不现实,在这种情况下,排它模式下的SQLite3将更快,因为它只写入新数据,而不是写入整个表内容。
这听起来像是一种局限,但在我们看来,它更像是一种特征。对于某些特定的表,我们不需要也不希望有一个完整的RDBMS/SQL引擎,只需直接快速地访问TObjectList。该特性是与我们的REST引擎集成,如果TSQLRestStorageInMemory存储对于您使用的进程来说太受限,那么以后仍然能将您的数据改为存储在通常的数据库中(SQLite3或外部)。
有时候,将所有数据库进程驻留在单个程序中可能还不够。您可以使用TSQLRestServer.RemoteDataCreate()方法实例化一个TSQLRestStorageRemote类,它将所有ORM操作跳转到指定的TSQLRest实例,可以是远程(通过TSQLRestClientHttp)或进程内(TSQLRestServer)。在简单的用例中,REST跳转可能就够用了,而完整的主/从复制可能太过复杂。
例如,在TTestExternalDatabase回归测试中,您将发现以下代码:
aExternalClient := TSQLRestClientDB.Create(fExternalModel,nil,'testExternal.db3',TSQLRestServerDB);
historyDB := TSQLRestServerDB.Create(
TSQLModel.Create([TSQLRecordMyHistory],'history'),
'history.db3',false);
historyDB.Model.Owner := historyDB;
historyDB.DB.Synchronous := smOff;
historyDB.DB.LockingMode := lmExclusive;
historyDB.CreateMissingTables;
'history.db3',false);
aExternalClient.Server.RemoteDataCreate(TSQLRecordMyHistory,historyDB);
aExternalClient.Server.DB.Synchronous := smOff;
aExternalClient.Server.DB.LockingMode := lmExclusive;
aExternalClient.Server.CreateMissingTables;
...
这将创建两个SQLite3数据库,一个“testExternal.db3”主数据库,和一个分离的”history.db3”数据库。两者都将关闭同步和开启独占访问模式,参见前面的ACID和速度。
在“history.db3“文件,将有MyHistory表,而在“testExternal.db3“,不会有任何MyHistory表。所有TSQLRecordMyHistory CRUD进程将透明地重定向到historyDB。
然后,通过隐藏的TSQLRestStorageRemote实例将aExternalClient到TSQLRecordMyHistory表的任何ORM访问重定向到historyDB。不会有任何明显的性能损失,相反,独立的数据库会更好。
另一种选择可能是在SQLite3级别上使用ATTACH表语句,但它只能在本地使用,并且您无法切换到另一个数据库引擎。RemoteDataCreate()方法是通用的,并且将与外部数据库(见下文,甚至是NoSQL数据库)一起工作,或者通过TSQLRestClientHTTP实例访问远程mORMot服务器。唯一的先决条件是主模型中的所有TSQLRecord类都存在于重定向数据库模型中。
注意,重定向的TSQLRest实例可以拥有自己的模型、自己的身份验证和授权模式以及自己的缓存策略。在调优应用程序时,这可能会有很大的好处。
请注意,如果您使用TRecordReference字段,模型更好的选择是在本地和跳转TSQLRest实例之间共享,或者至少TSQLRecord类应该有相同的顺序,否则运行侧对TRecordReference的查询值将指向错误的表。
有关这种重定向特性如何与框架的其他功能交互的一些解释,请参阅mORMot基本体系结构—客户端服务端实现和客户端服务端实现中关于服务端如何通过跳转特性与其它框架交互。
这种重定向模式的一个实际应用可能与典型的公司业务有关。
在公司总部可能有一个mORMot主服务器,然后在每个分支机构有本地mORMot服务器,在本地网络为终端应用提供服务:
每个分支机构都可能有自己的TSQLRecord专用表和数据。其他一些表将在各本地办公室之间共享,比如全局配置。
可以在Delphi代码中创建自己的类及专用表:
type
TSQLRecordCustomerAbstract = class // never declared in Model
.... // here the fields used for Customer business
end;
TSQLRecordCustomerA = class(TSQLRecordCustomerAbstract); // for office A
TSQLRecordCustomerB = class(TSQLRecordCustomerAbstract); // for office B
TSQLRecordCustomerClass = class of TSQLRecordCustomerAbstract;
在这里,TSQLRecordCustomerA可能只属于Office A服务器的TSQLModel,而TSQLRecordCustomerB可能只属于Office B服务器的TSQLModel。这能提高安全性,并且在中心主服务器中,TSQLRecordCustomerA和TSQLRecordCustomerB类都将是TSQLModel的一部分,专用的基于接口的服务将能够发布存储表的一些高级数据和统计信息。
您可以在客户端代码中使用TSQLRecordCustomerClass变量,它将包含TSQLRecordCustomerA或TSQLRecordCustomerB,这取决于它运行的位置和它连接的服务器。
在主服务器上存有每个远程办公室的数据库存储表,名为CustomerA或CustomerB。
您将受益于每个TSQLRest实例的缓存功能(见下文)。您可能在本地站点上对缓存进行了调优,而主数据库中的缓存将保持较少水平,但更安全。
此外,即使在单点服务器上,TSQLRecordHistory表或更通用的所有聚合数据都可以在本地或廉价存储上托管,而主数据库将保留在SSD或SAS上。由于有了这个重定向功能,您可以按照预期调整主机。
最后,如果您的目的是将给定TSQLRestServer的所有表重定向到另一个远程TSQLRestServer(出于安全或托管目的),您可以考虑使用TSQLRestServerRemoteDB。这个类将所有表重定向到一个外部实例。
TSQLRestStorageRemote和TSQLRestServerRemoteDB类都还不支持SQlite3的虚拟表机制。因此,如果使用这些特性,您可能无法从重定向实例运行联合查询,事实上,SQlite3主引擎将抱怨“testExternal.db3”中缺少MyHistory表。我们最终将定义所需的TSQLVirtualTableRemote和TSQLVirtualTableCursorRemote类来实现这个特性。
遗憾的是,如果连接丢失,这种重定向模式将无法工作。中心服务器需要始终可访问,以便远程办公室继续工作。您可以考虑使用主/从复制来允许本地办公室使用它们自己的主数据本地副本。实际上,听起来比起简单的跳转,复制才是首选,特别是在网络和资源使用方面。
如下所述,我们的ORM可以访问一些外部数据库。
SQLite3的虚拟表特性支持对远程表的访问就像“本地”SQLite3表一样。事实上,你可以编写一个跨SQLite3、MS SQL Server、MySQL、FireBird、PostgreSQL、MySQL、DB2、Informix和Oracle数据库的SQL连接查询,包括在多个连接和多个远程服务器的情况下。可以将其视为基于orm的支持任意数据源的BI系统,将各种数据源添加到基于代码的报告引擎(能够生成pdf)中,这可能是一种非常强大的整合任何类型数据的方法。
为了定义这样的外部表,像往常一样先定义普通的TSQLRecord类,然后调用VirtualTableExternalRegister()或VirtualTableExternalMap()函数将将该类定义为由外部数据库引擎管理的虚拟表。使用专用的外部数据库服务器将支持更好的响应时间或其他特性(比如与其他应用程序或语言共享数据)。如果确实需要内部数据库,服务器端可能会忽略对VirtualTableExternalRegister()的调用,并将在运行时允许定制数据库配置,具体取决于客户的需要(或许可)。
对于外部数据库(见下文),SQL转换将以一种更高级的方式进行,因此您能够在客户端使用这种虚拟表,而无需任何特定的模型通知。在这种情况下,您可以安全地将表定义为TSQLValue1 = class(TSQLRecord),而无需在客户端进一步编写代码。
在使用静态(内存/ TObjectList)存储时,如果您希望所有ORM特性都能远程工作,那么您需要通知客户端模型有一个虚拟表。否则,在执行请求时可能会遇到一些SQL错误,比如“no such column: ID”。
例如,假设您在服务器端定义了两个JSON内存虚拟表:
type
TSQLServer = class(TSQLRestServerDB)
private
FHttpServer: TSQLHttpServer;
public
constructor Create;
destructor Destroy; override;
end;
constructor TSQLServer.Create;
var aModel: TSQLModel;
begin
aModel := CreateModel;
aModel.VirtualTableRegister(TSQLValue1, TSQLVirtualTableJSON);
aModel.VirtualTableRegister(TSQLValue2, TSQLVirtualTableJSON);
aModel.Owner := self; // model will be released with TSQLServer instance
inherited Create(aModel, ChangeFileExt(ParamStr(0), '.db'), True);
Self.CreateMissingTables(0);
FHttpServer:= TSQLHttpServer.Create('8080', Self);
end;
destructor TSQLServer.Destroy;
begin
FHttpServer.Free;
inherited;
end;
您需要告知客户端TSQLValue1和TSQLValue2是虚拟表。
你有几种选择:
type
TSQLValue1 = class(TSQLRecordVirtualTableAutoID)
(...)
TSQLValue2 = class(TSQLRecordVirtualTableAutoID)
(...)
如果表定义为TSQLValue1 = class(TSQLRecord),则可以这样修改客户端模型:
type
TSQLClient = class(TSQLHttpClient)
public
constructor Create;
end;
constructor TSQLClient.Create;
var aModel: TSQLModel;
begin
aModel:= CreateModel;
aModel.Props[TSQLValue1].Kind := rCustomAutoID;
aModel.Props[TSQLValue2].Kind := rCustomAutoID;
aModel.Owner := self; // model will be released within TSQLServer instance
inherited Create('127.0.0.1', '8080', aModel);
SetUser('Admin', 'synopse');
end;
如果表定义为TSQLValue1 = class(TSQLRecord),那么最简单的方法就是在创建共享模型时设置属性:
function CreateModel: TSQLModel;
begin
result:= TSQLModel.Create([TSQLAuthGroup, TSQLAuthUser, TSQLValue1, TSQLValue2]);
result.Props[TSQLValue1].Kind := rCustomAutoID;
result.Props[TSQLValue2].Kind := rCustomAutoID;
end;
最简单的方法是让静态内存表从TSQLRecordVirtualTableAutoID继承。只需按照本书所述来使用框架,参见虚拟内存表。
同样,此限制不适用于访问外部数据库的情况。