7. 数据库层

7.1. SQLite3驱动但不限于SQLite3

  这个框架的核心数据库使用SQLite3库,它是一个免费的、安全的、零配置的、无服务器的、单一稳定的跨平台文件数据库引擎。

  译者注:Server-less,无服务架构,旨在帮助开发者摆脱运行后端应用程序所需的服务器设备的设置和管理工作。这项技术的目标并不是为了实现真正意义上的“无服务器”,而是指由第三方供应商负责后端基础结构的维护,以服务的方式为开发者提供所需功能,例如数据库、消息,以及身份验证等。这种服务基础结构通常可以叫做后端即服务(Backend-as-a-Service,BaaS),或移动后端即服务(MobileBackend-as-a-service,MBaaS)。

  现在,无服务器架构是指大量依赖第三方服务(也叫做后端即服务,即“BaaS”)或暂存容器中运行的自定义代码(函数即服务,即“FaaS”)的应用程序,函数是无服务器架构中抽象语言运行时的最小单位,在这种架构中,我们并不看重运行一个函数需要多少CPU或RAM或任何其他资源,而是更看重运行函数所需的时间,我们也只为这些函数的运行时间付费。无服务器架构中函数可以多种方式触发,如定期运行函数的定时器、HTTP请求或某些相关服务中的某个事件。

  如下所述,如果您愿意,您可以使用任何其他数据库访问层:

  • 包含了一个快速的内存引擎,在速度方面比任何sql的解决方案都要好,但是在磁盘上满足ACID行为的代价高(可使用RAM满足ACID);
  • 一个集成的SQLite3引擎,它是嵌入式解决方案(即便是在服务器端)的最佳候选;
  • 任何远程RDBMS数据库,通过一个或多个OleDB、ODBC、Zeos或Oracle连接来存储您宝贵的ORM对象。或者你也可以使用任意DB.pas单元,访问NexusDB或DBExpress、FireDAC、AnyDAC、UniDAC(或已废弃的BDE)支持的任何数据库引擎。在所有这些情况下,ORM目前支持SQLite3、Oracle、Jet/MSAccess、MS SQL、Firebird、DB2、PostgreSQL、MySQL、Informix和NexusDB SQL;
  • 任何其他TSQLRest实例(TSQLRestServer或远程TSQLRestClientHTTP);
  • 直接访问MongoDB数据库,它实现了真正的NoSQL和对象文档映射(ODM)设计。
direct
direct
virtual
virtual
direct
direct
direct
direct
DB.pas
TDataSet
Oracle SQLite3 ODBC
OleDB ZDBC
FireDAC AnyDAC UniDAC
BDE DBExpress NexusD
TObjectList
NoSQL
External SQL
RDBMS
MongoDB
NoSQL
TSQLRest
redirection
SQLite3
mORMot
ORM

  SQlite3将被用作主要的SQL引擎,由于其虚拟表的独特特性,它能够连接所有上述这些表。实际上,您可以在相同的数据库模型中混合使用内部和外部引擎,并在一条SQL语句中访问所有数据。

7.1.1. SQLite3是核心

  这个框架使用了官方SQLite3库源代码的编译版本,并将其集成在Delphi代码中。因此,这个框架为标准的SQLite3数据库引擎添加了一些非常有用的功能,但又保留了它的所有优势,如本文前一段所述:

  • 可以静态链接到可执行文件,或动态加载sqlite3.dll;
  • 更快的数据库访问,通过统一的内存模型,使用FastMM4内存管理器(在内存分配方面几乎比Windows默认内存管理器快10倍);
  • 可选的直接加密磁盘数据(至AES256级别,即最高加密安全级别);
  • 通过mORMot的ORM将数据库在Delphi源代码中定义一次(作为类的发布属性),避免了很多SQL语句编写,避免了公共字段或表名的不匹配问题;
  • 数据库的记录级锁定(SQLite3只支持文件级锁定);
  • 当然,SQLite3引擎主要新增强化的是它可以既可独立部署,也可部署在C/S架构中,而SQLite3库原本仅在独立模式下工作。

  从技术角度来看,以下是目前构建SQLite3引擎时使用的编译选项:

  • 使用ISO 861:2004格式妥善处理文本字段中的日期/时间值,或更快捷的Int64自定义类型(TTimeLog / TModTime / TCreateTime或TUnixTime);
  • SQLite3库的单元编译使用了RTREE扩展,支持快速大范围查询;
  • 可包含FTS3/FTS4全文搜索引擎(匹配操作),集成SQL优化排序功能;
  • 框架仅使用最新的API (sqlite3_prepare_v2),并遵循最新的SQLite3官方设计文档;
  • 添加了额外的排序规则(如排序功能),不仅可以有效地处理UTF-8文本,还可以有效地处理ISO 8601时间编码、快速Win1252不相容性比较和本地慢但准确的Windows UTF-16功能;
  • 额外的SQL函数,如用于英语/法语/西班牙语的Soundex语音算法、MOD或CONCAT,以及一些专门的函数,可以直接在Delphi高级类型(比如序列化的动态数组)定义的BLOB字段中搜索数据;
  • 使用开源PCRE库处理SQL语句中的正则表达式查询的REGEXP操作符/函数;
  • 在Delphi代码中自定义SQL函数;
  • SQL语句参数准备自动化,加快执行速度;
  • TSQLDatabase可以为SELECT语句缓存最后的结果,或使用客户端、服务端记录逐一缓存的优化方案,以实现更轻便的web服务器或为客户端用户接口提升读取速度。
  • 用户身份验证处理(SQLite3是免验证设计);
  • SQLite3源代码编译时没有线程互斥,调用者必须具有线程安全意识,这在很多结构中会更快,因为互斥每次必须获取,底层sqlite3_*()函数不是线程安全的,TSQLRequest和TSQLBlobStream只是包装它们,但TSQLDataBase是线程安全的,因为TSQLTableDB / TSQLRestServerDB / TSQLRestClientDB是调用TSQLDataBase;
  • 使用SQLITE_OMIT_SHARED_CACHE定义进行编译,因为框架的C/S实现方法,不可能发生并发访问,并且内部增加了高效的缓存算法,在多用户环境中减少了对SQLite3引擎的调用(所有AJAX调用都会从中受益);

  译者注:SQLITE_OMIT_SHARED_CACHE,省略共享缓存,允许消除代码中性能部分的许多关键条件,可以给性能带来明显的改善。

  SHARED_CACHE,同一线程或同一进程对同一数据库的多个连接(connection)可以以共享缓存的方式呈现,实际上对于数据库只有一个连接。

  • 嵌入的SQLite3数据库引擎可以从http://sqlite.org的官方SQLite3源代码轻松更新,使用C语言的混合编程,只做了一些小修改(文档在SynSQLite3Static.pas单元中),最终C语言源代码以.obj的形式交付,也可以在官方源代码库中找到。

  在您的服务端应用包含SQlite3所付出的成本是值得的,对可执行文件只增加了KB级字节,但是有很多不错的特性,即使只使用其它外部数据库。

7.1.2. 对SQLite3虚拟表的扩充

  由于框架是真正面向对象的,因此可以使用其它数据库引擎来代替框架。您可以很容易地编写自己的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。

7.1.3. 数据访问的基准测试

  这里的目的不是说一个库或数据库比另一个更好或更快,而是发布mORMot持久化层针对每个数据库后端的访问能力的基本情况。

  在这种情况下,我们不只是对单纯的SQL/DB层访问能力进行基准测试(SynDB.pas单元),还包括框架在C/S架构下的ORM访问能力。

  以下测试过程涉及ORM的各个方面:

  • 通过高级CRUD方法访问(添加/更新/删除/检索,按对象或批处理模式);
  • 通过优化的RTTI对TSQLRecord实例的读写访问;
  • JSON编码各种值(为网络传输做好了准备);
  • REST路由,具有安全性、日志记录和统计功能;
  • 使用SQLite3内核实现了虚拟跨数据库层;
  • SQL动态生成和转换(在虚拟模式下);
  • 通过几个库或驱动访问数据库引擎。

  在这些测试中,我们只是绕过了通信层,因为TSQLRestClient和TSQLRestServer是在进程中运行的,在同一个线程中,就像TSQLRestServerDB实例一样。因此,这里有一些关于我们的框架的ORM和RESTful核心性能的第一手证据,当通过网络在高端硬件上运行时,框架会有很好的伸缩性。

  在最近的笔记本电脑(Core i7和SSD驱动器)上,根据后端数据库接口,mORMot在速度方面表现出色,如下所示:

  • 您可以每秒持久化57万个对象,或者每秒检索87万个对象(对于我们的纯Delphi内存引擎而言);
  • 当从服务端或客户端的ORM缓存中检索数据时,无论数据库后端是什么,每秒可以读取超过90万个对象;
  • 使用像Oracle这样的高性能数据库和我们的直接访问类,您可以在100 MB的网络上每秒写入70,000个对象(通过数组绑定)和读取160,000个对象;
  • 当使用数据库访问库(例如Zeos或基于DB.pas的类),速度较低(无论是DB2、MS SQL、PostgreSQL还是MySQL),但仍然足以完成大多数工作,这是由于mORMot代码中的一些优化(如准备语句的缓存、SQL多值插入、直接JSON导入/导出、SQlite3虚拟模式设计、避免了大多数临时内存分配…)。

  我想很难找到一个更快的ORM。

7.1.3.1. 软硬件配置

  下面的表总结了所有的配置选项,并给出一些基准测试(平均每秒读写对象)。

  • 'SQLite3(file full/off/exc)'表示使用内部SQLite3引擎,Synchronous := smOff是否开启或DB.LockingMode := lmExclusive,参见下图;
  • SQLite3 (mem)'表示在内存中运行的内部SQLite3引擎;
  • 'SQLite3 (ext…)'表示访问SQLite3引擎文件或内存外部数据库,请参阅下面;
  • ‘TObjectList’表示一个TSQLRestStorageInMemory实例,如下所示,静态(没有SQL支持)或虚拟(即SQL通过SQLite3虚拟表机制),可以将数据以JSON或压缩二进制形式保存在磁盘上;
  • 'WinHTTP SQLite3'和'Sockets SQLite3'表示使用我们的SynDBRemote.pas单元在HTTP上发布的SQLite3引擎,在客户端使用WinHTTP API或普通套接字,然后通过ORM作为外部数据库访问;
  • “NexusDB”是免费的嵌入式版本,可从官方网站获得;
  • 'Jet'表示通过OleDB访问的Jet/MSAccess数据库引擎。
  • 'Oracle'显示了我们的直接OCI访问层(SynDBOracle.pas)的结果;
  • 'Zeos *'表示数据库直接通过ZDBC层访问;
  • 'FireDAC *'表示FireDAC库;
  • 'UniDAC *'代表UniDAC库;
  • 'BDE *'时使用BDE连接;
  • 'ODBC *'用于直接通过ODBC访问;
  • 'MongoDB ack/no ack'表示直接MongoDB访问(SynMongoDB.pas),带或不带写入应答。

  此数据库驱动程序列表未来将扩充,欢迎任何反馈!

  数字表示行/秒或对象/秒,这个基准测试是用Delphi XE4编译的,因为较新的编译器往往会提供更好的结果,这主要归功于函数内联(在Delphi 6-7中不支持)。

  注意,这些测试不是对每个数据库引擎的速度比较,而是在mORMot中集成了多个数据库进行访问的当前状态。

  基准测试运行在i7笔记本上,运行Windows 7,配备标准SSD,包括杀毒和后台应用程序:

  • 通过100兆以太网连接到Oracle 11.2.0.1共享数据库;
  • 本机在64位模式下运行的MS SQL Express 2008 R2;
  • 本机在64位模式下运行的IBM DB2 Express-C edition 10.5;
  • 本机在64位模式下运行的PostgreSQL 9.2.7;
  • 本机在64位模式下运行的MySQL 5.6.16;
  • Firebird 2.5.2嵌入式版本;
  • NexusDB 3.11免费的嵌入式版本;
  • 在64位模式下运行的MongoDB 2.6。

  所以它是一个开发环境,非常类似于低成本的生产现场,不致力于提供最好的性能。在此过程中,CPU只被用于内存中的SQLite3和TObjectList,大多数情况下,瓶颈不是CPU,而是存储或网络。因此,速率和时间可能会因网络和服务器负载的不同而有所不同,但是您得到的结果与在客户端使用普通硬件配置时所期望的结果相似。当使用优化后的Linux高级服务器和存储运行时,您可以期望得到更好的数字。

  测试是用Delphi XE4 32位模式编译的。大多数测试在编译为64位程序时都能通过,但是有些程序(如Jet)除外,它在这个平台上是不可用的。速度结果几乎相同,只是稍微慢一些,这里就不展示了。

  您可以编译“15 - External DB performance”提供的示例代码,并在自己的配置上运行相同的基准测试。欢迎反馈!

  在我们的测试中,我们使用的UniDAC版本在与DB2一起使用时存在很大的稳定性问题:测试没有通过,DB2服务器会挂起查询操作,而其他库没有问题。它现在可能已经修复了,但是在这里,您在下面的基准测试中找不到关于“UniDAC DB2”的结果。

7.1.3.2. 插入速度

  我们将在不同的场景各插入5000行数据:

  • “Direct”表示调用Client.Add()逐条插入。
  • “Batch”批处理模式;
  • “Trans”表示所有插入都嵌套在一个事务中,会导致很大差异。

  下面是一些插入速度值(单位:对象/秒):

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.pasSynDBFireDAC.pas`(在FireDAC/AnyDAC中称为数组DML)库的Oracle direct逐条访问中,数组绑定特性的批处理优势很大。

  对于大多数引擎,我们的ORM内核能够生成正确的SQL语句来加速批量插入。例如:

  • SQlite3, MySQL, PostgreSQL, MSSQL 2008, DB2, MySQL或NexusDB会使用多条INSERT INTO .. VALUES (..),(..),(..)..处理INSERT语句;
  • Oracle使用INSERT INTO .. INTO .. SELECT 1 FROM DUAL(奇怪的语法,不是吗?);
  • Firebird实现了EXECUTE BLOCK

  因此,当使用BatchAdd()时,一些引擎显示了很好的速度提升,即便使用SQLite3外部引擎也比逐条执行时更快!该特性处于ORM/SQL级别,因此它对任何外部数据库都有好处。当然,如果给定的库具有更好的实现模式(如我们的direct Oracle、Zeos或带有本机数组绑定的FireDAC),也会使用它。

  MongoDB支持大容量数据插入,在批处理模式下也实现了惊人的速度提升。基于MongoDB写回执模式,插入速度可以非常高,默认情况下,对每个写操作服务器都有回执,但是你可以设置wcUnacknowledged 模式旁路回执,在这种情况下,任何错误(如一个唯一字段值重复)您永远得不到通知,所以它不能用于生产,除非你需要这个特性快速填充数据库,或尽可能快地合并一些数据。

  译者注:write concern mode,写回执模式?

7.1.3.3. 读取速度

  现在通过ORM层获取相同的数据:

  • “By one”表示每次调用读取一个对象(ORM为Client.Retrieve()调用生成SELECT * FROM table WHERE ID=?);
  • “All *”是在一次调用中读取全部5000个对象(即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次读请求,无论是否使用数据库。

7.1.3.4. 分析和使用建议

  当声明为虚表时(通过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进行传输的好处。

  大多数知名的闭源数据库是可用的:

  • 对Oracle的直接访问在批处理模式(即数组绑定)中产生了令人印象深刻的结果。如果您的最终客户将其数据存储在此类服务器中,并且希望降低IT解决方案的许可成本,那么Oracle Express edition是免费的,但在数据/硬件大小方面有些限制(请参阅其许可条款);
  • 通过OleDB(或ODBC)直接访问MS SQL Server提供了很好的时效。microsoft SQL Server 2008 R2 Express与Windows环境集成得非常好,价格非常实惠,免费的LocalDB (MSI installer)版本初期已经足够使用了,但也有数据/硬件大小限制,就像Oracle Express一样;
  • IBM DB2是另一个很好的候选,express - C(“C”代表社区)提供了一个免费的机会,以运行一个行业标准引擎,企业级特性,没有数据大小限制,但有硬件性能限制(最新的10.5版本限制16 GB内存和2个CPU);
  • 我们这里没有包含Informix数据,因为这个数据库的支持是由一个补丁用户提供的,感谢Esteban Martin的分享!在这里我们不使用这类服务器;
  • 如果您拥有现有的Delphi代码和数据,则可以考虑使用NexusDB,但它与其商业竞争对手相比知名度和认知度较低。

  开源数据库也是值得考虑的,尤其是与mORMot这样的开源框架一起使用时:

  • MySQL是许多web站点使用的著名引擎,主要用于LAMP (Linux Apache MySQL PHP)配置。Windows不是运行它的最佳平台,但它可能是一个相当不错的选择,尤其是在它的MariaDB分支平台上,后者Oracle拥有所有权,听起来官方主版本更具吸引力;
  • PostgreSQL是一个企业级数据库,作为开源替代品,它拥有许多令人惊叹的特性,能真正与商业解决方案竞争。即使在Windows下,它也易于安装和管理,并且比其他商业引擎使用更少的资源;
  • Firebird在通过Zeos/ZDBC访问时给出了相当一致的时间。我们在这里展示的是嵌入式版本,但是服务器版本也值得考虑,因为许多Delphi程序员都能熟练地使用这个免费的Interbase替代方案;
  • MongoDB似乎是SQL数据库的一个有力竞争者,它具有水平伸缩和安装/管理易用性的潜在优势,性能非常高,而且基于文档的存储与mORMot的高级ORM特性(如无共享架构\分片)非常匹配。

  要访问这些数据库,还可以使用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表优化数据库访问。

7.2. SQLite3的使用

  从框架的1.15版本开始,SQLite3引擎从mORMotSQLite3.pas单元剥离,独立定义为SynSQLite3.pas单元。

  可以这样来使用它:

  • 通过SynSQLite3.pas独立调用其所有功能,甚至底层的C API,但此时你不能轻松地切换到另一个数据库引擎;
  • 通过SynDBSQLite3.pas ,使用SynDB.pas通用访问类,独立使用高级SQL访问,这样你就可以在需要的时候更换成其他数据库引擎(如MS SQL, PostgreSQL, MySQL或Oracle);
  • 基于C/S模式可调用ORM所有功能,参见mORMotSQLite3.pas。

  我们将在这里明确一些使用SQLite3引擎的重点内容,并参考http://sqlite.org上这个伟大的开源项目的官方文档,以了解其常用特性的基本信息。

7.2.1. 静态链接或使用外部dll

  框架从1.18版,SynSQlite3.pas单元可以通过两种方式调用SQLite3引擎:

  • 静态链接到项目可执行文件;
  • 使用外部sqlite3.dll库。

  SQLite3 API和常量在SynSQlite3.pas中定义,可以通过定义TSQLite3Library类来访问。如下定义了一个全局sqlite3变量:

var
  sqlite3: TSQLite3Library;

  要使用SQLite3引擎,需要将TSQLite3Library的类实例分配给这个全局变量,然后mORMot的所有调用都将通过它进行,例如调用sqlite3.open()而不是sqlite3_open()。

  它有两个实现类:

TSQLite3LibraryStatic
TSQLite3Library
TSQLite3LibraryDynamic
单元 用途
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外部动态连接库。

7.2.1.1. bcc编译的.obj

7.2.1.2. 官方MinGW编译的sqlite3.dll

7.2.1.3. Visual C++编译的sqlite3.dll

7.2.2. 语句缓存

  为了减少在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语句,以达到最佳的速度。

7.2.3. R-Tree范围索引

  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子句进行匹配)。

7.2.4. FTS3/FTS4/FTS5

  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的其他平台)。

7.2.4.1. FTS3/FTS4/FTS5专用记录类型

  为了便于使用FTS特性,我们定义了一些类型:

  • TSQLRecordFTS3用于创建一个FTS3表与默认的“简单”词干;
  • TSQLRecordFTS3Porter使用波特词干提取算法创建一个FTS3表;
  • TSQLRecordFTS3Unicode61使用Unicode61的词干提取算法创建一个FTS3表;
  • TSQLRecordFTS4创建一个默认的“简单”词干的FTS4表;
  • TSQLRecordFTS4Porter使用波特词干提取算法创建一个FTS4表;
  • TSQLRecordFTS4Unicode61使用Unicode61接口创建一个FTS4表;
  • TSQLRecordFTS5创建一个默认的“简单”词干的FTS5表;
  • TSQLRecordFTS5Porter使用波特词干提取算法创建一个FTS5表;
  • TSQLRecordFTS5Unicode61创建一个使用Unicode61词干的FTS5表;

  译者注:stem-词干,stemming-提取词干,在英语中,一个单词常常是另一个单词的“变种”,如:happy=>happiness,这里happy叫做happiness的词干(stem)。在信息检索系统中,我们常常做的一件事,就是在Term规范化过程中,提取词干,即除去英文单词分词变换形式的结尾。

  下面的图将详细描述这个类层次结构:

TSQLRecord
TSQLRecordVirtual
TSQLRecordFTS3
TSQLRecordFTS3Porter
TSQLRecordFTS4
TSQLRecordFTS3Unicode61
TSQLRecordFTS4Porter
TSQLRecordFTS5
TSQLRecordFTS4Unicode61
TSQLRecordFTS5Porter
TSQLRecordFTS5Unicode61

  实际上,在处理拉丁内容时,您应该使用TSQLRecordFTS5,使用TSQLRecordFTS5Unicode61对非拉丁支持更好,如果您的内容是纯英语文本,则应该使用TSQLRecordFTS5Porter。

7.2.4.2. 词干提取

  “词干提取”算法(参见http://sqlite.org/fts3.html#tokenizer)是对英文文本进行解析以便从原始文本创建单词索引的方法。

  默认的简单分词器提取符号特征或使用基本的FTS全文检索时会遵循以下规则:

  • 术语是符合条件的字符序列,其中符合条件的字符都是字母数字字符、“_”字符以及UTF代码点大于或等于128的所有字符。在将文档拆分为术语时,将丢弃所有其他字符。它们唯一的贡献是分离相邻项。
  • 作为标记化过程的一部分,ASCII范围内的所有大写字符(UTF代码点小于128)都被转换为小写字符。因此,在使用简单的分词器进行全文检索不区分大小写。

  例如,当一个文档包含“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”还从拉丁字符中删除所有的变音符号。

7.2.4.3. FTS检索

  一个好的方法是将数据存储在普通的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”列的两倍。

7.2.4.4. 无内容的FTS4索引表

  正如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页面。

7.2.5. 列排序规则

  在任何数据库中,都需要定义如何比较列数据,用于正确搜索和排序数据。这就是所谓排序规则。

  默认情况下,当SQLite比较两个字符串时,它使用顺序排序或排序函数(两个单词表示相同的东西)来确定哪个字符串更大,或者两个字符串是否相等。SQLite有三个内置的排序函数:BINARY、NOCASE和RTRIM:

  • BINARY,使用memcmp()比较字符串数据,而不考虑文本编码。
  • NOCASE,与二进制相同,除了ASCII的26个大写字符在执行比较之前被转换成小写。注意,只有ASCII字符是大小写转换的。由于表的大小限制,SQLite一般不会对Unicode进行大小写转换,但是您可以使用mORMot的SYSTEMNOCASE或WIN32CASE/WIN32NOCASE自定义排序来增强大小写转换支持(见下面);
  • 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语句。

7.2.6. 正则表达式运算符

  我们的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,并维护已编译正则表达式的每个连接缓存,以确保最佳性能。

7.2.7. ACID和速度

  如上面的数据访问基准测试所述,在普通硬盘上运行时,默认的SQlite3写入速度相当慢。默认情况下,引擎在发出OS级的写命令后会暂停。这保证了数据被写入磁盘,并具有数据库引擎的ACID属性。

  ACID是“Atomicity Consistency Isolation Durability”属性的缩写,它保证数据库事务被可靠地处理。例如,在电源丢失或硬件故障的情况下,数据能以一致的方式保存在磁盘上,而不会有潜在的数据丢失。

  在SQLite3中,ACID是通过两种方法在文件级实现的:

  • 同步写入:这意味着引擎在处理下一个请求之前将等待任何写入的内容被刷新到磁盘;
  • 文件锁定:这意味着数据库文件在写入期间被锁定为独占使用,允许多个进程并发地访问同一数据库文件。

  更改这些默认设置可以提升写入性能。

7.2.7.1. 写入同步

  可以通过设置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引擎!

7.2.7.2. 文件锁

  您可以修改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内容的服务器来说会很有效。

7.2.7.3. 性能优化

  默认情况下,缓慢但真正的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)。

7.2.8. 数据库备份

  在所有情况下,不要忘记尽可能频繁地执行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”文件。

7.3. 神奇的虚表

  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。

7.3.1. 虚拟表模块类

  从版本1.13开始,向框架添加了专用机制,以便使用纯Delphi代码轻松地添加这样的虚拟表。

  为了实现新的虚拟表类型,您必须定义一个所谓的模块来处理字段和数据访问,并为SELECT语句定义一个关联的游标。这是由mORMot.pas单元中定义的TSQLVirtualTable和TSQLVirtualTableCursor两个类实现的。

  例如,以下是从这些类派生出来的默认虚拟表类:

TSQLVirtualTableLog
TSQLVirtualTable
TSQLVirtualTableBinary
TSQLVirtualTableJSON
TSQLVirtualTableCursorJSON
TSQLVirtualTableCursorIndex
TSQLVirtualTableCursorLog
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数据库引擎实现了这一优秀特性。

7.3.2. 定义虚拟表模块

  下面是如何定义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=0fMax=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方法调用之前文本内存仍然可用。

7.3.3. 使用虚拟表模块

  从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)可以与任何类型的数据一起使用,只要您定义相应虚拟表模块的适用方法。

7.3.4. 虚拟表、ORM和TSQLRecord

  框架ORM能够使用虚拟表模块,只需定义一些继承TSQLRecordVirtual专用类的TSQLRecord:

TSQLRecordVirtualTableForcedID
TSQLRecordVirtual
TSQLRecordLogFile
TSQLRecordVirtualTableAutoID
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,对独立应用程序)。否则,在创建虚拟表时将引发异常。

7.3.5. 内存中的“静态”过程

  我们已经看到TSQLVirtualTableJSON、TSQLVirtualTableBinary和TSQLVirtualTableCursorJSON类使用TSQLRestStorageInMemory实例实现了一个虚拟表模块,以处理快速的静态内存数据库。

  既然可以使用:memory:文件名在内存中创建SQLite3表时,为什么还要使用这种类型的数据库呢?这就是问题所在…

  • SQlite3内存中的表不是持久的,而我们的JSON或二进制虚拟表模块可以根据需要写入磁盘,此时将aServer.StaticVirtualTable[aClass].CommitShouldNotUpdateFile属性设置为true,并显式调用aServer.StaticVirtualTable[aClass].UpdateToFile来写入文件;
  • SQlite3内存中的表需要两个数据库连接,或者调用ATTACH DATABASESQL语句,两者都不是由我们的C/S框架进行处理的;
  • SQlite3内存表只能通过SQL语句访问,而TSQLRestStorageInMemory表可以更快地直接访问许多常见的RESTful命令(GET / POST / PUT / DELETE单个行),这可能会影响服务端CPU负载能力,尤其是框架的批处理特性;
  • 在服务端,纯Delphi代码直接使用内存中的TSQLRecord实例表可能非常方便,这正是TSQLRestStorageInMemory所支持的,对于ORM框架来说这无疑是很有意义的;
  • 在客户端或服务端,您可以使用Delphi为TSQLRestStorageInMemory编写专用的“getter”方法,轻松地创建计算字段,而SQlite3内存表需要额外的SQL编码;
  • SQLite3表存储在主数据库文件,在某些情况下,在一些分离的数据库文件中也便于增加一些额外的表内容(轮询调度表,一个配置表写入JSON,一些内容在用户间共享…),这使得使用我们的JSON或二进制虚拟表模块成为可能(老实说,ATTACH DATABASE语句可以提供类似的功能);
  • TSQLRestStorageInMemory类可以独立使用,也就是说,不需要SQLite3引擎,它可以用来生成小型高效的服务端软件,参见“SQLite3\Samples\01 - In Memory ORM”文件夹。

7.3.5.1. 内存表

  独立于SQLite3引擎使用静态表的第一种方法是调用TSQLRestServer.StaticDataCreate方法。

  当然,这个方法只能在服务端调用。对于客户端,普通的表和静态表没有区别。

  稍后可以通过TSQLRestServer的StaticDataServer[]属性数组访问TSQLRestStorageInMemory实例管理的存储内容。

  正如我们刚刚指出的,这个简单但高效的数据库引擎可以在不需要SQLite3数据库引擎的情况下编译链接到可执行文件中使用,这样可以减少KB级的代码尺寸,也足以处理大多数基本的RESTful请求。

7.3.5.2. 虚拟内存表

  使用静态表的更高级和更强大的方法是定义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属性仅在服务器端可用,所以如果您的客户机更新了表数据,而这个更新根本没有磁盘,那么您需要重新检查代码!

7.3.5.3. 内存和ACID特性

  对于存储在内存中的数据,TSQLRestStorageInMemory表是满足ACID特性的。
  这意味着并发访问的一致性,可以实现期望的安全运行。

  在磁盘上,这种表只有在其内容写入文件时才体现出ACID特性。
  意思是,整个文件写操作是满足ACID特性的,文件将始终保持一致。

  这些内存表的确切处理过程是,每次向TSQLRestStorageInMemory表写入一些新数据时:

  • 它在内存中是满足ACID特性的(即在并发模式下安全工作);
  • 单个记录的写操作(INSERT/UPDATE/DELETE)不会自动写入文件;
  • COMMIT操作默认将整个表写入文件(以JSON或压缩二进制格式);
  • 如果CommitShouldNotUpdateFile属性设置为TRUE,COMMIT操作将不会将数据写入文件中;
  • ROLLBACK过程不会做任何事情,所以不需要考虑ACID特性,但是因为您的代码以后可能会使用真实的RDBMS,所以总是显式地书写该命令是一个好习惯,就像上面的示例代码一样,明确地书写except aClient.RollBack。

  当您将数据写入文件时,将重写整个文件,在每次写入时都将数据写入磁盘可能不现实,在这种情况下,排它模式下的SQLite3将更快,因为它只写入新数据,而不是写入整个表内容。

  这听起来像是一种局限,但在我们看来,它更像是一种特征。对于某些特定的表,我们不需要也不希望有一个完整的RDBMS/SQL引擎,只需直接快速地访问TObjectList。该特性是与我们的REST引擎集成,如果TSQLRestStorageInMemory存储对于您使用的进程来说太受限,那么以后仍然能将您的数据改为存储在通常的数据库中(SQLite3或外部)。

7.3.6. 跳转外部TSQLRest

  有时候,将所有数据库进程驻留在单个程序中可能还不够。您可以使用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服务器,在本地网络为终端应用提供服务:

Main Office
Office A
Office B
HTTP
HTTP
local
network
local
network
local
network
local
network
local
network
local
network
local
network
Main
Server
External DB
Local
Server A
Client 1
Client 2
Client 3
Local
Server B
Client 1
Client 2
Client 3
Client 4

  每个分支机构都可能有自己的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类来实现这个特性。

  遗憾的是,如果连接丢失,这种重定向模式将无法工作。中心服务器需要始终可访问,以便远程办公室继续工作。您可以考虑使用主/从复制来允许本地办公室使用它们自己的主数据本地副本。实际上,听起来比起简单的跳转,复制才是首选,特别是在网络和资源使用方面。

7.3.7. 虚拟表访问外部数据库

  如下所述,我们的ORM可以访问一些外部数据库。

  SQLite3的虚拟表特性支持对远程表的访问就像“本地”SQLite3表一样。事实上,你可以编写一个跨SQLite3、MS SQL Server、MySQL、FireBird、PostgreSQL、MySQL、DB2、Informix和Oracle数据库的SQL连接查询,包括在多个连接和多个远程服务器的情况下。可以将其视为基于orm的支持任意数据源的BI系统,将各种数据源添加到基于代码的报告引擎(能够生成pdf)中,这可能是一种非常强大的整合任何类型数据的方法。

  为了定义这样的外部表,像往常一样先定义普通的TSQLRecord类,然后调用VirtualTableExternalRegister()或VirtualTableExternalMap()函数将将该类定义为由外部数据库引擎管理的虚拟表。使用专用的外部数据库服务器将支持更好的响应时间或其他特性(比如与其他应用程序或语言共享数据)。如果确实需要内部数据库,服务器端可能会忽略对VirtualTableExternalRegister()的调用,并将在运行时允许定制数据库配置,具体取决于客户的需要(或许可)。

7.3.8. 客户端虚拟表

  对于外部数据库(见下文),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是虚拟表。

  你有几种选择:

  • 不要从TSQLRecord继承表,而是通过TSQLRecordVirtualTableAutoID继承表,如上所述,这是虚拟表的标准过程,参见虚拟内存表
  • 如果将表定义为TSQLRecord,则确保客户端将其自身模型的表属性设置为rCustomAutoID;
  • 如果将表定义为TSQLRecord,请确保客户端、服务端都将其自身模型的表属性设置为rCustomAutoID。
    第一种选择可以这样做:
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继承。只需按照本书所述来使用框架,参见虚拟内存表

  同样,此限制不适用于访问外部数据库的情况。