8. 访问外部SQL数据库

  我们的ORM RESTful框架能够通过一组通用单元和类访问许多数据库引擎。可以访问SQL和NoSQL引擎,这在ORM环境中是一个非常独特的特性。

  请记住介绍mORMot时的这张数据库层的图:

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

  框架将SQLite3作为其在服务器上的SQL核心,但是特有的机制允许访问任何远程数据库,并将这些表内容与框架的本地ORM表混合。由于SQLite3独特神奇的虚拟表机制,这些外部表可以在SQL语句中视为本地SQLite3表访问,甚至对于NoSQL引擎也是如此。

模式 引擎
SQL SQLite3, Oracle, NexusDB, MS SQL, Jet/MSAccess, FireBird, MySQL, PostgreSQL, IBM DB2, IBM Informix
NoSQL MongoDB, JSON或二进制持久性TObjectList
参见访问外部NoSQL数据库内存静态过程

  您可以混合使用数据库,如同样的mORMot ORM支持同时对分布在多个数据库的数据持久化,一些TSQLRecord作为快速内部SQLite3表或TObjectList,其它的在PostgreSQL数据库(与外部报告/ SAP引擎绑定),并整合MongoDB实例数据。

8.1. SynDB直接RDBMS访问

  可以通过SynDB.pas单元访问外部关系数据库管理系统(RDBMS)。之后,框架ORM能够通过mORMotDB.pas 单元访问它们。您也可以直接使用SynDB.pas单元,而与我们的ORM没有任何链接。

  当前处理数据访问的库列表如下:

驱动 SynDB单元 RDBMS引擎
SQLite3 SynDBSQLite3.pas 直接SQLite3访问(dll或链接到exe)
Oracle SynDBOracle.pas 直接Oracle访问(通过OCI)
OleDB SynOleDB.pas MS SQL, Jet/MSAccess或其他
ODBC SynDBODBC.pas MS SQL、FireBird、MySQL、PostgreSQL、IBM DB2、Informix或其他
ZeosLib SynDBZeos.pas MS SQL, SQLite3, FireBird, MySQL, PostgreSQL
DB.pas/
TDataset
SynDBDataset.pas NexusDB and databases supported by DBExpress, FireDAC, AnyDac, UniDAC, BDE...
HTTP SynDBRemote.pas 通过HTTP远程访问任何SynDB数据库

  这个列表还没完,可能在不久的将来继续完善。欢迎任何形式的帮助,因为按照现有模式实现一个新单元并不困难,您可以从现有的驱动程序(例如Zeos或Alcinoe库)开始,开源贡献总是受欢迎的!

  感谢我们的SynDB.pas类设计,实现SQLite3直接访问非常简单方便。它甚至被用于我们的回归测试,以实现独立的统一测试。

  添加了针对Oracle的专用的直接访问,因为Oracle所有可用的OleDB驱动程序(微软的和Oracle的)在处理BLOB方面都有问题,我们希望我们的客户端能轻量快速访问这个伟大的数据库。

  事实上,OleDB是一个很好的数据库访问选择,它具有良好的性能、本地Unicode以及许多可用的驱动程序。由于OleDB,我们能够访问几乎任何现有的数据库。与添加其他第三方Delphi库相比,服务端可执行文件中的代码开销也要小得多。我们将使用Microsoft或其它OleDB对每个驱动程序执行所有的测试和调试。

  自版本1.17以来,对ODBC层的直接访问已经包含在框架数据库单元中。它有更广泛的免费驱动(包括MySQL或FireBird),是OleDB的正式替代产品(下一个版本的MS SQL Server将只提供ODBC驱动,就像微软警告其客户的那样)。

  自1.18版以来,可以使用任何ZeosLib/ZDBC驱动快速直接访问RDBMS底层客户端库。因为ZDBC库不依赖于DB.pas,绕过了低效的TDataSet 组件,其性能非常高。ZDBC维护者做了很多优化,特别是与mORMot一起工作,这个库是与我们的框架一起工作的一流库。

  同样自1.18版本,DB.pas可以与我们的SynDB.pas一起使用。当然,使用TDataset 作为中间层会比SynDB.pas直接访问模式慢。但是它允许您复用任何现有(第三方)数据库连接驱动程序,这对存量应用程序的演化是有意义的,或者用于尚未支持的数据库引擎。

  最后但最重要的是,SynDBRemote.pas单元允许您创建在远程SynDB HTTP服务器上执行SQL操作的数据库应用,而不是在数据库服务器上。您可以像其他SynDB数据库一样创建连接,但是传输将通过HTTP进行,不需要在应用程序中安装数据库客户端,参见通过HTTP远程访问

  因此,下列连接是可行的:

SQLite3
Oracle
MS SQL
Sybase
PostgreSQL
Jet/Access
Firebird
MySQL
Advantage
Interbase
Informix
DB2
Paradox
NexusDB
ZDBC
Zeos
OleDB
ODBC
FireDAC
AnyDAC
DBExpress
UniDAC
BDE
any RDBMS
DB.pas
TDataset
SynDB
Remote
SynDB

  这个图在最新的层次上有点难理解,但是我想您已经得到了通用的分层设计,稍后它将被分割成更小的聚焦图。

8.1.1. 直接访问RDBMS引擎

  SynDB.pas单元有如下特点:

  • 通过OleDB、ODBC、ZDBC、Oracle (OCI)或SQLite3(静态链接或外部dll)直接快速访问;
  • 对基于DB.pas / TDataset的组件的轻量包装器(如NexusDB、DBExpress、FireDAC、AnyDAC、UniDAC、BDE…)
  • 通用抽象OOP方案,使用一组受限制的数据类型,但能够与任何基于SQL的数据库引擎一起工作;
  • 通过了MS SQL Server 2008/2012、Firebird 2.5.1、PostgreSQL 9.2/9.3、MySQL 5.6、IBM DB2 10.5、Oracle 11g以及最新的SQLite3引擎测试;
  • 可以使用任何Delphi版本(包括Delphi 7 personal、Turbo Explorer或Starter edition)访问任何本地或远程数据库,而且是免费的(事实上,它不使用DB.pas标准单位元及其所有依赖单元);
  • Unicode,包括Delphi以前的Unicode版本(如Delphi 7或2007),因为它在内部使用UTF-8编码;
  • 可以处理NULL或BLOB内容的参数和结果(包括存储过程);
  • 避免了很多的内存复制或不必要的内存分配:我们直接从检索到的数据缓冲区访问数据,就像从OleDB / ODBC或底层数据库客户端(例如Oracle的OCI,或SQLite3引擎)访问数据一样;
  • 为32位或64位Windows设计实现最佳性能:大部分时间花在数据库驱动程序(OleDB、ODBC、OCI、SQLite3)上,添加到数据库客户端的代码层非常轻量,并经过了优化;
  • 可以在多线程应用/服务器中安全使用(使用专用的线程安全方法,即使数据库客户端不是官方正式的多线程也可以使用);
  • 允许对就绪的请求进行参数绑定,快速访问任何参数或列名(多亏了TDynArrayHashed);
  • 大多数Delphi类型都可以访问列值,包括变体或泛型字符串/ WideString;
  • 可用的ISQLDBRows接口,避免键入try...finally Query.Free end;并允许单条SQL语句;
  • 后期绑定列访问,通过自定义变体类型访问结果集;
  • 两种优化的TDataSet结果集:一种是基于TClientDataSet的读写,另一种是更快的只读TSynSQLStatementDataSet
  • 直接创建UTF-8 JSON内容,不需要临时数据复制和内存分配(该特性将是我们基于JSON的ORM服务端中最常用的特性);
  • 高级目录/数据库设计抽象方法,支持在数据库反向工程中检索表和列属性(包括索引);还提供SQL语句以数据库抽象的方式创建表或索引,这些特性将被我们的ORM直接使用;
  • 设计用于ORM,但也可以单独使用(完整的Delphi 7客户端可执行文件只有大约200kb),也可以在任何现有的Delphi应用程序中使用,这要归功于一个类似TQuery的包装器;
  • TQuery仿真类,用于与现有代码直接重用,已替换为基于DB.pas的代码(包括过时的BDE技术),极大地提高了速度(因为我们绕过了极慢的TDataSet组件);
  • 通过HTTP对任何SynDB引擎进行快速和安全的远程访问,而不需要将RDBMS客户端库与应用程序一起部署;
  • 免费的SynDBExplorer工具,一个简单快速运行查询的用户界面,对所有支持的引擎,发布为服务端或作为SynDB客户端通过HTTP远程访问,它也是独立使用这些库的一个很好的示例程序。

8.1.2. 数据类型

  我们的ORM不需要完整的特性集(不要指望将这个数据库类用于您的VCL DB RAD组件),仅处理我们需要的基本的SQL列类型(派生自SQLite的内部列类型): NULL, Int64, Double, Currency, DateTime, RawUTF8BLOB

  它们在SyncDB .pas中定义如下:

  TSQLDBFieldType =
    (ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8, ftBlob);
TSQLDBFieldType 内容
ftNull 对应SQL的NULL值
ftInt64 64位整数
ftDouble 64位浮点数
ftCurrency 财务类型,最多4位小数(货币)
ftDate 日期和时间,对应Delphi的TDateTime类型
ftUTF8 Unicode文本,UTF-8编码,有或没有大小限制
ftBlob 二进制内容,存储为RawByteString

  这些类型将映射为底层数据库级访问类型,而不是像在mORMot.pas中定义的TSQLFieldType那样的高级Delphi类型,也不是标准VCLDB.pas单元中定义的泛型大型TFieldType 。事实上,它更依赖于标准的SQLite3泛型类型,即NULL、INTEGER、REAL、TEXT、BLOB(添加了ftCurrencyftDate类型,以更好地支持大多数DB引擎),请参见http://www.sqlite.org/datatype3.html。

  您可能注意到这里唯一处理的字符串类型使用UTF-8编码(使用我们的RawUTF8类型实现),用于跨delphi版本实现真正Unicode处理。代码可以通过variantstringwidestring变量和参数访问文本数据,但是我们的单元将在内部使用UTF-8编码,请参阅Unicode和UTF-8,因此,它将与使用同样编码的ORM直接接口。当然,如果某列在数据库中没有定义为Unicode文本,则需要在数据驱动级实现与对应字符集的相互转换;但是在用户相关代码中,您应该始终使用Unicode内容。

  BLOB列或参数作为RawByteString变量访问,这些变量通过TRawByteStringStream映射为标准TStream

8.1.3. 数据库类型

  除了原始数据访问,SynDB.pas单元还处理一些SQL级语句生成,并用于我们的ORM内核使用。

  以下RDBMS数据库引擎在SynDB.pas中的定义:

 TSQLDBDefinition = (dUnknown, dDefault, dOracle, dMSSQL, dJet,
   dMySQL, dSQLite, dFirebird, dNexusDB, dPostgreSQL, dDB2, dInformix);
TSQLDBDefinition 已测试的RDBMS
dDefault 遵循SQL-92标准的任何数据库
dOracle Oracle 11g
dMSSQL MS SQL Server 2008/2012
dJet Jet/MSAccess (仅支持Win32
dMySQL MySQL 5.6
dSQLite SQLite3 3.7.11及以上版本(我们为静态链接提供了最新版本)
dFirebird Firebird 2.5.1
dNexusDB NexusDB 3.11
dPostgreSQL PostgreSQL 9.2/9.3
dDB2 IBM DB2 10.5
dInformix IBM Informix 11.70

  以上版本已经经过测试,但是更新或更老的版本也可以工作。欢迎您的反馈,我们无法完成对数据库和客户端所有可能组合的测试!

  SynDB.pas单元能够生成上述引擎的SQL语句,CREATE TABLE / CREATE INDEX命令,检索元数据(如表和字段信息),正确计算SELECT的limit/offset语法、计算多条INSERT语句,检查SQL关键字,定义特定的schema/owner命名约定,处理日期和时间值,处理错误和异常,甚至创建一个数据库。

8.1.4. SynDB单元

  以下是实现外部数据库无关特性的单元:

文件 描述
SynDB.pas 抽象的数据库直接访问类
SynOleDB.pas OleDB直接访问类
SynDBODBC.pas ODBC直接访问类
SynDBZeos.pas ZDBC直接访问类
SynDBOracle.pas Oracle DB直接访问类(通过OCI)
SynDBSQLite3.pas SQLite3直接访问类
SynDBDataset.pas TDataset (DB.pas)访问类
SynDBFireDAC.pas TDataset (DB.pas)访问类
SynDBUniDAC.pas TDataset (DB.pas)访问类
SynDBNexusDB.pas TDataset (DB.pas)访问类
SynDBBDE.pas TDataset (DB.pas)访问类
SynDBRemote.pas HTTP远程访问
SynDBVCL 只读的TSynSQLStatementDataSet结果集
SynDBMidasVCL 可读写的TClientDataSet结果集

  值得注意的是,这些单元只依赖于SynCommons.pas,它们独立于框架的ORM部分(甚至远程访问)。它们可以单独使用,使用普通SQL代码访问所有这些外部数据库。它们的所有类都继承自SynDB.pas中定义的抽象类,所以从一个数据库引擎切换到另一个数据库引擎(即便是远程HTTP访问)只是更改一个类型的问题。

8.1.5. SynDB类

  数据通过三大类访问:

  • 连接属性,存储数据库高级属性(如数据库实现类、服务器和数据库名称、用户名和密码);
  • 连接,根据指定的连接属性实现到远程数据库的实际连接,对于相同的连接属性实例,可以有多个连接;
  • 语句,这些语句是单个SQL查询或请求,对于一个现有连接可能有很多。

  在实践中,您定义一个TSQLDBConnectionProperties实例,然后使用专用的NewConnection / ThreadSafeConnection / NewStatement方法生成TSQLDBConnection and TSQLDBStatement实例。

  以下是所有可用远程连接属性的通用类层次结构:

TOleDBMSOracleConnectionProperties
TOleDBMSSQL2012ConnectionProperties
TOleDBMSSQL2005ConnectionProperties
TSQLDBFireDACConnectionProperties
TSQLDBUniDACConnectionProperties
TSQLDBBDEConnectionProperties
TOleDBOracleConnectionProperties
TOleDBODBCSQLConnectionProperties
TOleDBMySQLConnectionProperties
TOleDBMSSQLConnectionProperties
TOleDBJetConnectionProperties
TOleDBAS400ConnectionProperties
TSQLDBCurlConnectionProperties
TSQLDBWinHTTPConnectionProperties
TSQLDBWinINetConnectionProperties
TSQLDBZEOSConnectionProperties
TSQLDBOracleConnectionProperties
TSQLDBDatasetConnectionProperties
TOleDBConnectionProperties
TODBCConnectionProperties
TSQLDBNexusDBConnectionProperties
TSQLDBSocketConnectionProperties
TSQLDBHttpRequestConnectionProperties
TSQLDBSQLite3ConnectionProperties
TSQLDBConnectionPropertiesThreadSafe
TSQLDBHTTPConnectionPropertiesAbstract
TSQLDBConnectionProperties

  这些类是SynDB.pas的根类,大部分数据库处理将通过该单元实现。mORMot框架的ORM只需要给定的TSQLDBConnectionProperties实例就可以访问任何外部数据库。

  然后定义了以下连接类:

TODBCConnection
TOleDBConnection
TSQLDBBDEConnection
TSQLDBOracleConnection
TSQLDBFireDACConnection
TSQLDBUniDACConnection
TSQLDBZEOSConnection
TSQLDBNexusDBConnection
TSQLDBSQLite3Connection
TSQLDBProxyConnection
TSQLDBConnectionThreadSafe
TSQLDBConnection

  每个连接可以创建一个对应的语句实例:

TSQLDBDatasetStatementAbstract
TSQLDBUniDACStatement
TSQLDBBDEStatement
TSQLDBNexusDBStatement
TSQLDBZEOSStatement
TSQLDBOracleStatement
TSQLDBDatasetStatement
TODBCStatement
TSQLDBStatementWithParams
TSQLDBSQLite3Statement
TOleDBStatement
TSQLDBProxyStatement
TSQLDBStatement
TSQLDBStatementWithParamsAndColumns
TSQLDBFireDACStatement

  在上面的层次结构中,TSQLDBDatasetStatementAbstract用于自定义类参数处理,例如FireDAC的TADParams (它以数组DML为特征)。

  还定义了一些专用的异常类:

EODBCException
EOleDBException
ESQLDBZEOS
ESQLDBRemote
ESQLDBBDE
ESQLDBFireDAC
ESQLDBUniDAC
ESQLDBException
ESQLDBOracle
ESQLQueryException
ESQLDBDataset
Exception

  查看TestOleDB.dpr示例程序,位于SQlite3文件夹中,使用我们的SynOleDB 单元连接到本地的MS SQL Server 2008 R2 Express edition,该版本将使用Person.Address表的JSON表示写入AdventureWorks2008R2示例数据库文件。

8.1.6. ISQLDBRows接口

  最简单的是保持在TSQLDBConnectionProperties级别,使用这个实例的Execute() 方法,并通过ISQLDBRows接口访问所有返回的数据。它将以抽象的方式自动建立到数据库的线程安全的连接。

  SynDB.pas的典型用法包括:

  • 初始化一个共享的TSQLDBConnectionProperties实例;
  • 直接从这个实例的Execute*()方法执行语句。

  定义数据库连接非常简单:

var Props: TSQLDBConnectionProperties;
 ...
  Props := TOleDBMSSQLConnectionProperties.Create('.\\SQLEXPRESS','AdventureWorks2008R2','','');
  try
    UseProps(Props);
  finally
    Props.Free;
  end;

  根据TSQLDBConnectionProperties子类的不同,输入参数也有所不同。请参阅文档中每个类的Create() constructor,以便按照要求设置所有参数。

  然后,任何子代码都可以执行任何SQL请求,具有可选的绑定参数,如下所示:

procedure UseProps(Props: TSQLDBConnectionProperties);
var I: ISQLDBRows;
begin
  I := Props.Execute('select * from Sales.Customer where AccountNumber like ?',['AW000001%']);
  while I.Step do
    assert(Copy(I['AccountNumber'],1,8)='AW000001');
end;

  在这个过程中,没有定义TSQLDBStatement,也不需要添加try ... finally Query.Free; end;语句块。

  事实上,MyConnProps.Execute方法将以ISQLDBRows的形式返回一个TSQLDBStatement实例,可以使用这些方法遍历结果行,并检索各个列值。在上面的代码中,I['FirstName']实际上会调用I.Column[]默认属性,该属性将以变体形式返回列值。您还有其他专用的方法,如ColumnUTF8ColumnInt,可以直接检索需要的数据。

  注意,当使用我们的TSynLog类记录日志时,所有绑定参数都将出现在SQL语句中。

8.1.7. 正确使用ISQLDBRows接口

  您可能已经注意到,在前面的代码示例中,我们使用了UseProps()子过程。这是有目的的。

  我们可以这样写一个小的测试:

var Props: TSQLDBConnectionProperties;
    I: ISQLDBRows;
 ...
  Props := TOleDBMSSQLConnectionProperties.Create('.\\SQLEXPRESS','AdventureWorks2008R2','','');
  try
    I := Props.Execute('select * from Sales.Customer where AccountNumber like ?',['AW000001%']);
    while I.Step do
      assert(Copy(I['AccountNumber'],1,8)='AW000001');
  finally
    Props.Free;
  end;
end;

  事实上,您不应该使用这种模式。这段代码将在运行时导致意外的访问冲突。

  在后台,编译器会生成一些隐含的代码来释放I: ISQLDBRows局部变量,如下所示:

 ...
  finally
    Props.Free;
  end;
  I := nil; // this is generated by the compiler, just before the final "end;"
end;

  因此,在Props 实例之后释放ISQLDBRows,将导致访问冲突。

  正确的写法是使用一个子函数(函数离开时释放局部ISQLDBRows),或者显式地释放接口变量:

    while I.Step do
      assert(Copy(I['AccountNumber'],1,8)='AW000001');
  finally
    I := nil; // release local variable
    Props.Free;
  end;

  当然,大多数情况下,您将全局初始化进程的TSQLDBConnectionProperties,然后在进程结束时释放它。每个请求都将在它自己的子方法中处理,因此将在TSQLDBConnectionProperties主实例之前释放。

  最后但最重要的是,您不应该在每次需要访问数据库时才创建TSQLDBConnectionProperties实例,这样您可能会丢失很多SynDB特性,比如每个线程连接池或语句缓存。

8.1.8. 后期绑定

  我们通过自定义变体时间实现了列值的后期绑定访问。它使用了Ole自动化的内部机制,访问列内容,就像访问本地对象属性所在的列名一样。

  查看Delphi代码就很清楚和明显:

procedure UseProps(Props: TSQLDBConnectionProperties);
var Row: Variant;
begin
  with Props.Execute('select * from Sales.Customer where AccountNumber like ?',
    ['AW000001%'],@Row) do
    while Step do
      assert(Copy(Row.AccountNumber,1,8)='AW000001');
end;

  Props.Execute返回ISQLDBRows接口,因此上面的代码将初始化(或复用已有的)线程安全连接(OleDB使用每个线程模型),初始化一条语句,执行它,通过Step方法和Row变体访问行,通过Row.AccountNumber语句直接行检索列值。

  上述代码是完全安全的,所有内存都将通过ISQLDBRows接口的引用计数垃圾回收特性释放。您不需要添加任何try..finally Free; end代码。

  这就是神奇的Delphi后期绑定。我们的SynBigTable单元也提供了类似的特性。

  实际上,这段代码比使用标准属性的访问要慢,如下所示:

while Step do
  assert(Copy(ColumnUTF8('AccountNumber'),1,8)='AW000001');

  但是前一个版本使用了列名的后期绑定,这看起来更自然。

  当然,由于是后期绑定,编译器在编译时无法检查列名,如果源代码中的列名是错误的,则运行时才会触发错误。

  首先,让我们看看访问行内容的最快方式。

  在所有情况下,使用列名的文本版本('AccountNumber')比直接使用列索引要慢,即使我们的SynDB.pas库使用散列快速查找,下面的代码总是更快:

var Customer: Integer;
begin
  with Props.Execute(
    'select * from Sales.Customer where AccountNumber like ?',
    ['AW000001%'],@Customer) do begin
    Customer := ColumnIndex('AccountNumber');
    while Step do
      assert(Copy(ColumnString(Customer),1,8)='AW000001');
  end;
end;

  但是,老实说,在分析之后,发现大部分时间都花在了Step方法上,尤其是fRowSet.GetData。实际上,使用上面的代码,我没有发现任何值得一提的速度增长。

  我们通过散列函数(即TDynArrayHashed)查找名称的目标非常好。

  相反,在分析之后发现基于Ole自动化的后期绑定要慢一些。事实上,Row.AccountNumber表达式调用了一个隐含的DispInvoke函数,该函数在多次调用时很慢。我们的SynCommons.pas单元能够深入VCL,并通过修补内存中的VCL代码,调用该函数的优化版本,其结果是速度非常接近直接的Column['AccountNumber']调用。

8.1.9. TDataset和SynDB

  因为我们的SynDB.pas单元不依赖于Delphi的DB.pas单元,其结果集不从TDataset继承。

  这样做的好处是,当从对象代码访问这些结果集时,它们会快得多。

  但缺点是,您不能在通常的VCL应用中使用它们。

  为了便于SynDB.pas与VCL组件的使用,可以从任何SynDB查询创建TDataSet结果集。

  您可以使用两种优化的TDataSet结果集:

TDataSet 操作 单元 备注
TClientDataSet 读写 SynDBMidasVCL.pas 由于内存复制而变慢
TSynSQLStatementDataSet 只读 SynDBVCL.pas 直接映射,速度很快

  因此,您可以将SynDB请求的结果赋给任何TDataSource,例如我们的TSynSQLStatementDataSet快速只读存储:

  ds1.DataSet.Free; // release previous TDataSet
  ds1.DataSet := ToDataSet(ds1,aProps.Execute('select * from people',[]));

  或TClientDataSet类型的内存存储:

  ds1.DataSet.Free; // release previous TDataSet
  ds1.DataSet := ToClientDataSet(ds1,aProps.Execute('select * from people',[]));

  参见示例“17 - TClientDataset use”了解更多关于使用此类TDataSet的信息,包括一些速度信息。您需要先运行TestSQL3.dpr回归测试,获得所需的SQlite3数据文件。

8.1.10. TQuery仿真类

  SynDB.pas单元提供了一个类似TQuery的类。这个类模拟通常的TQuery类,没有继承DB.pas及其缓慢的TDataSet

  它模仿了基本的TQuery VCL方法,具有以下优点:

  • 没有从TDataset继承,但在SynDB.pas ISQLDBStatement结果集上有自己的轻量级实现,因此通常要快得多;
  • 在使用名称访问字段和参数时更快,甚至快过索引;
  • Unicode支持,即使是使用Delphi以前的unicode版本,也能够独立于当前系统字符集以WideString的形式返回数据;
  • 您仍然可以通过在SynDBVCL.pas中定义的ToDataSet()函数从SynDBTQuery创建TDataSet

  当然,由于它不是TDataSet组件,所以不能直接使用它作为RAD代码的简单替代。

  但是,如果您的应用是以数据为中心,并且计划用一些类封装其业务逻辑,例如,如果它准备正确地实现OOP,而不是RAD,您仍然可以直接用TQuery仿真器替换现有代码:

  Q := TQuery.Create(aSQLDBConnection);
  try
    Q.SQL.Clear; // optional
    Q.SQL.Add('select * from DOMAIN.TABLE');
    Q.SQL.Add('  WHERE ID_DETAIL=:detail;');
    Q.ParamByName('DETAIL').AsString := '123420020100000430015';
    Q.Open;
    Q.First;    // optional
    while not Q.Eof do begin
      assert(Q.FieldByName('id_detail').AsString='123420020100000430015');
      Q.Next;
    end;
    Q.Close;    // optional
  finally
    Q.Free;
  end;

  您应该使用TSQLDBStatement而不是这个包装器,但是使用这种TQuery兼容代码可以简化一些现有代码的升级,特别是对于遗留代码和现有项目。

  它将有助于避免使用弃用过时的BDE,生成更小的可执行文件,访问各种数据库而无需支付大笔费用,避免对一个很大的遗留应用重写大量的现有代码行,或者让你的旧应用与数据库通过普通的HTTP通信,而不需要安装任何RDBMS客户端。

8.1.11. 连接属性存储为JSON

  您可以使用TSynConnectionDefinition将连接属性作为JSON内容保存在内存或文件中。

  典型的存储内容可能是这样的:

 {
   "Kind": "TSQLDBSQLite3ConnectionProperties",
   "ServerName": "database.db3",
   "DatabaseName": "",
   "UserID": "",
   "Password": "PtvlPA=="
 }

  “Kind”参数将用于存储实际的TSQLDBConnectionProperties类名。因此,通过修改存储在JSON文本文件中的配置(无需重新编译应用程序),就可以在运行时轻松地从一个数据库切换到另一个数据库。注意,SynDB*的实现类单元应该编译到可执行文件中,如SynDBSQLite3.pas用于TSQLDBSQLite3ConnectionPropertiesSynDBZeos.pas用于TSQLDBZeosConnectionProperties

  要从本地JSON文件创建一个新的TSQLDBConnectionProperties实例,只需编写:

var Props: TSQLDBConnectionProperties;
 ...
  Props := TSQLDBConnectionProperties.CreateFromFile('localDBsettings.json');

  为安全起见,密码被加密并编码为Base64,您可以使用TSynConnectionDefinitionPasswordPasswordPlain属性来计算写到磁盘上的值。

  因为TSynConnectionDefinition是一个TSynPersistent类,所以可以将它嵌套到应用程序中包含所有设置的TSynAutoCreateFields实例中。

  然后mORMot.pasObjectToJSON/ObjectToJSONFileJSONToObject/JSONFileToObject函数就可以将这些全局设置持久化到文件或数据库中。

  有关ORM/REST级上的类似特性,请参见TSQLRest.CreateFrom(),以及mORMotDB.pas中定义的函数function TSQLRestCreateFrom( aDefinition: TSynConnectionDefinition),它创建一个普通的本地ORM的,如果aDefinition.Kind是一个TSQLRest类名;也可使用外部数据库存储的ORM,如果aDefinition.KindTSQLDBConnectionProperties类名。

8.2. SynDB客户端

  从SynDB.pas的逻辑视角来看,以下是数据库是如何访问的:

ZDBC
ODBC
OleDB
Oracle
SQLite3
DB.pas
TDataset
NexusDB
BDE
DBExpress
FireDAC
AnyDAC
UniDAC
SynDB

  当然,正如SynDB架构中所述,其实物理实现很复杂。
  现在我们将详细介绍这些数据库连接如何实现SynDB.pas接口。

8.2.1. OleDB或ODBC的统一管理

  OleDB(对象链接和嵌入数据库,也写成OLE DB或OLE-DB)是一种由Microsoft设计的API,用于统一访问来自各种数据源的数据。

Oracle
MS SQL
Jet/Access
MySQL
PostgreSQL
Interbase
Firebird
Sybase
SynDB
OleDB

  当然,您可以使用Microsoft SQL Native客户端访问MS SQL Server 2005/2008/2012,Oracle也提供了一个本地OleDB驱动程序(但我们发现无论Oracle驱动程序,还是Microsoft版本,在blob方面都有问题)。不要忘记还有Advantage Sybase OleDB驱动程序等等…

  如果你想连接到MS SQL Server,我们强烈推荐使用TOleDBMSSQL2012ConnectionProperties类,对应SQLNCLI11,Microsoft®SQL Server®2012本地客户端的一部分,它可以连接到各种MS SQL Server修订版(包括MS SQL Server 2008),这样更稳定。你可以从http://www.microsoft.com/en-us/download/details.aspx?下载操作系统对应的sqlncli.msi。大多数情况下,您应该下载X64的sqlncli.msi,该安装包也支持安装32位版本的SQL Server Native Client,因此也适用于32位的Delphi可执行文件,X86包只适用于32位Windows系统。

  ODBC (Open DataBase Connectivity)是一种用于访问数据库管理系统(DBMS)的标准C语言中间件API。ODBC最初是在20世纪90年代初由Microsoft开发的,后来被OleDB取代。最近,微软正式宣布不支持OleDB,并敦促所有开发人员改用开放的、跨平台的ODBC API进行本地连接。又一次来自Micro$oft的糟糕的回退战略!
http://blogs.msdn.com/b/sqlnativeclient/archive/2011/08/29/microsoft-is-aligning-with-odbc-for-native-relational-data-access.aspx

Oracle
MS SQL
MySQL
DB2
Infomix
PostgreSQL
Interbase
Firebird
Sybase
Jet/Access
Advantage
SynDB
ODBC

  通过使用我们自己的OleDB和ODBC实现,我们将能够直接将OleDB或ODBC二进制行转换为JSON,而不需要临时转换为Delphi高级类型(如临时字符串或变体分配)。由于我们绕过了BDE/dbExpress/FireDAC/AnyDAC组件集引入的许多层,因此性能要比使用标准TDataSet或其他组件高得多。

  大多数OleDB / ODBC驱动是免费的(包括由数据库所有者维护的),少数驱动提供商需要付费许可证。

  值得指出的是,用于mORMot客户端-服务端架构时,OleDB或ODBC远程访问的对象持久性更希望实现服务器端的数据库实例访问。客户端可以通过标准HTTP进行通信,因此不需要任何特定的端口转发或其他IT配置就可以正常工作。

8.2.2. 基于ZDBC的ZEOS

8.2.2.1. mORMot最好的伙伴

  ZeosLib,又名Zeos,是一个开放源码库,它为Delphi、Kylix和Lazarus / FreePascal开发,提供许多数据库系统的本地访问。

  它是完全面向对象的,具有完全模块化的设计。它通过包装本地客户端库来连接数据库,并通过抽象层ZDBC来访问它们。最初,ZDBC是JDBC 2.0(Java数据库连接API)到对象Pascal的一个端口。从那时起,API略有扩展,但主要思想没有改变,所以正式的JDBC 2.0规范是ZDBC API的主要切入点。

  最新的7.x分支进行了深度重构,引入了新的方法和性能优化。事实上,我们与Michael (ZeosLib的主要贡献者)一起工作,以确保达到最优性能,所以mORMot和ZeosLib在读取或写入数据方面产生了令人印象深刻的协同作用。

  自框架的1.18版本以来,我们将ZeosLib直接集成到mORMot持久层,并直接访问ZDBC层。也就是说,我们的SynDBZeos单元不引用DB.pas单元,但可以直接访问ZDBC接口。

Oracle
MS SQL
MySQL
PostgreSQL
Interbase
Firebird
Sybase
SQLite3
SynDB
ZDBC

  这种直接访问,绕过了VCL的DB.pas层及TDataSet瓶颈,非常接近我们的SynDB.pas设计。因此,ZeosLib是mORMot的首选类库。SynDBZeos单元也成为外部SQL数据库访问的优选。

8.2.2.2. 版本推荐

  我们建议您下载Zeos/ZDBC的7.2分支,它是撰写本文时的trunck分支。

  Zeos/ZDBC作者已经进行了深入的代码重构(非常感谢Michael,又名EgonHugeist!),他甚至考虑到了mORMot的需要,以提供最佳的性能和集成,如UTF-8内容处理。

  与之前的7.1版本相比,速度可以提高10倍以上,具体取决于数据库的后端和用例!

  在写入数据(即Add/Update/Delete操作)时,已经向Zeos/ZDBC 7.2分支添加了数组绑定支持,我们的SynDBZeos单元将通过检测IZDatabaseInfo.SupportsArrayBindings属性是否为true来使用它,目前Oracle和FireBird驱动程序就是这样。当按批处理模式处理时,我们的ORM从中受益,包括允许ZDBC创建优化的SQL。

  在检索单条记录情况下,读取性能非常高,远远高于其它基于DB.pas的数据库。如TSQLDBZEOSStatement.ColumnsToJSON()将避免很多临时内存分配,并能够直接从底层ZDBC二进制缓冲区创建JSON。

  如果你需要停留在7.2版本前,并希望使用SQlite3后端(但是你没有理由这样做,因为Zeos与SynDBSQlite3相比会很慢),对于Zeos<7.2的TZSQLiteCAPIPreparedStatement.ExecuteQueryPrepared()TZSQLiteResultSet.FreeHandle方法,您需要应用一些补丁,正如SynDBZeos.pas开头的注释所述。

8.2.2.3. 连接示例

  如果你想通过Zeos/ZDBC连接MySQL,请遵循以下步骤:

  • 从http://dev.mysql.com/downloads/connector/c下载“Windows (x86, 32位),ZIP Archive”,然后解压文档:仅需要libmysql.dll,将其放入可执行文件夹中,或者放在系统路径中;
  • 像平常一样连接:
 fConnection := TSQLDBZEOSConnectionProperties.Create(
   'zdbc:mysql://192.168.2.60:3306/world?username=root;password=dev', '', '', '');
  • 或者使用URI()方法:
 fConnection := TSQLDBZEOSConnectionProperties.Create(
   TSQLDBZEOSConnectionProperties.URI(dMySQL,'192.168.2.60:3306'),'root','dev');

  对于PostgreSQL,Zeos驱动程序只需要libpq.dlllibintl.dll,如从http://www.enterprisedb.com/products.services-training/pgbindownload下载。

 PropsPostgreSQL := TSQLDBZEOSConnectionProperties.Create(
   TSQLDBZEOSConnectionProperties.URI(dPostgreSQL,'localhost:5432'),
   'dbname','username','password');

  之后您可以使用TSQLDBZEOSConnectionProperties.URI()方法来生成需要的ZDBC连接字符串:

 PropsOracle := TSQLDBZEOSConnectionProperties.Create(
   TSQLDBZEOSConnectionProperties.URI(dOracle,'','oci64\oci.dll'),
   'tnsname','user','pass');
 PropsFirebirdEmbedded := TSQLDBZEOSConnectionProperties.Create(
   TSQLDBZEOSConnectionProperties.URI(dFirebird,'','Firebird\fbembed.dll')
   'databasefilename','',');
 PropsFirebirdRemote := TSQLDBZEOSConnectionProperties.Create(
   TSQLDBZEOSConnectionProperties.URI(dFirebird,'192.168.1.10:3055',
     'c:\Firebird_2_5\bin\fbclient.dll',false),
  '3camadas', 'sysdba', 'masterkey');

  请参阅TSQLDBZEOSConnectionProperties文档,了解这些语法的更多信息,以及这个伟大的开源库的可用功能。

8.2.3. 基于OCI的Oracle访问

  对于我们的框架,在实现SynDBZeosSynOleDBSynDBODBC单元的同时,也实现了SynDBOracle单元。它允许使用Oracle Call Interface直接访问任何远程Oracle服务器。

  Oracle Call Interface (OCI)是面向Oracle数据库的最全面、高性能、本机非托管的接口,它公开了Oracle数据库的全部功能。直接接入oci.dll库的接口是使用我们在SynDB.pas中引入的DB抽象类编写的。

  我们努力实现Oracle参考文档中关于Building High Performance Drivers for Oracle详细介绍的所有最佳实践模式。

  得到的速度相当惊人:对于所有请求,SynDBOracle的速度是使用Oracle提供的本地OleDB驱动程序的SynOleDB连接的3到5倍。与Oracle的官方ODBC驱动程序(基于SynDBODBC的连接)相比时,也发现了类似(甚至更糟糕)的速度损失。有关更详细的数字,请参见数据访问基准测试

8.2.3.1. 优化客户端库

  值得指出的是,使用mORMot客户端-服务端架构时,Oracle数据库对象持久性只需要在服务器端访问Oracle实例,就像使用OleDB或ODBC一样。

  以下是SynDBOracle单元的主要特点:

  • 直接访问Oracle Call Interface (OCI)客户端,无需BDE、Midas、DBExpress及OleDB/ODBC等驱动程序;
  • 从版本8开始,支持与Oracle OCI interface的各种版本协同工作;
  • 针对Oracle 11g/12c的最新特性进行了优化(如使用本地Int64检索不带小数的NUMBER字段);
  • 能够免安装Oracle客户端,无需安装应用程序(通过文件/文件夹复制安装);
  • 原生Unicode(使用内部UTF-8编码),适用于所有版本的Delphi,并对每个数据库字符集进行特殊处理;
  • 努力实现Oracle客户端各个版本的最佳性能;
  • 适用于32位或64位架构的各种Windows版本(但OCI库必须使用与已编译的Delphi应用程序相同的版本,即当前版本仅支持32位);
  • 使用新的专用变体类型(类似于Ole自动化运行时属性)对列名进行后期绑定访问;
  • 连接是多线程的,具有较低的内存和CPU资源消耗;
  • 可以使用'//host[:port]/[service_name]'这样的连接字符串,避免使用TNSNAME.ORA文件;
  • 使用行数组和BLOB获取,以获得最佳性能(ZEOS/ZDBC还没有这样处理);
  • 根据我们的实验,在客户端和服务端同时进行准备语句处理,如果可能的话,服务端缓存可以提高3倍的速度;
  • 实现数组绑定,用于高速的批量修改,一次插入、更新或删除大量行;
  • 实现绑定TInt64DynArrayTRawUTF8DynArray作为参数,如用于SELECT .. INwhere子句;
  • 游标支持,常用于处理存储过程和遗留代码。

  当然,该单元与外部SQL数据库访问过程完美集成。如具有本地导出JSON方法的特性,这是ORM框架的主要输入,直接在批处理序列处理数组绑定。

8.2.3.2. 无需安装客户端的直接连接

  您可以使用Oracle提供的Oracle Instant Client (OIC)的最新版本,参见http://www.oracle.com/technetwork/database/features/instant-client,它允许在不安装标准巨大的Oracle客户机和配置ORACLE_HOME的情况下运行客户端应用程序。

TCP/IP
TCP/IP
TCP/IP
RAD Application
DBExpress
or BDE
installed
Oracle Client
Oracle Server
mORMot Application
installed
Oracle Client
Oracle Server
mORMot Application
with OIC dlls
Oracle Server

  只需在应用程序(可能是一个mORMot服务器)目录放入几个dll文件,它就可以以惊人的速度工作,并具有Oracle的所有特性(其他独立的直接Oracle访问库依赖于已废弃的Oracle 8协议)。

8.2.3.3. Oracle Wallet支持

  译者注:Oracle Wallet,Oracle从10gR2开始提供了wallet,用于解决用户认证信息(用户名和密码)的存放问题,通过使用wallet可以实现无密码登录数据库,这样一来就不需要在应用程序中嵌入数据库密码(或者在配置文件中明文存放密码),同时更加方便维护大量服务器的环境的数据库密码维护工作,因为可以直接分发wallet文件,实现批量修改密码。

  连接数据库的密码凭据现在可以存储在客户端Oracle Wallet中,这是一个用于存储身份验证和签名凭据的安全软件容器。

  这种wallet的使用可以简化依赖密码凭据连接数据库的大规模部署。配置此功能后,应用程序代码、批处理作业和脚本不再需要嵌入用户名和密码。风险降低了,因为这样的密码不再显示地公开,而且在用户名或密码更改时无需更改应用程序代码,密码管理策略更容易执行。

  为了使用这个特性,在连接数据库之前设置TSQLDBOracleConnectionProperties.UseWallettrue

  Wallet配置在服务端运行的计算机上执行。您必须执行一个完整的Oracle客户端安装:OIC,无需安装客户端的直接连接是不允许访问wallet身份验证的。

  创建Wallet的步骤:

  1) 为wallet创建一个文件夹:

> mkdir c:\OraWallets

  2) 在命令行使用以下语法在客户端创建wallet:

> mkstore -wrl c:\OraWallets -create

  Oracle会询问设置wallet主密码,一定要记住!

  3) 在命令行使用以下语法在wallet中创建数据库连接凭证:

 mkstore -wrl c:\OraWallets -createCredential TNS_alias_name_from_tnsnames_ora username password

  其中password是数据库用户的密码。Oracle将要求您输入wallet密码,使用前面步骤中的主密码。

  4) 在客户端sqlnet.ora文件中,添加WALLET_LOCATION参数并将其设置为wallet的目录,并设置SQLNET.WALLET_OVERRIDE参数为TRUE

SQLNET.WALLET_OVERRIDE = TRUE
WALLET_LOCATION =
  (SOURCE =
    (METHOD = FILE)
    (METHOD_DATA =
  (DIRECTORY = c:\OraWallets)
  )
)

  您不能在数据库有wallet时删除它,您需要通过以下命令删除wallet凭证:

mkstore -wrl wallet_location -deleteCredential db_alias

  Oracle将要求输入wallet密码,使用与创建wallet时相同的密码。

  请注意,如果您更喜欢使用GUI工具进行数据库管理,那么在数据库发行版中还可以使用Oracle Wallet Manager工具。
  参见https://docs.oracle.com/cd/B28359_01/network.111/b28530/asowalet.htm

8.2.4. SQLite3

  我们的ORM框架封装了一个高效的SQLite3,可以静态(即在exe中)连接SQLite3引擎,也可以从外部SQLite3.dll连接SQLite3引擎。

  从我们的SynDB.pas数据库抽象类调用SynSQLite3.pas单元很容易。添加另一个这样的数据库只是一个非常轻量的层,它通过SynDBSQLite3.pas单元实现。

  如果希望将SQLite3引擎链接到项目可执行文件,请确保您的uses子句引用了SynSQLite3Static.pas单元。否则,定义一个TSQLite3LibraryDynamic实例来加载外部sqlite3.dll库:

 FreeAndNil(sqlite3); // release any previous instance (e.g. static)
 sqlite3 := TSQLite3LibraryDynamic.Create;

  要创建到已存在的SQLite3数据库文件的连接属性,请调用TSQLDBSQLite3ConnectionProperties.Create构造函数,将实际的SQLite3数据库文件作为ServerName参数,可以在Password中选择使用加密密码(自1.16之后版本可用),而忽略其它参数(DataBaseNameUserID)。

  这些类将实现一个内部语句缓存,就像用于TSQLRestServerDB的那样。在实践中,使用缓存可以使处理速度提高两倍(在处理小的请求时)。

  在mORMot ORM中您有两种访问SQLite3引擎的方法:

  • 要么直接从ORM核心;
  • 或者是外部虚拟表。
mORMot ORM
SynDB
SQLite3

  如果您基于mORMot的应用仅是使用一个集中的SQLite3数据库,那么使用SynDBSQLite3外部表是没有意义的。但是,如果您希望将来能够连接到任何外部数据库,或者将数据分割到几个数据库文件中,那么使用外部SQLite3表就很有意义。当然,SQlite3引擎库本身将与内部和外部进程共享。

8.2.5. DB.pas库

  自框架的1.18修订版,引入了一个新的SynDBDataset.pas单元,支持各种基于DB.pas的数据库与我们的SynDB.pas类对接,并使用TDataset获取结果。由于TDataset的设计,相对于直接d的SynDB.pas连接(例如SQLite3或Oracle的结果),性能有所下降,但它也打开了各种可能的数据库访问。

  在mORMot源代码存储库的SynDBDataset子文件夹中已经发布了一些专用的驱动程序。到目前为止,已对接FireDAC(以前是AnyDAC)、UniDAC和BDE库,并且可以直接连接到NexusDB引擎。

  由于这里有许多可能的组合(请参阅[SynDB架构),所以欢迎反馈。由于我们的敏捷过程,我们将首先关注于我们需要和使用的驱动,这取决于mORMot用户要求的额外特性,并在可能的情况下提供包装器,或者至少提供测试功能,包括Embarcadero刚刚收购了AnyDAC并对其进行了改进并重新命名为FireDAC,以使其成为新的官方平台,DBExpress也将受益于该整合。

8.2.5.1. NexusDB访问

  NexusDB是一个“royalty-free、兼容SQL:2003核心、客户端/服务端和嵌入式数据库系统,具有与其他大量许可产品相竞争的特性”(供应商说的)。

  译者注:royalty-free,免版税。版税制是国际出版业通行的使用作品的支付方式,近年来我国也在逐渐实行。具体地讲,它是指著作权人因他人使用其作品而获得的一定货币份额。版税率一般多在6%到10%之间。以出版版税为例。出版版税的计算方法是,图书单价×图书印数或销量×版税率。RF是Royalty-Free的缩写,直接翻译成中文是“免版税”。用户在网上购买下载的图片,不需要按照其使用图片取得的收入的一定比例来向作者支付“版税”。而是一次性支付小额的使用费,便可以获得作者的授权使用。所以我们成为“RF授权”,具有“一次购买,多次使用”的特点。

SynDB
DB.pas
TDataset
NexusDB

  我们使用并测试了免费的嵌入式版本,它非常适合像mORMot这样的客户端-服务端ORM框架,请参见http://www.nexusdb.com/support/index.php

8.2.5.2. FireDAC/AnyDAC库

  FireDAC是一组独特的通用数据访问组件,用于在Delphi上开发跨平台数据库应用程序。这实际上是一个第三方组件集,由Embarcadero从DA-SOFT Technologies收购(以前称为AnyDAC),并包含在Delphi XE3自后版本中。这是Delphi中用于高速数据库开发的新官方平台,支持现在已被弃用的DBExpress。

SynDB
DB.pas
TDataset
FireDAC
AnyDAC
SQLite3
ODBC
Oracle
MS SQL
MySQL
DB2
Postgresql
Interbase
Firebird
Sybase
Jet/Access
Advantage

  我们已经优化了SynDB.pas集成单元和mORMot持久层。如您可以通过ORM批处理过程,通过所谓的数组绑定,直接访问高速的FireDAC数组DML特性。

8.2.5.3. UniDAC库

  通用数据访问组件(UniDAC)是一个跨平台的组件库,提供Delphi对多个数据库的直接访问。参见http://www.devart.com/unidac

Oracle
MS SQL
MySQL
DB2
Postgresql
Sybase
Jet/Access
Advantage
Interbase
Firebird
NexusDB
SQLite3
SynDB
DB.pas
TDataset
UniDAC
OleDB
ODBC

  例如,要访问MySQL远程数据库,您应该使用:

PropsMySQL := TSQLDBUniDACConnectionProperties.Create(
  TSQLDBUniDACConnectionProperties.URI(dMySQL,'192.168.2.60:3306'),
  'world', 'root', 'dev');

  与FireDAC相比,这个库提供了相当稳定的结果,但是缺少数组绑定特性。

8.2.5.4. BDE引擎

  Borland Database Engine (BDE)是Delphi早期版本附带的基于windows核心的数据库引擎和连接软件。即便它被弃用,自2000年以来被DBExpress所取代,但它仍然是一个工作解决方案,易于与SynDB.pas驱动进行接口。

Oracle
DB2
MySQL
Postgresql
Firebird
Sybase
Jet/Access
Advantage
MS SQL
Interbase
Paradox
Infomix
SynDB
DB.pas
TDataset
BDE
ODBC

  请不要在任何新项目上使用BDE !
  您最好切换到另一个访问库。

8.2.6. 通过HTTP远程访问

  SynDBRemote.pas单元允许您创建在远程HTTP服务器上执行SQL操作的数据库应用程序,而不用在数据库服务器上。您可以像任何其他SynDB.pas一样创建连接数据库,但传输将通过HTTP进行。因此,不需要在最终用户应用程序上部署数据库客户端:它只会使用HTTP请求,包括在Internet上也是如此。您可以使用SynDB.pas类的所有特性,可以轻松地实现一个优化的HTTP连接。

Office 2
Server
Office 1
Remote B
Remote A
direct
local network
HTTP
HTTPS
Internet
Client 3
SynDBRemote
Server
DB
Client 1
Client 4
Client 2

  这个特性不是RESTful ORM的一部分,因此不使用mORMot.pas单元,它有自己的优化协议,使用增强的安全性(传输加密与用户身份验证和可选的HTTPS)和自动数据压缩。仅使用了SynCrtSock.pas单元的HTTP客户端和服务端类。

  由于您的应用程序可以同时使用TDataSet(请参阅TDataSet和SynDB)和TQuery(请参阅TQuery仿真类),这种新的传输方式可以轻松地将现有Delphi客户端-服务端应用程序转换为多层体系结构,而只需对源代码进行很小的改动。而对于您的新代码,您可以使用mORMot的RESTful功能切换到SOA/ORM设计。

  传输协议采用优化的二进制格式,同时在两端进行压缩、加密和数字签名,远程用户身份验证将通过挑战验证方案执行。如果需要,还可使用http.sys`内核模式发布HTTPS服务。

8.2.6.1. 服务端和客户端类

  为发布您的SynDB.pas连接,您需要初始化一个在SynDBRemote.pas中定义的TSQLDBServer*类:

TSQLDBServerHttpApi
TSQLDBServerAbstract
TSQLDBServerSockets

  您可以基于TSQLDBServerSockets套接字API 定义HTTP服务器,也可以基于TSQLDBServerHttpApi类(仅Windows)定义更快、更稳定的HTTP服务,是基于Windows XP版本之后的http.sys内核模式的HTTP服务。

  对于客户端,您可以使用SynDBRemote.pas中定义下列类:

TSQLDBWinHTTPConnectionProperties
TSQLDBHttpRequestConnectionProperties
TSQLDBWinINetConnectionProperties
TSQLDBCurlConnectionProperties
TSQLDBSocketConnectionProperties
TSQLDBHTTPConnectionPropertiesAbstract

  注意,TSQLDBHttpRequestConnectionProperties是一个抽象的父类,因此不应该直接实例化它,而应该实例化它的派生实现。

  如您所见,您可以在普通套接字API客户端,WinINetWinHTTP(在Windows下)客户端,libcurl API(主要在Linux上)之间进行选择。在Windows上,TSQLDBWinHTTPConnectionProperties类在Internet上更稳定,即使普通套接字在localhost上的数据访问基准测试给出的数据更好。

8.2.6.2. 通过HTTP发布一个SynDB连接

  可以这样定义HTTP服务:

uses SynDB, // RDBMS core
     SynDBSQLite3, SynSQLite3Static, // static SQLite3 engine
     SynDBRemote; // for HTTP server
 ...
var Props: TSQLDBConnectionProperties;
    HttpServer: TSQLDBServerAbstract;
 ...
  Props := TSQLDBSQLite3ConnectionProperties.Create('data.db3','','','');
  HttpServer := TSQLDBServerHttpApi.Create(Props,'syndbremote','8092','user','pass');

  上面的代码将初始化到本地data.db3的SQlite3数据库连接(在Props变量中),然后通过http.sys的HTTP内核模式将服务发布到http://1.2.3.4:8092/syndbremote URI,假定服务器的IP是1.2.3.4。

  首先,使用'user'/'pass'要素定义了用户。注意,在我们的远程访问中,用户管理与RDBMS用户权限不一致:您最好在应用程序级别拥有自己的一组用户,以获得更高的安全性,并更好地与业务逻辑集成。如果想在RDBMS上创建新用户可能会很痛苦,但在SynDBRemote.pas侧,通过Protocol.Authenticate属性,管理远程用户身份验证则非常容易:

HttpServer.Protocol.Authenticate.AuthenticateUser('toto','pipo');
HttpServer.Protocol.Authenticate.AuthenticateUser('toto2','pipo2');
...

  您还可以依照用户逐一认证所述的方法共享mORMot的REST身份验证用户,方法是将缺省的TSynAuthentication类实例替换为TSynAuthenticationRest,就像在mORMot.pas中定义的那样。注意,同时使用SynDBRemote和mORMot的ORM/SOA听起来像是一种弱设计,但是在处理遗留代码和许多现有SQL语句时可能会有它的优点。

  URI应该进行注册,就像http.sys API所期望的那样。您可以使用系统管理员权限运行服务端,或者在安装应用程序中调用以下方法(就像我们在TestSQL3Register.dpr中所做的那样):

THttpApiServer.AddUrlAuthorize('syndbremote','8092',false,'+');

8.2.6.3. 通过HTTP访问SynDB客户机

  在客户端,你可以这样写:

uses SynDB, // RDBMS core
     SynDBRemote; // for HTTP client
 ...
var Props: TSQLDBConnectionProperties;
 ...
  Props := TSQLDBWinHTTPConnectionProperties.Create('1.2.3.4:8092','syndbremote','user','pass');

  如您所见,在客户端无需连接到SynDBSQLite3.pasSynSQLite3Static.pas,只需要HTTP链接。不需要在应用程序中部署RDBMS客户端库,也不需要设置本地网络防火墙。

  我们在这里定义了一个具有'user' / 'pass'凭据的单个用户,但是您可以使用TSQLDBServerAbstractProtocol.Authenticate属性在服务器端管理更多的用户。。

  然后,像往常一样使用连接执行您最喜欢的SQL:

procedure Test(Props: TSQLDBConnectionProperties);
var Stmt: ISQLDBRows;
begin
  Stmt := Props.Execute('select * from People where YearOfDeath=?',[1519]);
  while Stmt.Step do begin
    assert(Stmt.ColumnInt('ID')>0);
    assert(Stmt.ColumnInt('YearOfDeath')=1519);
   end;
end;

  或者可以通过SynDBVCL.pas单元与VCL组件一起使用:

  ds1.DataSet.Free; // release previous TDataSet
  ds1.DataSet := ToDataSet(ds1,Props.Execute('select * from people',[]));

  TSynSQLStatementDataSet结果集将直接映射TSQLDBServer*类返回的原始二进制数据,从而避免客户端应用程序中各种缓慢的数据编组,即使是大型内容。所有的数据都是由服务端计算和发送的:即使只在TDBGrid中显示一行,数据也是服务端传输的。事实上,只检索部分数据在本地网络上工作得很好,但在Internet上大量传输却不是一个好主意,因为太多的ping操作。因此,可以考虑添加一些筛选字段或一些应用程序级分页,以减少需要从SynDBRemote服务端检索的行数。

  如果服务端类定义了自己的TSynAuthentication类(如通过TSynAuthenticationRest使用REST用户和组),您应该创建自己的类,并覆盖以下方法:

procedure TSQLDBWinHTTPConnectionPropertiesRest.SetInternalProperties;
begin
  if fProtocol=nil then
    fProtocol := TSQLDBRemoteConnectionProtocol.Create(
      TSynAuthenticationRest.Create(nil,[]));
  inherited;
end;

  该重载方法从TSQLDBWinHTTPConnectionProperties继承其所有行为,但使用ORM/SOA身份验证方案在服务端验证其用户。

8.2.6.4. 高级用例

  您可以使用这个远程连接特性,如将一个独立的共享SQLite3数据库升级为高性能、低维护的客户端-服务器数据库引擎。您可以在服务器端创建它:

var props: TSQLDBSQLite3ConnectionProperties;
    server: TSQLDBServerHttpApi;
...
  props := TSQLDBSQLite3ConnectionProperties.Create('database.db3','','','');
  props.MainSQLite3DB.Synchronous := smOff;
  props.MainSQLite3DB.LockingMode := lmExclusive; // tune the performance
  server := TSQLDBServerHttpApi.Create(props,'syndbremote','8092','user','password');
...

  您可以通过创建如下属性来共享现有的SQlite3数据库实例(如创建用于RESTful ORM的TSQLRestServerDB,请参阅数据库层):

  props := TSQLDBSQLite3ConnectionProperties.Create(aRestServerDB.DB);
  server := TSQLDBServerHttpApi.Create(props,'syndbremote','8092','user','password');

  当使用http.sys内核模式服务,如果数据库名称(即这里的'syndbremote')与ORM表或方法服务不冲突,那么在普通ORM/SOA操作(对于纯粹的HTTP服务可能是80)和远程SynDB访问之间可以共享相同的IP端口。

  您还可以在服务器和服务端设置自己的TSQLDBProxyConnectionProtocol类来定制传输协议。

8.2.6.5. 与SynDBExplorer集成

  我们的SynDBExplorer工具能够将任何SynDB连接作为HTTP服务发布,或者通过HTTP连接它。这样可能非常方便,包括调试来说也是。

  要为现有数据库提供服务,只需像往常一样连接它,然后单击列表左下方的"HTTP Server"按钮。您可以调整服务端属性(HTTP端口、用于URI的数据库名称、用户凭据),然后单击"Start" 按钮。

  要连接到这个远程连接,请再运行一个SynDBExplorer实例。,使用"Remote HTTP"作为连接类型创建一个新的连接,并根据服务端设置相应的选项值,替换默认的"localhost:8092"(替换localhost为服务器IP以通过网络访问)为服务器名称、"syndbremote" 为数据库名称、"synopse"为用户名和密码。

  这样就能远程访问主服务器实例,就像通过常规客户端访问数据库一样。

  如果服务端数据库是SQLite3,那么您只需将这个本地引擎升级为一个真正的客户端-服务端数据库,您可能会对这样做的性能感到惊讶。

8.2.6.6. 别忘了还有mORMot!

  即使你可以使用这样的远程访问来实现一个n层架构,但您还是应该使用mORMot的客户端-服务端ORM,这样会提供更好的客户端-服务端集成,领域驱动设计模式的无感持久性,更好的OOP和SOLID模型设计,比原始SQL操作更高的性能。我们轻量化的mORMot不是简单在上面添加了数据传输层的ORM:它是一个完整的RESTful系统,是真正的SOA设计。

  但是为了将一些遗留的SQL代码集成到一个新的架构,SynDBRemote.pas有它的优点,可以与mORMot的高级特性结合使用。

  请注意,对于跨平台客户端,mORMot的ORM/SOA模式是一种更好的方法:不要在移动应用程序中使用SQL,而是使用服务,这样您就不需要对业务逻辑进行各种小的修改之后重新验证并将应用程序重新发布到商店!

8.3. SynDB ORM集成

8.3.1. 代码优先还是数据库优先

  在使用任何对象关系映射(ORM)时,您主要有两种可能:

  • 从头开始,即编写你的类,让ORM创建所有的数据库结构,它将直接反映对象的属性,这称之为“代码优先”;
  • 使用现有数据库,然后在模型中定义类如何映射现有数据库结构,这是“数据库优先”。

  我们的mORMot框架两条路径都支持,即使像其他ORM一样,代码优先听起来更直接。

8.3.2. 代码优先

  外部记录可以按照mORMot的ORM所要求的那样定义:

type
  TSQLRecordPeopleExt = class(TSQLRecord)
  private
    fData: TSQLRawBlob;
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fYearOfBirth: integer;
    fYearOfDeath: word;
    fLastChange: TModTime;
    fCreatedAt: TCreateTime;
  published
    property FirstName: RawUTF8 index 40 read fFirstName write fFirstName;
    property LastName: RawUTF8 index 40 read fLastName write fLastName;
    property Data: TSQLRawBlob read fData write fData;
    property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
    property YearOfDeath: word read fYearOfDeath write fYearOfDeath;
    property LastChange: TModTime read fLastChange write fLastChange;
    property CreatedAt: TCreateTime read fCreatedAt write fCreatedAt;
  end;

  可见,这与内部ORM类没有区别:它继承了TSQLRecord,您也可以从TSQLRecordMany继承,以便使用基于数据透视表的ORM实现

  唯一的区别是index 40属性定义了FirstNameLastName的发布属性:定义了创建文本列外部字段时使用的长度(UTF-16 WideChar或UTF-8编码):

    property FirstName: RawUTF8
      index 40
      read fFirstName write fFirstName;

  事实上,SQLite3并不关心文本字段的长度,但是几乎所有其他数据库引擎都希望在表中定义VARCHAR列时指定最大长度。如果您没有在字段定义中指定任何长度(如没有定义index ???属性), ORM将创建一个长度不受限制的列(如MS SQL Server的varchar(max))。在这种情况下,代码可以工作,但是性能和磁盘效率可能会大大降低,通过CLOB访问的速度明显变慢。这种性能损失的唯一例外是SQlite3和PostgreSQL,对于它们,无限制的文本列的处理速度与varchar(#)一样快。

  默认情况下,ORM不会执行任何检查,来确保字段长度符合外部数据库中的列大小要求。您可以使用TSQLRecordPropertiesSetMaxLengthValidatorForTextFields()SetMaxLengthFilterForTextFields()方法在发送数据到外部数据库之前先执行验证和筛选规则,请参阅筛选和验证

  以下是对于外部数据库的回归测试摘要:

var RExt: TSQLRecordPeopleExt;
  (...)
fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');
aExternalClient := TSQLRestClientDB.Create(fExternalModel,nil,'testExternal.db3',TSQLRestServerDB);
try
  aExternalClient.Server.StaticVirtualTableDirect := StaticVirtualTableDirect;
  aExternalClient.Server.CreateMissingTables;
  Check(aExternalClient.Server.CreateSQLMultiIndex(
    TSQLRecordPeopleExt,['FirstName','LastName'],false));
  (...)
  Start := aExternalClient.ServerTimestamp;
  (...)
  aID := aExternalClient.Add(RExt,true);
  (...)
  aExternalClient.Retrieve(aID,RExt);
  (...)
  aExternalClient.BatchStart(TSQLRecordPeopleExt);
  aExternalClient.BatchAdd(RExt,true);
  (...)
  Check(aExternalClient.BatchSend(BatchID)=HTTP_SUCCESS);
  Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
  Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
  (...)
  RExt.FillPrepare(aExternalClient,'FirstName=? and LastName=?',
    [RInt.FirstName,RInt.LastName]); // query will use index -> fast :)
  while RExt.FillOne do ...
  (...)
  Updated := aExternalClient.ServerTimestamp;
  (...)
  aExternalClient.Update(RExt);
  aExternalClient.UnLock(RExt);
  (...)
  aExternalClient.BatchStart(TSQLRecordPeopleExt);
  aExternalClient.BatchUpdate(RExt);
  (...)
  aExternalClient.BatchSend(BatchIDUpdate);
  (...)
  aExternalClient.Delete(TSQLRecordPeopleExt,i)
  (...)
  aExternalClient.BatchStart(TSQLRecordPeopleExt);
  aExternalClient.BatchDelete(i);
  (...)
  aExternalClient.BatchSend(BatchIDUpdate);
  (...)
  for i := 1 to BatchID[high(BatchID)] do begin
    RExt.fLastChange := 0;
    RExt.CreatedAt := 0;
    RExt.YearOfBirth := 0;
    ok := aExternalClient.Retrieve(i,RExt,false);
    Check(ok=(i and 127<>0),'deletion');
    if ok then begin
      Check(RExt.CreatedAt>=Start);
      Check(RExt.CreatedAt<=Updated);
      if i mod 100=0 then begin
        Check(RExt.YearOfBirth=RExt.YearOfDeath,'Update');
        Check(RExt.LastChange>=Updated);
      end else begin
        Check(RExt.YearOfBirth<>RExt.YearOfDeath,'Update');
        Check(RExt.LastChange>=Start);
        Check(RExt.LastChange<=Updated);
      end;
    end;
  end;
  (...)

  如上可见,使用本地SQLite3引擎或远程数据库引擎没有区别。

  从客户端角度看,您只需调用通常的RESTful CRUD方法,即Add() Retrieve() Update() UnLock() Delete(),或更快的Batch*()修订,甚至如带有复杂WHERE子句的FillPrepare、服务端的CreateSQLMultiIndex / CreateMissingTables等高级方法。

  在远程数据库中创建表('CREATE table…’SQL语句)是由框架在调用CreateMissingTables方法时执行,并根据数据库的要求(如SQLite3的文本将是Oracle的NVARCHAR2字段)使用适当的列属性。

  在外部数据库上创建的表如下:

ID : TIDData : TSQLRawBlobFirstName : RawUTF8LastName : RawUTF8YearOfBirth : integerYearOfDeath : wordID : INTEGERData : BLOBFirstName : NVARCHAR(40)LastName : NVARCHAR(40)YearOfBirth : INTEGERYearOfDeath : INTEGER

  唯一特别的指令是全局VirtualTableExternalRegister()函数,它必须运行在服务端(在客户端上运行它没有任何意义,因为客户端没有表,简单的说就是客户端不关心存储,这是服务端的事)。

  为了按预期工作,应该在TSQLRestServer构造函数之前调用VirtualTableExternalRegister()。当服务端初始化时,ORM服务器必须知道需要管理内部或外部数据库。在上面的代码中,TSQLRestClientDB.Create()将实例化它自己的嵌入式TSQLRestServerDB实例。

  注意,TSQLRecordExternal.LastChange字段被定义为TModTime:这样,每次记录更新时,都会存储当前日期和时间,也就是说,如每次的aExternalClient.AddaExternalClient.Update调用。测试代码使用循环来检查RExt.LastChange>=StartRExt.LastChange<=Updated,记录的时间是“服务器时间”,即当前服务器上的日期和时间,在外部数据库的情况下,是远程服务器的时间(在MS SQL将执行select getdate() 获取日期并插入LastChange)。为了获取服务端时间戳,应该调用Start := aExternalClient.ServerTimestamp而不是TimeLogNow本地时间函数。

  对CreatedAt 发布字段(定义为TCreateTime)也测试了类似的特性:它将在创建记录时自动设置为当前服务器时间(在修改时不会更改),这就是上述代码检查RExt.CreatedAt<=Updated的目的。

8.3.3. 数据库优先

  正如前面看到的,下面一行初始化ORM,以便使用外部数据库连接fProperties通过SQL访问TSQLRecordPeopleExt数据:

VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');

  我们还定制了外部表的名称,将默认的'PeopleExt'(通过从TSQLRecordPeopleExt去掉TSQLRecord前缀)改为'PeopleExternal'

  除了映射表名之外,ORM还能够将TSQLRecord的发布属性名映射为数据库的各种自定义列名。实际上,现有数据库的很多表没有显式的列名是非常普遍的,当直接映射为TSQLRecord属性名时,这听起来可能非常奇怪,甚至已有数据库的主键与ORM将其命名为ID的要求也不尽一致。

  默认情况下,对于代码驱动的方法,内部属性名称将与外部表列名匹配,请参见TSQLRecordPeopleExt代码优先的字段/列映射

  您可以对默认映射进行定制,例如:

fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');
fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB.
  MapField('ID','Key').
  MapField('YearOfDeath','YOD');
 (...) // the remaining code stays the same

  作为替代,您可以使用更简洁的VirtualTableExternalMap函数接口:

fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalMap(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal').
  MapField('ID','Key').
  MapField('YearOfDeath','YOD');

  之后像平常一样在Delphi代码中使用TSQLRecordPeopleExt表,包含IDYearOfDeath字段。

  但在底层,mORMot ORM将在创建所有需要的SQL语句时进行映射:

  • “内部”TSQLRecord类将存储在PeopleExternal外部表中;
  • “内部”TSQLRecord.ID字段将是一个外部“Key: INTEGER”列;
  • “内部”TSQLRecord.YearOfDeath字段将是一个外部“YOD: INTEGER”列;
  • 其他内部发布的属性将默认以相同的名称映射到外部列。

  因此,结果映射如下:

ID : TIDData : TSQLRawBlobFirstName : RawUTF8LastName : RawUTF8YearOfBirth : integerYearOfDeath : wordKey : INTEGERData : BLOBFirstName : NVARCHAR(40)LastName : NVARCHAR(40)YearOfBirth : INTEGERYOD : INTEGER

  注意,只有IDYearOfDeath列名是定制的。

  由于SQLite3虚拟表的设计,以及目前mORMot内部构件,数据库主键必须是一个整数字段,以便按照ORM的要求进行映射。但是您可以通过stored AS_UNIQUE代码定义各种辅助键,如对文本字段。

8.3.4. 与遗留代码共享数据库

  您很可能必须基于现有的数据库,使用许多已经编写好的SQL语句来维护和发展遗留项目,请参阅遗留代码和项目。如您希望将mORMot用于新特性,添加移动或HTML客户端。

  在这种情况下,ORM高级特性(如ORM缓存或批处理)可能与遗留代码冲突,因为可能需要共享表。在处理这样一些项目时,这里有一些指导原则。

  为了详尽地说明这个问题,我们需要考虑每一个ORM CRUD操作。我们可能需要将它们分为三类:查询、插入和对现有数据的修改。

  关于ORM查询,即Retrieve()方法,ORM缓存可以根据表进行调优,您肯定缺少一些缓存,但请记住:

  • 您可以为该缓存设置一个“超时”周期,以便在大多数情况下仍可从中获益;

  • 您在服务级有一个缓存,在客户级有另一个缓存,因此您可以对其进行优化,使其在客户端上不那么具有侵略性;

  • 您可以根据每个ID优化ORM缓存,以便仍然可以缓存一些不太可能更改的项目。

  关于ORM插入,即Add()BatchAdd()方法,在使用外部引擎时,如果一些外部进程可能插入新行,请确保将TSQLRestStorageExternal EngineAddUseSelectMaxID属性设置为TRUE,以便手工计算下一个最大ID。

  但这仍然可能是一个问题,因为外部进程可能在ORM插入期间执行插入。

  因此,最好不要使用ORM Add()或BatchAdd()方法,而是依赖于专用的INSERT SQL语句,如托管在服务端基于接口的服务中。

  关于ORM修改,即Update() Delete() BatchUpdate() BatchDelete()方法,它们听起来很安全,可以与修改数据库的外部进程一起使用,只要您使用事务使修改成为原子的,并且不会与遗留代码中的任何并发修改冲突。

  在处理一些遗留代码将在后台修改的外部表时,可能更安全的模式是绕过这些ORM方法,并定义基于服务端接口的服务。这些服务可能包含手动SQL,而不是使用神奇的ORM。但是这将取决于您的业务逻辑,并且您将无法从框架的ORM特性中获益。

  然而,在应用程序中引入面向服务的体系结构(SOA)将非常有益:ORM不是强制性的,特别是如果您熟悉SQL查询,知道如何使它们尽可能地标准化,拥有大量遗留代码,并且可能已经调优了SQL语句。

  要将新的客户端(如移动应用程序或AJAX现代站点)接入到应用程序,必须引入SOA。公平地说,您不应该再直接访问数据库,就像以前使用Delphi应用程序和RAD DB组件那样。

  所有新特性,包括用于存储新数据的新表,仍然受益于mORMot的ORM,并且仍然可以托管在相同的外部数据库中,与现有代码共享。

  然后,您将能够在遗留代码中识别seam(请参阅遗留代码和项目),并将它们移动到新的mORMot服务中,然后让您的应用程序演化为新的SOA/MVC体系结构,而不会破坏任何东西,也不会从头开始。

8.3.5. 自动映射SQL冲突字段名称

  如果应用程序可能运行在多个数据库上,那么从一个引擎切换到另一个引擎时,可能很难处理各种潜在的字段名称冲突。因此,ORM将确保没有字段名与底层数据库的SQL关键字冲突。

  在代码优先模式下,您可以使用以下方法来确保不会发生这种冲突:

fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB.MapAutoKeywordFields;

  对于数据库优先的数据库,将使用以下语法来检查字段名:

fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB. // custom field mapping
    MapField('ID','Key').
    MapField('YearOfDeath','YOD').
    MapAutoKeywordFields;

  或者使用简洁完整的接口定义:

VirtualTableExternalMap(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal').
    MapField('ID','Key').
    MapField('YearOfDeath','YOD').
    MapAutoKeywordFields

  对于数据库优先的数据库,在任何手动字段映射之后调用MapAutoKeywordFields方法是一个好主意,因为即使是自定义字段名称也可能与SQL关键字冲突。

  如果一些字段名可能与SQL关键字冲突,那么它将映射为后跟'_'。如'Select'发布属性将映射为表中的SELECT_列。

  即使这个选项默认情况下是禁用的,日志中也会出现一条警告消息,建议使用这个MapAutoKeywordFields方法,以帮助您识别此类问题。

8.3.6. ORM处理外部数据库

  mORMotDB.pas单元为框架基于SynDB.pas的外部数据库实现虚拟表访问。

  实际上,这个特性将使用TSQLRestStorageExternalTSQLVirtualTableCursorExternalTSQLVirtualTableExternal类,定义如下:

TSQLRecordVirtualTableAutoID
TSQLRecordVirtual
TSQLRecord
TSQLVirtualTableCursorExternal
TSQLVirtualTableCursor
TSQLVirtualTableExternal
TSQLVirtualTable

  类的注册是通过调用以下新的全局过程完成的:

procedure VirtualTableExternalRegister(aModel: TSQLModel; aClass: TSQLRecordClass;
  aExternalDB: TSQLDBConnectionProperties; const aExternalTableName: RawUTF8);

  该程序将在服务器端为ORM类注册一个外部数据库:

  • 它将定义提供的类的行为,类似于TSQLRecordVirtualTableAutoID类(如在这个ORM模型中,TSQLModelRecordProperties.Kind属性将被覆写为rCustomAutoID
  • 它将提供的类与TSQLVirtualTableExternal模块关联;
  • TSQLDBConnectionProperties实例应该由所有类共享,并在ORM不再需要时全局释放;
  • 正如外部数据库所希望的那样,这里应该提供完整的表名(当通过关联的SQLite3虚拟表调用时,SQLTableName将在内部用作表名),如果没有指定表名("),将使用SQLTableName(如名为TSQLCustomer的类,使用'Customer');
  • 内部动态地将SQL从内部ORM表示转换为外部SQL需要的格式(如表名或ID属性),请参见TSQLRestStorage.AdaptSQLForEngineList方法。

  典型用法如下:

aProps := TOleDBMSSQLConnectionProperties.Create('.\SQLEXPRESS','AdventureWorks2008R2','','');
aModel := TSQLModel.Create([TSQLCustomer],'root');
VirtualTableExternalRegister(aModel,TSQLCustomer,aProps,'Sales.Customer');
aServer := TSQLRestServerDB.Create(aModel,'application.db'),true)

  如对象关系映射所述,其余所有代码将使用普通ORM类、方法和函数。

  为了存储在外部数据库中,ORM记录可以从各种TSQLRecord类继承。即使这个类没有从TSQLRecordVirtualTableAutoID继承,一旦为该类调用了VirtualTableExternalRegister函数,它也会这样做。

  与任何普通的TSQLRecord类一样,ORM核心希望外部表映射一个Integer ID发布属性,在每次插入记录时自动递增。由于并非所有数据库都处理此类字段,如Oracle自动增量将通过在初始化时执行select max(id) from tablename语句处理,然后通过线程安全缓存动态计算新插入使用的RowID。

  您不必知道数据持久性存储在哪里以及如何存储,框架将为您完成所有底层的数据库工作。由于SQlite3的虚拟表特性,内部和外部表可以在SQL语句中混合使用。根据实现需要,类可以通过内部SQLite3引擎或通过外部数据库持久化,只需在服务端初始化之前调用VirtualTableExternalRegister()即可。

  实际上,TSQLVirtualTableCursorExternal会根据外部数据库已有的索引,将外部表上的任何查询转换为正确优化的SQL查询。TSQLVirtualTableExternal还将把SQLite3级上的单个SQL修改语句(如insert / update / delete)转换为外部数据库的远程SQL语句。

  大多数情况下,所有RESTful方法(GET/POST/PUT/DELETE)都将由TSQLRestStorageExternal类直接处理,不会使用虚拟表机制。在实践中,对外部数据库的大多数访问都与直接访问一样快,但是虚拟表总是能够解释任何跨数据库的复杂请求或语句。

  直接REST访问将按照以下方式处理,如添加对象时:

internal
table
external
table
REST
INSERT INTO...
internal engine
INSERT INTO...
ODBC/ZDBC/OleDB...
TSQLRestServerDB.Add
TSQLRestServerDB.EngineAdd
TSQLRestStorageExternal.EngineAdd
TSQLRequest
SQlite3 engine
SQLite3 file
ISQLDBStatement
External DB client
External DB server

  通过虚拟表的间接访问将处理如下:

internal or
external table
INSERT INTO...
internal engine
internal
table
external
table
INSERT INTO...
ODBC/ZDBC/OleDB...
TSQLRestServerDB.Add
TSQLRestServerDB.EngineAdd
TSQLRequest
SQlite3 engine
SQlite3 file
TSQLVirtualTableExternal.Insert
ISQLDBStatement
External DB client
External DB server

  关于速度,这里是回归测试日志文件的摘录(见前一段的代码),它显示了RESTful调用和虚拟表调用之间的区别,处理超过11,000行数据:

  - External via REST: 133,666 assertions passed  409.82ms
  - External via virtual table: 133,666 assertions passed  1.12s

  第一次运行使用TSQLRestServer.StaticVirtualTableDirect设置为TRUE(这是默认设置),也就是说,它将直接调用TSQLRestStorageExternal来执行RESTful命令;第二次将把这个属性设置为FALSE,也就是说,它将调用SQLite3引擎,并通过虚拟表机制将其转换为另一个SQL调用。

  值得指出的是,该测试使用内存SQLite3数据库(即通过SQLITE_MEMORY_DATABASE_NAME伪文件名实例化)作为外部数据库,因此我们在这里测试的主要是ORM开销,而不是外部数据库速度。对于真正的基于文件或远程数据库(如MS SQL),远程连接的开销远远大于虚拟表的使用。

  在所有情况下,默认的StaticVirtualTableDirect=true将确保最佳性能。正如数据访问基准测试所述,使用虚拟或直接调用不会影响CRUD操作速度:只要可能,它将绕过虚拟引擎。

8.3.7. 进程优化

  下面将详细介绍服务端的多线程能力以及所有可用的设置。

  默认情况下,所有ORM读操作将在并发模式下运行,所有ORM写操作将在阻塞模式下执行。所以使用我们的内部SQLite3引擎或大多数外部数据库,既安全又快速。但您可能需要更改此默认行为,具体取决于所连接的外部引擎。

  通常TSQLDBConnectionProperties将从TSQLDBConnectionPropertiesThreadSafe继承,因此将为每个连接创建一个线程,这是有效的,但是一些驱动可能会有问题。

  首先,一些数据库客户端库可能不允许在多个线程之间共享事务,如MS SQL。其他客户端可能会为每个连接消耗大量资源,或者可能没有良好的多线程伸缩能力。有些数据库服务端确实为每个连接的客户端分配不同的进程,如PostgreSQL:您可能希望只使用一个连接来减少服务端资源,因此服务端只运行一个进程。为了避免这些问题,您可以强制所有ORM写操作在一个专用线程中执行,即通过设置amMainThread(这在没有UI的服务端不太合适),或者更好地通过amBackgroundThreadamBackgroundORMSharedThread

 aServer.AcquireExecutionMode[execORMWrite] := amBackgroundThread;

  其次,特别是在长时间运行的n层mORMot服务端,可能会出现连接异常中断。如一个晚上没有任何活动之后,对外部数据库的访问可能在早上失败,因为数据库服务器可能断开了连接。

  您可以使用TSQLDBConnectionProperties.ConnectionTimeOutMinutes属性指定最大不活动时间,在此之后将刷新和重新创建所有连接,以避免潜在的断开连接问题。

  在实践中,在一段时间后重新创建连接是安全的,不会减慢进程,相反,它可能有助于减少消耗的资源,并稳定长时间运行的n层服务器。

  ThreadSafeConnection方法将检查其TSQLDBConnectionProperties实例上的最后一个活动,然后调用ClearConnectionPool,以便在空闲时间过长时释放所有活动连接。

  因此,如果您使用ConnectionTimeOutMinutes属性,您应该确保后台没有其他连接仍然处于活动状态,否则可能会发生一些意外问题。

  例如,您应该确保您的mORMot ORM服务器在读写的阻塞模式下运行所有语句:

aServer.AcquireExecutionMode[execORMGet] := am***;
 aServer.AcquireExecutionMode[execORMWrite] := am***;

  在这里,安全阻塞am***模式是除amunlock外的任何模式,即amLockedamBackgroundThreadamBackgroundORMSharedThreadamMainThread